feat(netty-demo): netty-demo

netty-demo
This commit is contained in:
ovo 2024-12-09 22:22:26 +08:00
parent 2a337b68b2
commit 7e51730e60
26 changed files with 713 additions and 34 deletions

1
86C111T1K131FJQU.dat Normal file
View File

@ -0,0 +1 @@
EWDRGAHMADDPERCSGJDUHABIENHHJEEAFRFSATFTEFDSGZEQCYEOCOIRABDOEMEQAYAAABHEBBDWCMGEARHUJOGHBAJUFJJNFDGFAHDBJMGUBCIZBIEHIKDWBRAZIAINIGBFFFEKGQBUJGELGZIYBRGTHAEHHZCUEQJBAOGJAZBLACAFDFJKALCECDGKCQIYDJDODSDNDTIDHQJSDFFADLJLBNFAAAIODQEEILGGGVGEEKFSJRACFZDZIQEOILBLDXCRCPAOEBEDIPDDBNHCDJCKEGEJBHHBFKDNAOFOCCIQIBGVBSGPAJCDGTEMHGBCHYGLJOCPDYGWIFGIDXBUIHEKETDIFEGQHVGGFJAKAVHNJLEKBXJAFXDNHBGQAZDRHZGOGXHAALCXEZCNGLGHGOIDDKGCIYDSFJFNGIAQGMAHCXAFAVDVBPFMHSJSCODLHACXBHGUJEJLGLEREPBOEFCJDQDIDOIEFIHLCICOBWBTCAHOJTENCYDSBDGLFQBPIWCBGJDDIRCCDNEJITEJFHIWJEGLBGDMJRDGCFIFJCENCGCXHUCJGZGXAWAJCYDFIKJSIPDHFPBHGLCLGVFOEUAUIKFRISFQEXEPGEDVIFFGDOBNBFIMAUJOHJJNJAEQAMHBJVDTIUDDHBAQILFXHOGEFXIPBMDUCKCTCWGRFZIYFOENDJHPCTIYIXDXAEDGHNHTJQCWCQHAEVBDGLAZEQDPIQGIEVHNDUCUHMBMIVJKDNDJDJFGBAADHDIWJEGJJFDAEKALBMDOHTAQFFIDIBJVHQDNBXIABPIJCLAIBHHNIOGKDUCUCSAXDUCMDMGYAMBUEUBOAEATCMGBBJAOJKFRCYEXCJIZDKFAFCAJAGDYJGGPDKCJAPDWFIGAITHCEOFTIJEQGUGQHKAAHGFFEWGHDDEMDCHSAYJNCGIAJUHGDLAKFGHCGLEHHEAPENDVBAIOFEGOBKIKERHNFSIRCCAHHUBMGXDYATBYADEZCLCBDEDUHCJRESHMJCJVFTGLJJDYDYAFIMBCJKAOHEAHBMHMASIHCMGEGREQEUBSHFEECBJOJUBKDXGWJVADHLECJHHAADBKGCFQGIAYEHAJBFCVBTILJBIKBPAYFFFPCQEWJB

1
ArcFacePro64.dat Normal file
View File

