parent
2a337b68b2
commit
7e51730e60
|
@ -0,0 +1 @@
|
|||
EWDRGAHMADDPERCSGJDUHABIENHHJEEAFRFSATFTEFDSGZEQCYEOCOIRABDOEMEQAYAAABHEBBDWCMGEARHUJOGHBAJUFJJNFDGFAHDBJMGUBCIZBIEHIKDWBRAZIAINIGBFFFEKGQBUJGELGZIYBRGTHAEHHZCUEQJBAOGJAZBLACAFDFJKALCECDGKCQIYDJDODSDNDTIDHQJSDFFADLJLBNFAAAIODQEEILGGGVGEEKFSJRACFZDZIQEOILBLDXCRCPAOEBEDIPDDBNHCDJCKEGEJBHHBFKDNAOFOCCIQIBGVBSGPAJCDGTEMHGBCHYGLJOCPDYGWIFGIDXBUIHEKETDIFEGQHVGGFJAKAVHNJLEKBXJAFXDNHBGQAZDRHZGOGXHAALCXEZCNGLGHGOIDDKGCIYDSFJFNGIAQGMAHCXAFAVDVBPFMHSJSCODLHACXBHGUJEJLGLEREPBOEFCJDQDIDOIEFIHLCICOBWBTCAHOJTENCYDSBDGLFQBPIWCBGJDDIRCCDNEJITEJFHIWJEGLBGDMJRDGCFIFJCENCGCXHUCJGZGXAWAJCYDFIKJSIPDHFPBHGLCLGVFOEUAUIKFRISFQEXEPGEDVIFFGDOBNBFIMAUJOHJJNJAEQAMHBJVDTIUDDHBAQILFXHOGEFXIPBMDUCKCTCWGRFZIYFOENDJHPCTIYIXDXAEDGHNHTJQCWCQHAEVBDGLAZEQDPIQGIEVHNDUCUHMBMIVJKDNDJDJFGBAADHDIWJEGJJFDAEKALBMDOHTAQFFIDIBJVHQDNBXIABPIJCLAIBHHNIOGKDUCUCSAXDUCMDMGYAMBUEUBOAEATCMGBBJAOJKFRCYEXCJIZDKFAFCAJAGDYJGGPDKCJAPDWFIGAITHCEOFTIJEQGUGQHKAAHGFFEWGHDDEMDCHSAYJNCGIAJUHGDLAKFGHCGLEHHEAPENDVBAIOFEGOBKIKERHNFSIRCCAHHUBMGXDYATBYADEZCLCBDEDUHCJRESHMJCJVFTGLJJDYDYAFIMBCJKAOHEAHBMHMASIHCMGEGREQEUBSHFEECBJOJUBKDXGWJVADHLECJHHAADBKGCFQGIAYEHAJBFCVBTILJBIKBPAYFFFPCQEWJB
|
|
@ -0,0 +1 @@
|
|||
EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTGFGBEAAWGLBZAUCNHCCPBZCIIKBJATGYARHRAZHXFRBIEBCHIXAFDLFQBZFVJQGTCEHDIMIVGQEJAJIYHXHTISIVETBACMCCGFDPEKDQHYGGAPFXIOCJAAGOHYIFHNHZHWIGCZIGHHDMDXHQGTFLJOHGAYIVBGIHIQHHHJDJFWHMCRHJAMHZESGWGGAUJRGDHAGHIQITIJIUAXAEIZGBGZCCHJAHASGNCVIIJAIYFOEQGFELEIEDECJTCXBCAPIKHTFTHBAGJTERHQGSAFEUDIHLDDITDPIMACAOGTIIHFEMHLHIHYJTFDFDGWAKHJEIEIJFGABQJCIHIADYCXAHIMJTHOASFLGFFIBJJEHDHLHOCMDIGGDOJDHNBQCJENFUBAGOAZITIRJSFBBUCUHABLHRFVIUBWCADJCMDXDPATBPJSJRJLBRABGVFTDNFOAQDOARDEBRHJAQGYHIGMBDCJCSJKFBBLGECAEFEYCVCHAEAZJRIOFEHLCJILEHJVGYIVCWGHCMGJGLBTFMHFCAEAAUJQJLAEARDHDFHDBJJGALEHFNGSAIHOJUBOEAJDDFFYFTINITHTBNIJFDHLEAFGBFHFFQGHGFGREVFHFDCZGYEVBWAZDSCAGLDMIAAEFOAXIXFECSFQDWHFFHCFASFSGAHVJSDBBZJQAZBXARILBAJFJEHCANAIABBMECBJJFIOGYGHBXCUCVBDJOCYBZDZAJEXAXEPFRFOGVHQAOJLCYBOHFEKJFIJBDHDDCEAAUJVDRIGGGGCJOFVECAHAQFSBSGYJVGKCQDDHPGUCIARFAIEJGGDITAUDIIVBBJUEFCIDTGJGYJODRDEJPBMFNCXAKCPAIGOJQGHBZHQJUCOBKCKDPJSGGCVCAIWFVHIIEAJJMEFGRHZDEDACFBQJODGDVJBAUBXGKGCFZERAHIOALGKGAILDNHQGGAZDEIGATBTCWHMDKGSIWFMHAIZHREBJBEFENDFBRBLGLCMERJAEOBXCNDBHVCSJBDMEHCLJLCFFOGVGWATBOJBFJEQETHGESEXFDIIFDAGJPDNHEDSFNBRIVFMFPGOEEIHEFCOCKJGJAIZJIFTIGAWITGWDXGBEFDTJHFXBF
|
|
@ -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
21
pom.xml
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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(":");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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", // 获取邮箱验证码
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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; // 点赞数
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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("搜索服务暂时不可用");
|
||||
}
|
||||
}
|
|
@ -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地址
|
|
@ -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`;
|
Loading…
Reference in New Issue