feat(live): embed room api and SRS hooks into crmeb-front
This commit is contained in:
parent
9cf2beef40
commit
d6577ad99b
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,8 @@ public class WebConfig implements WebMvcConfigurer {
|
||||||
//前端用户登录token
|
//前端用户登录token
|
||||||
registry.addInterceptor(frontTokenInterceptor()).
|
registry.addInterceptor(frontTokenInterceptor()).
|
||||||
addPathPatterns("/api/front/**").
|
addPathPatterns("/api/front/**").
|
||||||
|
excludePathPatterns("/api/front/live/public/**").
|
||||||
|
excludePathPatterns("/api/front/live/srs/**").
|
||||||
excludePathPatterns("/api/front/index").
|
excludePathPatterns("/api/front/index").
|
||||||
excludePathPatterns("/api/front/qrcode/**").
|
excludePathPatterns("/api/front/qrcode/**").
|
||||||
excludePathPatterns("/api/front/login/mobile").
|
excludePathPatterns("/api/front/login/mobile").
|
||||||
|
|
|
||||||
|
|
@ -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<List<LiveRoomResponse>> 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<LiveRoomResponse> 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<LiveRoomResponse> 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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, Object> onPublishJson(@RequestBody(required = false) Map<String, Object> body) {
|
||||||
|
String stream = body == null ? null : String.valueOf(body.get("stream"));
|
||||||
|
if (stream != null && !stream.trim().isEmpty()) {
|
||||||
|
liveRoomService.setLiveStatus(stream.trim(), true);
|
||||||
|
}
|
||||||
|
Map<String, Object> res = new HashMap<>();
|
||||||
|
res.put("code", 0);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "SRS 推流开始回调")
|
||||||
|
@PostMapping(value = "/on_publish", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
|
public Map<String, Object> onPublishForm(@RequestParam MultiValueMap<String, String> form) {
|
||||||
|
String stream = form == null ? null : form.getFirst("stream");
|
||||||
|
if (stream != null && !stream.trim().isEmpty()) {
|
||||||
|
liveRoomService.setLiveStatus(stream.trim(), true);
|
||||||
|
}
|
||||||
|
Map<String, Object> res = new HashMap<>();
|
||||||
|
res.put("code", 0);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "SRS 推流结束回调")
|
||||||
|
@PostMapping(value = "/on_unpublish", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public Map<String, Object> onUnpublishJson(@RequestBody(required = false) Map<String, Object> body) {
|
||||||
|
String stream = body == null ? null : String.valueOf(body.get("stream"));
|
||||||
|
if (stream != null && !stream.trim().isEmpty()) {
|
||||||
|
liveRoomService.setLiveStatus(stream.trim(), false);
|
||||||
|
}
|
||||||
|
Map<String, Object> res = new HashMap<>();
|
||||||
|
res.put("code", 0);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "SRS 推流结束回调")
|
||||||
|
@PostMapping(value = "/on_unpublish", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
|
public Map<String, Object> onUnpublishForm(@RequestParam MultiValueMap<String, String> form) {
|
||||||
|
String stream = form == null ? null : form.getFirst("stream");
|
||||||
|
if (stream != null && !stream.trim().isEmpty()) {
|
||||||
|
liveRoomService.setLiveStatus(stream.trim(), false);
|
||||||
|
}
|
||||||
|
Map<String, Object> res = new HashMap<>();
|
||||||
|
res.put("code", 0);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "SRS 观看回调")
|
||||||
|
@PostMapping("/on_play")
|
||||||
|
public Map<String, Object> onPlay(@RequestBody(required = false) Map<String, Object> body) {
|
||||||
|
Map<String, Object> res = new HashMap<>();
|
||||||
|
res.put("code", 0);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "SRS 停止观看回调")
|
||||||
|
@PostMapping("/on_stop")
|
||||||
|
public Map<String, Object> onStop(@RequestBody(required = false) Map<String, Object> body) {
|
||||||
|
Map<String, Object> res = new HashMap<>();
|
||||||
|
res.put("code", 0);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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<LiveRoom> {
|
||||||
|
}
|
||||||
|
|
@ -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<LiveRoom> {
|
||||||
|
|
||||||
|
List<LiveRoom> getAll();
|
||||||
|
|
||||||
|
LiveRoom createRoom(Integer uid, String title, String streamerName);
|
||||||
|
|
||||||
|
boolean setLiveStatus(String streamKey, boolean isLive);
|
||||||
|
}
|
||||||
|
|
@ -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<LiveRoomDao, LiveRoom> implements LiveRoomService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private LiveRoomDao dao;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LiveRoom> getAll() {
|
||||||
|
LambdaQueryWrapper<LiveRoom> 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<LiveRoom> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Zhibo/zhibo-h/sql/create_eb_live_room.sql
Normal file
13
Zhibo/zhibo-h/sql/create_eb_live_room.sql
Normal file
|
|
@ -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;
|
||||||
|
|
@ -79,4 +79,14 @@ vhost __defaultVhost__ {
|
||||||
# 减少正常包超时
|
# 减少正常包超时
|
||||||
normal_timeout 5000;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user