@ -0,0 +1 @@
EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTGFGBEAAWGLBZAUCNHCCPBZCIIKBJATGYARHRAZHXFRBIEBCHIXAFDLFQBZFVJQGTCEHDIMIVGQEJAJIYHXHTISIVETBACMCCGFDPEKDQHYGGAPFXIOCJAAGOHYIFHNHZHWIGCZIGHHDMDXHQGTFLJOHGAYIVBGIHIQHHHJDJFWHMCRHJAMHZESGWGGAUJRGDHAGHIQITIJIUAXAEIZGBGZCCHJAHASGNCVIIJAIYFOEQGFELEIEDECJTCXBCAPIKHTFTHBAGJTERHQGSAFEUDIHLDDITDPIMACAOGTIIHFEMHLHIHYJTFDFDGWAKHJEIEIJFGABQJCIHIADYCXAHIMJTHOASFLGFFIBJJEHDHLHOCMDIGGDOJDHNBQCJENFUBAGOAZITIRJSFBBUCUHABLHRFVIUBWCADJCMDXDPATBPJSJRJLBRABGVFTDNFOAQDOARDEBRHJAQGYHIGMBDCJCSJKFBBLGECAEFEYCVCHAEAZJRIOFEHLCJILEHJVGYIVCWGHCMGJGLBTFMHFCAEAAUJQJLAEARDHDFHDBJJGALEHFNGSAIHOJUBOEAJDDFFYFTINITHTBNIJFDHLEAFGBFHFFQGHGFGREVFHFDCZGYEVBWAZDSCAGLDMIAAEFOAXIXFECSFQDWHFFHCFASFSGAHVJSDBBZJQAZBXARILBAJFJEHCANAIABBMECBJJFIOGYGHBXCUCVBDJOCYBZDZAJEXAXEPFRFOGVHQAOJLCYBOHFEKJFIJBDHDDCEAAUJVDRIGGGGCJOFVECAHAQFSBSGYJVGKCQDDHPGUCIARFAIEJGGDITAUDIIVBBJUEFCIDTGJGYJODRDEJPBMFNCXAKCPAIGOJQGHBZHQJUCOBKCKDPJSGGCVCAIWFVHIIEAJJMEFGRHZDEDACFBQJODGDVJBAUBXGKGCFZERAHIOALGKGAILDNHQGGAZDEIGATBTCWHMDKGSIWFMHAIZHREBJBEFENDFBRBLGLCMERJAEOBXCNDBHVCSJBDMEHCLJLCFFOGVGWATBOJBFJEQETHGESEXFDIIFDAGJPDNHEDSFNBRIVFMFPGOEEIHEFCOCKJGJAIZJIFTIGAWITGWDXGBEFDTJHFXBF

83
docs/rtc.md Normal file
View File

@ -0,0 +1,83 @@
很久以前,人类以为只有神仙可以听到、看到千里之外的声音和景象,称之为千里眼和顺风耳,短短几百年里,人类的技术革命实现了质的飞跃。
1876 年,贝尔电话的发明,使人类可以听到千里之外声音的梦想终于成真。
此后,音视频技术不断发展。一方面,视频压缩技术从 H.261 到 H.264,再到现在的 H.265 及AV1视频压缩率越来越高音频压缩技术也从电话使用的 G.711、G.722 等窄带音频压缩技术,发展到现代的 AAC、OPUS 等宽带音频压缩技术。
另一方面,从中国 3G 网络正式商用开始,移动网络也发生了翻天覆地的变化。从 3G 到 4G ,再到马上要落地的 5G移动网络的带宽和质量越来越高为音视频数据传输打下了坚实的基础。
尤其是 2011 年 Google 推出 WebRTC 技术后,大大降低了音视频技术的门槛,可以在浏览器上快速开发出各种音视频应用。
如今在疫情的三年里视频会议远程会诊线上教学等需求将RTC技术推向高潮成为影响社会发展不可或缺的技术之一。
2023年从用3W法学习解构RTC开始笔者也开启了RTC分享之路。
1.什么是RTC?
RTC是Real-Time Communication的缩写译为实时通信目的是在设备端实时的转发音视频多媒体数据让用户能实时的进行音频和视频的会话即基于 IP 技术实现的实时交互的音视频通信技术。
具体涵义如下:
▪ 实时音视频数据传输的延迟要达到“实时”的标准也就是说延时要小于400ms能够实现低延时和无卡顿在正常通信过程中基本感受不到延迟的存在。
▪ 音视频:音视频数据传输,实时音视频通信通过服务端为中转节点,即时采集、渲染、处理、传输终端用户的图像、视频、音频数据进行,实现音视频流数据在终端节点间完成通信的过程。
▪ 实时音视频服务商一般以SDK的形式提供一整套解决方案。
2.为什么选择RTC技术
痛点:
基础音视频流程复杂且广泛:涵盖音视频收集、音视频压缩/解码、数据传输、终端适配、视频分发等系列环节,每个环节展开,都是复杂技术点。企业若想打造自主实时音视频方案,不仅要养一定规模软硬研发团队,还要花费一定时间沉淀,对于该企业来说,成本太高。
RTC技术优势
高音质
基于专有回声消除&降噪技术,可在嘈杂的环境下实现高音质通话,让对话里语音听得比较清晰,没有回声、啸叫的状况出现。
高画质
视频支持超高清晰度画面,一路视频提供多种分辨率,大屏幕可订阅更高分辨率提升视频通话体验,分分钟感受面对面交流感。
低延迟
全球通信节点支持支持实时性更好的UDP协议端到端延时低
抗弱网
自动增益控制&弱网丢包补偿技术 ,在丢包下保持音视频通话流畅。
当然基于一些特定行业的应用场景比如多人数直播时的高并发医疗行业的网闸透传成熟RTC服务商都有着良好的技术沉淀让越来越多的企业使用这项技术。
3.RTC有哪些使用场景
随着移动互联网的普及和智能终端设备的广泛应用,实时音视频正逐渐成为主流互动方式已在在线教育、社交娱乐、互动电商等热门领域得到广泛应用也赋能于更多创新场景如金融、政企服务、loT、医疗等帮助人们享受更便捷和更人性化的生活服务。
协同办公-视频会议
丰富的会议场景,轻松实现远程办公系统,打通团队沟通渠道,帮助企业充分挖掘和整合隐形资源。
典型应用Zoom腾讯会议
社交沟通-聊天室
支持 1v1 通话或群聊功能,频道内用户可自由发言,适用于语音通话、语音群聊、语音聊天室等场景。
典型应用微信语音通话YY语音
游戏&娱乐
玩家可通过语音/视频聊天推进游戏进程,团战作战、协同作战,及时分享游戏信息,一起连麦开黑,拉近玩家距离。
典型应用:网易游戏,虎牙直播
电商直播
通过IM+音视频拓展多样化电商直播玩法,增强购物体验,提升获客率,让购物更有趣,促进电商平台交易转化,实现全球购物零距离。
典型应用:淘宝直播
在线教育
视频面对面教学真实还原线下教学场景支持1V1教学、1对多教学、双师课堂等多种互动教学模式。
典型应用:小鹅通
远程医疗
基于IM及实时音视频RTC通过实施互动技术实现优质医疗资源和知识共享满足远程会诊、手术示教多种场景需求。
典型应用:微医
视频双录
根据金融监管要求为客户提供多场景的双录服务提供柜面双录、远程双录、移动双录、AI自助双录帮助金融机构实现业务回溯。
典型应用招商银行app

