From d6577ad99bd436f777151d2aa6f58ccbf95508c6 Mon Sep 17 00:00:00 2001 From: "xiao12feng@outlook.com" Date: Sun, 21 Dec 2025 18:07:19 +0800 Subject: [PATCH] feat(live): embed room api and SRS hooks into crmeb-front --- .../com/zbkj/common/model/live/LiveRoom.java | 54 +++++++++ .../java/com/zbkj/front/config/WebConfig.java | 2 + .../front/controller/LiveRoomController.java | 109 ++++++++++++++++++ .../controller/SrsCallbackController.java | 86 ++++++++++++++ .../request/live/CreateLiveRoomRequest.java | 20 ++++ .../front/response/live/LiveRoomResponse.java | 31 +++++ .../response/live/StreamUrlsResponse.java | 19 +++ .../com/zbkj/service/dao/LiveRoomDao.java | 7 ++ .../zbkj/service/service/LiveRoomService.java | 15 +++ .../service/impl/LiveRoomServiceImpl.java | 52 +++++++++ Zhibo/zhibo-h/sql/create_eb_live_room.sql | 13 +++ live-streaming/docker/srs/srs.conf | 10 ++ 12 files changed, 418 insertions(+) create mode 100644 Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveRoom.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/SrsCallbackController.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/request/live/CreateLiveRoomRequest.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveRoomResponse.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/StreamUrlsResponse.java create mode 100644 Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/LiveRoomDao.java create mode 100644 Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/LiveRoomService.java create mode 100644 Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomServiceImpl.java create mode 100644 Zhibo/zhibo-h/sql/create_eb_live_room.sql diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveRoom.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveRoom.java new file mode 100644 index 00000000..5f2a5678 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveRoom.java @@ -0,0 +1,54 @@ +package com.zbkj.common.model.live; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("eb_live_room") +@ApiModel(value = "LiveRoom对象", description = "直播房间") +public class LiveRoom implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键ID") + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + @ApiModelProperty(value = "房主用户ID") + private Integer uid; + + @ApiModelProperty(value = "直播间标题") + private String title; + + @ApiModelProperty(value = "主播名称") + @TableField("streamer_name") + private String streamerName; + + @ApiModelProperty(value = "流密钥(推流 streamKey)") + @TableField("stream_key") + private String streamKey; + + @ApiModelProperty(value = "是否直播中(1=直播中,0=未开播)") + @TableField("is_live") + private Integer isLive; + + @ApiModelProperty(value = "创建时间") + @TableField("create_time") + private Date createTime; + + @ApiModelProperty(value = "开始直播时间") + @TableField("started_at") + private Date startedAt; +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/config/WebConfig.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/config/WebConfig.java index e3e2296e..6343778b 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/config/WebConfig.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/config/WebConfig.java @@ -57,6 +57,8 @@ public class WebConfig implements WebMvcConfigurer { //前端用户登录token registry.addInterceptor(frontTokenInterceptor()). addPathPatterns("/api/front/**"). + excludePathPatterns("/api/front/live/public/**"). + excludePathPatterns("/api/front/live/srs/**"). excludePathPatterns("/api/front/index"). excludePathPatterns("/api/front/qrcode/**"). excludePathPatterns("/api/front/login/mobile"). diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java new file mode 100644 index 00000000..c96d936a --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java @@ -0,0 +1,109 @@ +package com.zbkj.front.controller; + +import com.zbkj.common.model.live.LiveRoom; +import com.zbkj.common.result.CommonResult; +import com.zbkj.common.token.FrontTokenComponent; +import com.zbkj.front.request.live.CreateLiveRoomRequest; +import com.zbkj.front.response.live.LiveRoomResponse; +import com.zbkj.front.response.live.StreamUrlsResponse; +import com.zbkj.service.service.LiveRoomService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("api/front/live") +@Api(tags = "直播 -- 房间") +public class LiveRoomController { + + @Autowired + private LiveRoomService liveRoomService; + + @Autowired + private FrontTokenComponent frontTokenComponent; + + @Value("${LIVE_PUBLIC_SRS_HOST:}") + private String publicHost; + + @Value("${LIVE_PUBLIC_SRS_RTMP_PORT:}") + private String publicRtmpPort; + + @Value("${LIVE_PUBLIC_SRS_HTTP_PORT:}") + private String publicHttpPort; + + @ApiOperation(value = "公开:直播间列表") + @GetMapping("/public/rooms") + public CommonResult> publicRooms(HttpServletRequest request) { + String requestHost = request.getServerName(); + return CommonResult.success(liveRoomService.getAll().stream() + .map(r -> toResponse(r, requestHost)) + .collect(Collectors.toList())); + } + + @ApiOperation(value = "公开:直播间详情") + @GetMapping("/public/rooms/{id}") + public CommonResult publicRoom(@PathVariable Integer id, HttpServletRequest request) { + LiveRoom room = liveRoomService.getById(id); + if (room == null) return CommonResult.failed("房间不存在"); + return CommonResult.success(toResponse(room, request.getServerName())); + } + + @ApiOperation(value = "创建直播间(登录)") + @PostMapping("/rooms") + public CommonResult create(@RequestBody @Validated CreateLiveRoomRequest req, HttpServletRequest request) { + Integer uid = frontTokenComponent.getUserId(); + if (uid == null) return CommonResult.failed("未登录"); + LiveRoom room = liveRoomService.createRoom(uid, req.getTitle(), req.getStreamerName()); + return CommonResult.success(toResponse(room, request.getServerName())); + } + + @ApiOperation(value = "删除直播间(登录)") + @DeleteMapping("/rooms/{id}") + public CommonResult delete(@PathVariable Integer id) { + Integer uid = frontTokenComponent.getUserId(); + if (uid == null) return CommonResult.failed("未登录"); + LiveRoom room = liveRoomService.getById(id); + if (room == null) return CommonResult.failed("房间不存在"); + if (!uid.equals(room.getUid())) return CommonResult.failed("无权限"); + liveRoomService.removeById(id); + return CommonResult.success(); + } + + private LiveRoomResponse toResponse(LiveRoom room, String requestHost) { + LiveRoomResponse resp = new LiveRoomResponse(); + BeanUtils.copyProperties(room, resp); + resp.setIsLive(room.getIsLive() != null && room.getIsLive() == 1); + resp.setViewerCount(0); + + String host = (publicHost != null && !publicHost.trim().isEmpty()) ? publicHost.trim() : requestHost; + int rtmpPort = parsePort(publicRtmpPort, 25002); + int httpPort = parsePort(publicHttpPort, 25003); + + StreamUrlsResponse urls = new StreamUrlsResponse(); + urls.setRtmp(String.format("rtmp://%s:%d/live/%s", host, rtmpPort, room.getStreamKey())); + urls.setFlv(String.format("http://%s:%d/live/%s.flv", host, httpPort, room.getStreamKey())); + urls.setHls(String.format("http://%s:%d/live/%s.m3u8", host, httpPort, room.getStreamKey())); + resp.setStreamUrls(urls); + + return resp; + } + + private int parsePort(String s, int def) { + try { + if (s == null) return def; + String v = s.trim(); + if (v.isEmpty()) return def; + return Integer.parseInt(v); + } catch (Exception e) { + return def; + } + } +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/SrsCallbackController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/SrsCallbackController.java new file mode 100644 index 00000000..0929027e --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/SrsCallbackController.java @@ -0,0 +1,86 @@ +package com.zbkj.front.controller; + +import com.zbkj.service.service.LiveRoomService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import org.springframework.util.MultiValueMap; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("api/front/live/srs") +@Api(tags = "直播 -- SRS 回调") +public class SrsCallbackController { + + @Autowired + private LiveRoomService liveRoomService; + + @ApiOperation(value = "SRS 推流开始回调") + @PostMapping(value = "/on_publish", consumes = MediaType.APPLICATION_JSON_VALUE) + public Map onPublishJson(@RequestBody(required = false) Map body) { + String stream = body == null ? null : String.valueOf(body.get("stream")); + if (stream != null && !stream.trim().isEmpty()) { + liveRoomService.setLiveStatus(stream.trim(), true); + } + Map res = new HashMap<>(); + res.put("code", 0); + return res; + } + + @ApiOperation(value = "SRS 推流开始回调") + @PostMapping(value = "/on_publish", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public Map onPublishForm(@RequestParam MultiValueMap form) { + String stream = form == null ? null : form.getFirst("stream"); + if (stream != null && !stream.trim().isEmpty()) { + liveRoomService.setLiveStatus(stream.trim(), true); + } + Map res = new HashMap<>(); + res.put("code", 0); + return res; + } + + @ApiOperation(value = "SRS 推流结束回调") + @PostMapping(value = "/on_unpublish", consumes = MediaType.APPLICATION_JSON_VALUE) + public Map onUnpublishJson(@RequestBody(required = false) Map body) { + String stream = body == null ? null : String.valueOf(body.get("stream")); + if (stream != null && !stream.trim().isEmpty()) { + liveRoomService.setLiveStatus(stream.trim(), false); + } + Map res = new HashMap<>(); + res.put("code", 0); + return res; + } + + @ApiOperation(value = "SRS 推流结束回调") + @PostMapping(value = "/on_unpublish", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public Map onUnpublishForm(@RequestParam MultiValueMap form) { + String stream = form == null ? null : form.getFirst("stream"); + if (stream != null && !stream.trim().isEmpty()) { + liveRoomService.setLiveStatus(stream.trim(), false); + } + Map res = new HashMap<>(); + res.put("code", 0); + return res; + } + + @ApiOperation(value = "SRS 观看回调") + @PostMapping("/on_play") + public Map onPlay(@RequestBody(required = false) Map body) { + Map res = new HashMap<>(); + res.put("code", 0); + return res; + } + + @ApiOperation(value = "SRS 停止观看回调") + @PostMapping("/on_stop") + public Map onStop(@RequestBody(required = false) Map body) { + Map res = new HashMap<>(); + res.put("code", 0); + return res; + } +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/request/live/CreateLiveRoomRequest.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/request/live/CreateLiveRoomRequest.java new file mode 100644 index 00000000..7ecdf414 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/request/live/CreateLiveRoomRequest.java @@ -0,0 +1,20 @@ +package com.zbkj.front.request.live; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@ApiModel(value = "CreateLiveRoomRequest", description = "创建直播间请求") +public class CreateLiveRoomRequest { + + @ApiModelProperty(value = "直播间标题") + @NotBlank + private String title; + + @ApiModelProperty(value = "主播名称") + @NotBlank + private String streamerName; +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveRoomResponse.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveRoomResponse.java new file mode 100644 index 00000000..c27e1c2f --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveRoomResponse.java @@ -0,0 +1,31 @@ +package com.zbkj.front.response.live; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "LiveRoomResponse", description = "直播间返回") +public class LiveRoomResponse { + + @ApiModelProperty(value = "房间ID") + private Integer id; + + @ApiModelProperty(value = "标题") + private String title; + + @ApiModelProperty(value = "主播名") + private String streamerName; + + @ApiModelProperty(value = "streamKey") + private String streamKey; + + @ApiModelProperty(value = "是否直播中") + private Boolean isLive; + + @ApiModelProperty(value = "观看人数(预留)") + private Integer viewerCount; + + @ApiModelProperty(value = "推流/播放地址") + private StreamUrlsResponse streamUrls; +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/StreamUrlsResponse.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/StreamUrlsResponse.java new file mode 100644 index 00000000..eaf0b24b --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/StreamUrlsResponse.java @@ -0,0 +1,19 @@ +package com.zbkj.front.response.live; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "StreamUrlsResponse", description = "推流/播放地址") +public class StreamUrlsResponse { + + @ApiModelProperty(value = "RTMP 推流地址") + private String rtmp; + + @ApiModelProperty(value = "HTTP-FLV 播放地址") + private String flv; + + @ApiModelProperty(value = "HLS 播放地址") + private String hls; +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/LiveRoomDao.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/LiveRoomDao.java new file mode 100644 index 00000000..d587dee0 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/LiveRoomDao.java @@ -0,0 +1,7 @@ +package com.zbkj.service.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.zbkj.common.model.live.LiveRoom; + +public interface LiveRoomDao extends BaseMapper { +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/LiveRoomService.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/LiveRoomService.java new file mode 100644 index 00000000..7a36dc0c --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/LiveRoomService.java @@ -0,0 +1,15 @@ +package com.zbkj.service.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.zbkj.common.model.live.LiveRoom; + +import java.util.List; + +public interface LiveRoomService extends IService { + + List getAll(); + + LiveRoom createRoom(Integer uid, String title, String streamerName); + + boolean setLiveStatus(String streamKey, boolean isLive); +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomServiceImpl.java new file mode 100644 index 00000000..c3b86587 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomServiceImpl.java @@ -0,0 +1,52 @@ +package com.zbkj.service.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.zbkj.common.model.live.LiveRoom; +import com.zbkj.service.dao.LiveRoomDao; +import com.zbkj.service.service.LiveRoomService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Service +public class LiveRoomServiceImpl extends ServiceImpl implements LiveRoomService { + + @Resource + private LiveRoomDao dao; + + @Override + public List getAll() { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.orderByDesc(LiveRoom::getCreateTime); + return dao.selectList(qw); + } + + @Override + public LiveRoom createRoom(Integer uid, String title, String streamerName) { + String streamKey = UUID.randomUUID().toString().replace("-", ""); + LiveRoom room = new LiveRoom(); + room.setUid(uid); + room.setTitle(title); + room.setStreamerName(streamerName); + room.setStreamKey(streamKey); + room.setIsLive(0); + room.setCreateTime(new Date()); + room.setStartedAt(null); + dao.insert(room); + return room; + } + + @Override + public boolean setLiveStatus(String streamKey, boolean isLive) { + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(LiveRoom::getStreamKey, streamKey); + uw.set(LiveRoom::getIsLive, isLive ? 1 : 0); + uw.set(LiveRoom::getStartedAt, isLive ? new Date() : null); + return dao.update(null, uw) > 0; + } +} diff --git a/Zhibo/zhibo-h/sql/create_eb_live_room.sql b/Zhibo/zhibo-h/sql/create_eb_live_room.sql new file mode 100644 index 00000000..33434f42 --- /dev/null +++ b/Zhibo/zhibo-h/sql/create_eb_live_room.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS `eb_live_room` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `uid` int(11) NOT NULL, + `title` varchar(255) NOT NULL, + `streamer_name` varchar(255) NOT NULL, + `stream_key` varchar(64) NOT NULL, + `is_live` tinyint(1) NOT NULL DEFAULT 0, + `create_time` datetime DEFAULT NULL, + `started_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_stream_key` (`stream_key`), + KEY `idx_uid` (`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/live-streaming/docker/srs/srs.conf b/live-streaming/docker/srs/srs.conf index e32c29fd..30c3206a 100644 --- a/live-streaming/docker/srs/srs.conf +++ b/live-streaming/docker/srs/srs.conf @@ -79,4 +79,14 @@ vhost __defaultVhost__ { # 减少正常包超时 normal_timeout 5000; } + + # HTTP Hooks: 推流/播放事件回调到 Java 后端 + # Windows Docker Desktop 下建议用 host.docker.internal 访问宿主机 + http_hooks { + enabled on; + on_publish http://host.docker.internal:8081/api/front/live/srs/on_publish; + on_unpublish http://host.docker.internal:8081/api/front/live/srs/on_unpublish; + on_play http://host.docker.internal:8081/api/front/live/srs/on_play; + on_stop http://host.docker.internal:8081/api/front/live/srs/on_stop; + } }