21
pom.xml
View File

@ -244,16 +244,33 @@
</dependency>
<!-- Elasticsearch -->
<!-- <dependency>-->
<!-- <groupId>org.elasticsearch.client</groupId>-->
<!-- <artifactId>elasticsearch-rest-high-level-client</artifactId>-->
<!-- <version>7.17.9</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.elasticsearch</groupId>-->
<!-- <artifactId>elasticsearch</artifactId>-->
<!-- <version>7.17.9</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.17.9</version>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.17.9</version>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.14.0</version>
</dependency>
</dependencies>

View File

@ -0,0 +1,84 @@
package com.guwan.backend.client;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Slf4j
@Component
@RequiredArgsConstructor
public class Go2RTCClient {
private final RestTemplate restTemplate;
@Value("${go2rtc.api.url}")
private String apiUrl;
/**
* 创建流
*/
public void createStream(String streamId, String sourceUrl) {
String url = apiUrl + "/api/streams/" + streamId;
StreamConfig config = new StreamConfig(sourceUrl);
try {
restTemplate.postForEntity(url, config, String.class);
} catch (Exception e) {
log.error("创建流失败: {}", e.getMessage());
throw new RuntimeException("创建流失败", e);
}
}
/**
* 删除流
*/
public void deleteStream(String streamId) {
String url = apiUrl + "/api/streams/" + streamId;
try {
restTemplate.delete(url);
} catch (Exception e) {
log.error("删除流失败: {}", e.getMessage());
throw new RuntimeException("删除流失败", e);
}
}
/**
* 获取WebRTC Offer
*/
public String getOffer(String streamId, String sdp) {
String url = apiUrl + "/api/stream/" + streamId + "/webrtc";
WebRTCRequest request = new WebRTCRequest(sdp);
try {
ResponseEntity<WebRTCResponse> response =
restTemplate.postForEntity(url, request, WebRTCResponse.class);
return response.getBody().getSdp();
} catch (Exception e) {
log.error("获取WebRTC Offer失败: {}", e.getMessage());
throw new RuntimeException("获取WebRTC Offer失败", e);
}
}
@Data
@AllArgsConstructor
static class StreamConfig {
private String input;
}
@Data
@AllArgsConstructor
static class WebRTCRequest {
private String sdp;
}
@Data
static class WebRTCResponse {
private String sdp;
}
}

View File

@ -1,9 +1,19 @@
package com.guwan.backend.config;
import cn.easyes.starter.factory.IndexStrategyFactory;
import cn.easyes.starter.register.EsMapperScan;
import cn.easyes.starter.config.EasyEsConfigProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EsMapperScan("com.guwan.backend.es.mapper")
@EnableConfigurationProperties(EasyEsConfigProperties.class)
public class EasyEsConfig {
@Bean
public IndexStrategyFactory indexStrategyFactory() {
return new IndexStrategyFactory();
}
}

View File

@ -15,12 +15,6 @@ public class ElasticsearchConfig {
@Value("${easy-es.address}")
private String address;
@Value("${easy-es.username:}")
private String username;
@Value("${easy-es.password:}")
private String password;
@Bean
public RestHighLevelClient restHighLevelClient() {
String[] parts = address.split(":");

View File

@ -0,0 +1,19 @@
package com.guwan.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;
import java.time.Duration;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(10))
.setReadTimeout(Duration.ofSeconds(30))
.build();
}
}

View File

@ -13,6 +13,7 @@ public class SecurityConstants {
*/
public static final List<String> WHITE_LIST = List.of(
"/demo/**", // 测试接口
"/api/products",
"/api/user/register", // 用户注册
"/api/user/login", // 用户登录
"/api/user/getEmailCode", // 获取邮箱验证码

View File

@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
@ -51,15 +52,15 @@ public class DemoController {
(bucketName, minioUtil.uploadFile(bucketName, file))));
}
@GetMapping("demo111")
public int saveTenPerson() {
try {
try {
GetObjectArgs args = GetObjectArgs.builder()
.bucket("images")
.object("test/webwxgetmsgimg.jpg")
.bucket("videos")
.object("James Gosling.jpg")
.build();
//判断人脸照片是否合格
@ -86,7 +87,7 @@ public class DemoController {
faceRecognitionResDTO.setRect(faceInfo.getRect());
byte[] featureBytes = faceEngineService.extractFaceFeature(rgbData, faceInfo, ExtractType.REGISTER);
if (featureBytes != null) {
System.out.println("featureBytes = " + featureBytes);
System.out.println("featureBytes = " + Arrays.toString(featureBytes));
}else{
log.error("图片不合格,未检测到人脸");
return 2;

View File

@ -0,0 +1,80 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.dto.live.CreateRoomRequest;
import com.guwan.backend.dto.live.StartLiveRequest;
import com.guwan.backend.dto.live.WebRTCRequest;
import com.guwan.backend.dto.live.WebRTCResponse;
import com.guwan.backend.entity.LiveRoom;
import com.guwan.backend.service.LiveStreamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Tag(name = "直播管理", description = "直播相关接口")
@RestController
@RequestMapping("/api/live")
@RequiredArgsConstructor
public class LiveStreamController {
private final LiveStreamService liveStreamService;
@Operation(summary = "创建直播间")
@PostMapping("/room")
public Result<LiveRoom> createLiveRoom(@RequestBody CreateRoomRequest request) {
try {
LiveRoom room = liveStreamService.createLiveRoom(
request.getTitle(),
request.getDescription(),
request.getUserId()
);
return Result.success(room);
} catch (Exception e) {
log.error("创建直播间失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "开始直播")
@PostMapping("/room/{id}/start")
public Result<Void> startLive(
@PathVariable Long id,
@RequestBody StartLiveRequest request) {
try {
liveStreamService.startLive(id, request.getSourceUrl());
return Result.success();
} catch (Exception e) {
log.error("开始直播失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "结束直播")
@PostMapping("/room/{id}/end")
public Result<Void> endLive(@PathVariable Long id) {
try {
liveStreamService.endLive(id);
return Result.success();
} catch (Exception e) {
log.error("结束直播失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取WebRTC Offer")
@PostMapping("/room/{id}/webrtc")
public Result<WebRTCResponse> getWebRTCOffer(
@PathVariable Long id,
@RequestBody WebRTCRequest request) {
try {
String sdp = liveStreamService.getWebRTCOffer(id, request.getSdp());
return Result.success(new WebRTCResponse(sdp));
} catch (Exception e) {
log.error("获取WebRTC Offer失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -0,0 +1,63 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.dto.product.ProductDTO;
import com.guwan.backend.service.ProductSearchService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@Tag(name = "商品搜索", description = "商品搜索相关接口")
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
@Autowired(required = false)
private ProductSearchService productSearchService;
@Operation(summary = "保存商品")
@PostMapping
public Result<ProductDTO> save(@RequestBody ProductDTO product) {
try {
if (productSearchService == null) {
return Result.error("搜索服务未启用");
}
productSearchService.saveOrUpdate(product);
return Result.success(product);
} catch (Exception e) {
log.error("保存商品失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "搜索商品")
@GetMapping("/search")
public Result<List<ProductDTO>> search(
@Parameter(description = "搜索关键词") @RequestParam String keyword) {
try {
return Result.success(productSearchService.search(keyword));
} catch (Exception e) {
log.error("搜索商品失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "按分类查询商品")
@GetMapping("/category/{category}")
public Result<List<ProductDTO>> getByCategory(
@Parameter(description = "商品分类") @PathVariable String category) {
try {
return Result.success(productSearchService.getByCategory(category));
} catch (Exception e) {
log.error("查询商品分类失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -3,6 +3,7 @@ package com.guwan.backend.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.common.Result;
import com.guwan.backend.dto.video.VideoDTO;
import com.guwan.backend.dto.video.VideoUploadDTO;
import com.guwan.backend.service.VideoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -26,13 +27,10 @@ public class VideoController {
@Operation(summary = "上传视频", description = "上传视频文件并返回视频信息")
// @SecurityRequirement(name = "bearer-jwt")
@PostMapping("/upload")
public Result<VideoDTO> uploadVideo(
@Parameter(description = "视频文件") @RequestParam("fileUrl") String fileUrl,
@Parameter(description = "视频标题") @RequestParam("title") String title,
@Parameter(description = "视频描述") @RequestParam("description") String description,
@Parameter(description = "视频标签,多个用逗号分隔") @RequestParam(value = "tags", required = false) String tags) {
public Result<VideoDTO> uploadVideo(@RequestBody VideoUploadDTO videoUploadDTO) {
try {
VideoDTO video = videoService.uploadVideo(fileUrl, title, description, tags);
VideoDTO video = videoService.uploadVideo(videoUploadDTO.getFileUrl(),
videoUploadDTO.getTitle(), videoUploadDTO.getDescription(), videoUploadDTO.getTags());
return Result.success(video);
} catch (Exception e) {
log.error("上传视频失败", e);

View File

@ -0,0 +1,18 @@
package com.guwan.backend.dto.live;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "创建直播间请求")
public class CreateRoomRequest {
@Schema(description = "直播间标题")
private String title;
@Schema(description = "直播间描述")
private String description;
@Schema(description = "主播用户ID")
private Long userId;
}

View File

@ -0,0 +1,12 @@
package com.guwan.backend.dto.live;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "开始直播请求")
public class StartLiveRequest {
@Schema(description = "推流地址(RTMP/RTSP)")
private String sourceUrl;
}

View File

@ -0,0 +1,12 @@
package com.guwan.backend.dto.live;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "WebRTC请求")
public class WebRTCRequest {
@Schema(description = "SDP offer")
private String sdp;
}

View File

@ -0,0 +1,14 @@
package com.guwan.backend.dto.live;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "WebRTC响应")
public class WebRTCResponse {
@Schema(description = "SDP answer")
private String sdp;
}

View File

@ -0,0 +1,30 @@
package com.guwan.backend.dto.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "商品信息DTO")
public class ProductDTO {
@Schema(description = "商品ID")
private String id;
@Schema(description = "商品名称")
private String name;
@Schema(description = "商品分类")
private String category;
@Schema(description = "商品价格")
private Double price;
@Schema(description = "库存数量")
private Integer stock;
@Schema(description = "商品描述")
private String description;
@Schema(description = "创建时间")
private LocalDateTime createdTime;
}

View File

@ -0,0 +1,19 @@
package com.guwan.backend.dto.video;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.web.bind.annotation.RequestParam;
@Schema(description = "视频上传DTO")
@Data
public class VideoUploadDTO {
@Schema(description = "视频文件地址")
String fileUrl;
@Schema(description = "视频标题")
String title;
@Schema(description = "视频描述")
String description;
@Schema(description = "视频标签,多个用逗号分隔", nullable = true)
String tags;
}

View File

@ -16,10 +16,7 @@ public class LiveRoom {
private String coverUrl; // 封面图
private Long userId; // 主播ID
private String username; // 主播名称
private String streamKey; // 推流密钥
private String rtmpUrl; // RTMP推流地址
private String hlsUrl; // HLS播放地址
private String replayUrl; // 回放地址
private String streamId; // Go2RTC流ID
private String status; // 状态PREPARING-准备中LIVING-直播中ENDED-已结束
private Integer onlineCount; // 在线人数
private Integer likeCount; // 点赞数

View File

@ -0,0 +1,34 @@
package com.guwan.backend.es.document;
import cn.easyes.annotation.IndexField;
import cn.easyes.annotation.IndexId;
import cn.easyes.annotation.IndexName;
import cn.easyes.annotation.rely.FieldType;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@IndexName("products")
public class ProductDocument {
@IndexId
private String id;
@IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name;
@IndexField(fieldType = FieldType.KEYWORD)
private String category;
@IndexField(fieldType = FieldType.DOUBLE)
private Double price;
@IndexField(fieldType = FieldType.INTEGER)
private Integer stock;
@IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String description;
@IndexField(fieldType = FieldType.DATE)
private LocalDateTime createdTime;
}

View File

@ -0,0 +1,9 @@
package com.guwan.backend.es.mapper;
import cn.easyes.core.conditions.interfaces.BaseEsMapper;
import com.guwan.backend.es.document.ProductDocument;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductEsMapper extends BaseEsMapper<ProductDocument> {
}

View File

@ -0,0 +1,88 @@
package com.guwan.backend.service;
import com.guwan.backend.client.Go2RTCClient;
import com.guwan.backend.entity.LiveRoom;
import com.guwan.backend.mapper.LiveRoomMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class LiveStreamService {
private final Go2RTCClient go2rtcClient;
private final LiveRoomMapper liveRoomMapper;
/**
* 创建直播间
*/
@Transactional
public LiveRoom createLiveRoom(String title, String description, Long userId) {
// 生成streamId
String streamId = UUID.randomUUID().toString();
// 创建直播间记录
LiveRoom room = new LiveRoom();
room.setTitle(title);
room.setDescription(description);
room.setUserId(userId);
room.setStreamId(streamId);
room.setStatus("PREPARING");
liveRoomMapper.insert(room);
return room;
}
/**
* 开始直播
*/
@Transactional
public void startLive(Long roomId, String sourceUrl) {
LiveRoom room = liveRoomMapper.selectById(roomId);
if (room == null) {
throw new IllegalArgumentException("直播间不存在");
}
// 创建Go2RTC流
go2rtcClient.createStream(room.getStreamId(), sourceUrl);
// 更新状态
room.setStatus("LIVING");
liveRoomMapper.updateById(room);
}
/**
* 结束直播
*/
@Transactional
public void endLive(Long roomId) {
LiveRoom room = liveRoomMapper.selectById(roomId);
if (room == null) {
throw new IllegalArgumentException("直播间不存在");
}
// 删除Go2RTC流
go2rtcClient.deleteStream(room.getStreamId());
// 更新状态
room.setStatus("ENDED");
liveRoomMapper.updateById(room);
}
/**
* 获取WebRTC Offer
*/
public String getWebRTCOffer(Long roomId, String sdp) {
LiveRoom room = liveRoomMapper.selectById(roomId);
if (room == null) {
throw new IllegalArgumentException("直播间不存在");
}
return go2rtcClient.getOffer(room.getStreamId(), sdp);
}
}

View File

@ -0,0 +1,81 @@
package com.guwan.backend.service;
import cn.easyes.core.conditions.LambdaEsQueryWrapper;
import com.guwan.backend.dto.product.ProductDTO;
import com.guwan.backend.es.document.ProductDocument;
import com.guwan.backend.es.mapper.ProductEsMapper;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
import lombok.SneakyThrows;
@Slf4j
@Service
@ConditionalOnProperty(name = "easy-es.enable", havingValue = "true")
@RequiredArgsConstructor
public class ProductSearchService {
private final ProductEsMapper productEsMapper;
public void saveOrUpdate(ProductDTO productDTO) {
ProductDocument document = convertToDocument(productDTO);
productEsMapper.insert(document);
}
public void delete(Long id) {
productEsMapper.deleteById(id);
}
public List<ProductDTO> search(String keyword) {
LambdaEsQueryWrapper<ProductDocument> wrapper = new LambdaEsQueryWrapper<>();
wrapper.and(w -> w
.match(ProductDocument::getName, keyword)
.or()
.match(ProductDocument::getDescription, keyword)
);
List<ProductDocument> documents = productEsMapper.selectList(wrapper);
return documents.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
public List<ProductDTO> getByCategory(String category) {
LambdaEsQueryWrapper<ProductDocument> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(ProductDocument::getCategory, category);
List<ProductDocument> documents = productEsMapper.selectList(wrapper);
return documents.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
private ProductDocument convertToDocument(ProductDTO dto) {
ProductDocument document = new ProductDocument();
BeanUtils.copyProperties(dto, document);
return document;
}
private ProductDTO convertToDTO(ProductDocument document) {
ProductDTO dto = new ProductDTO();
BeanUtils.copyProperties(document, dto);
return dto;
}
@PostConstruct
public void init() {
log.info("ProductSearchService initialized with ES enabled");
}
@SneakyThrows
public void handleError(String operation, Exception e) {
log.error("ES {} operation failed", operation, e);
throw new RuntimeException("搜索服务暂时不可用");
}
}

View File

@ -15,13 +15,13 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
password: 123456
# Redis配置
data:
redis:
host: localhost
port: 6379
port: 6380
password: 123456 # 如果有密码,请设置
database: 8
timeout: 10000
@ -117,10 +117,10 @@ springdoc:
config:
arcface-sdk:
version: 4.1
app-id: BYYPcPa3qQaAA88HZw7awne1BqZiePgT6axtALtK2qun
sdk-key: 3eFAwSCMQRhoUoAaZ2Li9XUVDUubAWYksQtNPWD32UrX
active-key: 82K1-11TQ-W11B-H7MU
active-file: ArcFacePro64.dat
app-id: 5nPWymNAibvWTq6XPypUWxroyzjMScZ9RwVkDjCFgK32
sdk-key: 7dsPvanADtYAP1TiiiFjTsms2mAU85m5duVwHChhumyV
active-key: 86C1-11T1-K131-FJQU
active-file: 86C111T1K131FJQU.dat
detect-pool-size: 16
compare-pool-size: 16
rec-face-thd: 0.8
@ -132,10 +132,8 @@ srs:
url: http://localhost:1985 # SRS HTTP API地址
easy-es:
enable: false # 改为false禁用Easy-Es
address: localhost:9200 # ES地址
username: ${ES_USERNAME:} # ES用户名可选
password: ${ES_PASSWORD:} # ES密码可选
enable: true
address: localhost:9200
global-config:
process-index-mode: manual
print-dsl: true
@ -143,7 +141,12 @@ easy-es:
response-log: true
db-config:
map-underscore-to-camel-case: true
index-prefix: video_ # 索引前缀
index-prefix: product_
async-process: true
schema-update: true
max-connect-num: 50
connect-timeout: 5000
socket-timeout: 60000
netty:
danmaku:
@ -155,4 +158,8 @@ netty:
heartbeat:
interval: 30
cluster:
nodes: localhost:8088,localhost:8089
nodes: localhost:8088,localhost:8089
go2rtc:
api:
url: http://localhost:1984 # Go2RTC API地址

View File

@ -0,0 +1,6 @@
ALTER TABLE `live_room`
ADD COLUMN `stream_id` varchar(50) NOT NULL COMMENT 'Go2RTC流ID' AFTER `username`,
DROP COLUMN `stream_key`,
DROP COLUMN `rtmp_url`,
DROP COLUMN `hls_url`,
DROP COLUMN `replay_url`;