parent
b8e7422a60
commit
0a79f9cc23
|
@ -0,0 +1,28 @@
|
||||||
|
listen 1935;
|
||||||
|
max_connections 1000;
|
||||||
|
daemon off;
|
||||||
|
http_api {
|
||||||
|
enabled on;
|
||||||
|
listen 1985;
|
||||||
|
}
|
||||||
|
http_server {
|
||||||
|
enabled on;
|
||||||
|
listen 8080;
|
||||||
|
}
|
||||||
|
vhost __defaultVhost__ {
|
||||||
|
hls {
|
||||||
|
enabled on;
|
||||||
|
hls_path ./objs/nginx/html;
|
||||||
|
hls_fragment 10;
|
||||||
|
hls_window 60;
|
||||||
|
}
|
||||||
|
http_remux {
|
||||||
|
enabled on;
|
||||||
|
mount [vhost]/[app]/[stream].flv;
|
||||||
|
}
|
||||||
|
dvr {
|
||||||
|
enabled on;
|
||||||
|
dvr_path ./recordings/[app]/[stream].flv;
|
||||||
|
dvr_plan session;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
srs:
|
||||||
|
image: ossrs/srs:4
|
||||||
|
ports:
|
||||||
|
- "1935:1935" # RTMP
|
||||||
|
- "8080:8080" # HTTP-FLV
|
||||||
|
- "8088:8088" # HLS
|
||||||
|
volumes:
|
||||||
|
- ./conf/srs.conf:/usr/local/srs/conf/srs.conf
|
||||||
|
- ./recordings:/usr/local/srs/recordings
|
20
pom.xml
20
pom.xml
|
@ -223,6 +223,26 @@
|
||||||
<!-- <systemPath>${project.basedir}/libs/arcsoft-sdk-face-3.0.0.0.jar</systemPath>-->
|
<!-- <systemPath>${project.basedir}/libs/arcsoft-sdk-face-3.0.0.0.jar</systemPath>-->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- WebSocket -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SRS Java SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ossrs</groupId>
|
||||||
|
<artifactId>srs-sdk</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Netty -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-all</artifactId>
|
||||||
|
<version>4.1.94.Final</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.guwan.backend.controller;
|
||||||
|
|
||||||
|
import com.guwan.backend.entity.LiveRoom;
|
||||||
|
import com.guwan.backend.entity.LiveRoomDTO;
|
||||||
|
import com.guwan.backend.service.LiveService;
|
||||||
|
import com.guwan.backend.util.Result;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/live")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LiveController {
|
||||||
|
|
||||||
|
private final LiveService liveService;
|
||||||
|
|
||||||
|
@PostMapping("/room")
|
||||||
|
public Result<LiveRoom> createLiveRoom(@RequestBody LiveRoomDTO dto) {
|
||||||
|
try {
|
||||||
|
return Result.success(liveService.createLiveRoom(dto));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建直播间失败", e);
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/room/{id}/start")
|
||||||
|
public Result<Void> startLive(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
liveService.startLive(id);
|
||||||
|
return Result.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("开始直播失败", e);
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/room/{id}/end")
|
||||||
|
public Result<Void> endLive(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
liveService.endLive(id);
|
||||||
|
return Result.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("结束直播失败", e);
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
@Data
|
||||||
|
public class LiveMessage {
|
||||||
|
private String type; // 消息类型:CHAT-聊天,GIFT-礼物,LIKE-点赞,ENTER-进入,LEAVE-离开
|
||||||
|
private Long userId; // 用户ID
|
||||||
|
private String username; // 用户名
|
||||||
|
private String content; // 消息内容
|
||||||
|
private String giftType; // 礼物类型
|
||||||
|
private Integer giftCount; // 礼物数量
|
||||||
|
private LocalDateTime time; // 时间
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
@Data
|
||||||
|
@TableName("live_room")
|
||||||
|
public class LiveRoom {
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String title; // 直播间标题
|
||||||
|
private String description; // 直播间描述
|
||||||
|
private String coverUrl; // 封面图
|
||||||
|
private Long userId; // 主播ID
|
||||||
|
private String username; // 主播名称
|
||||||
|
private String streamKey; // 推流密钥
|
||||||
|
private String rtmpUrl; // RTMP推流地址
|
||||||
|
private String hlsUrl; // HLS播放地址
|
||||||
|
private String status; // 状态:PREPARING-准备中,LIVING-直播中,ENDED-已结束
|
||||||
|
private Integer onlineCount; // 在线人数
|
||||||
|
private Integer likeCount; // 点赞数
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LiveService {
|
||||||
|
|
||||||
|
private final LiveRoomMapper liveRoomMapper;
|
||||||
|
private final SrsClient srsClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建直播间
|
||||||
|
*/
|
||||||
|
public LiveRoom createLiveRoom(LiveRoomDTO dto) {
|
||||||
|
LiveRoom room = new LiveRoom();
|
||||||
|
BeanUtils.copyProperties(dto, room);
|
||||||
|
|
||||||
|
// 生成推流密钥
|
||||||
|
String streamKey = generateStreamKey();
|
||||||
|
room.setStreamKey(streamKey);
|
||||||
|
|
||||||
|
// 生成推流地址
|
||||||
|
String rtmpUrl = generateRtmpUrl(streamKey);
|
||||||
|
room.setRtmpUrl(rtmpUrl);
|
||||||
|
|
||||||
|
// 生成播放地址
|
||||||
|
String hlsUrl = generateHlsUrl(streamKey);
|
||||||
|
room.setHlsUrl(hlsUrl);
|
||||||
|
|
||||||
|
room.setStatus("PREPARING");
|
||||||
|
room.setOnlineCount(0);
|
||||||
|
room.setLikeCount(0);
|
||||||
|
|
||||||
|
liveRoomMapper.insert(room);
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始直播
|
||||||
|
*/
|
||||||
|
public void startLive(Long roomId) {
|
||||||
|
LiveRoom room = liveRoomMapper.selectById(roomId);
|
||||||
|
room.setStatus("LIVING");
|
||||||
|
liveRoomMapper.updateById(room);
|
||||||
|
|
||||||
|
// 开始录制
|
||||||
|
startRecording(room.getStreamKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束直播
|
||||||
|
*/
|
||||||
|
public void endLive(Long roomId) {
|
||||||
|
LiveRoom room = liveRoomMapper.selectById(roomId);
|
||||||
|
room.setStatus("ENDED");
|
||||||
|
liveRoomMapper.updateById(room);
|
||||||
|
|
||||||
|
// 停止录制
|
||||||
|
stopRecording(room.getStreamKey());
|
||||||
|
|
||||||
|
// 生成回放地址
|
||||||
|
String replayUrl = generateReplayUrl(room.getStreamKey());
|
||||||
|
room.setReplayUrl(replayUrl);
|
||||||
|
liveRoomMapper.updateById(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始录制
|
||||||
|
*/
|
||||||
|
private void startRecording(String streamKey) {
|
||||||
|
srsClient.startRecord(streamKey, new RecordConfig()
|
||||||
|
.setFormat("flv")
|
||||||
|
.setFilePath("/recordings/" + streamKey + ".flv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止录制
|
||||||
|
*/
|
||||||
|
private void stopRecording(String streamKey) {
|
||||||
|
srsClient.stopRecord(streamKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.guwan.backend.websocket;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.guwan.backend.entity.LiveMessage;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentHashSet;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class LiveWebSocketHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
|
private static final Map<String, Set<WebSocketSession>> roomSessions = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<String, WebSocketSession> userSessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(WebSocketSession session) {
|
||||||
|
String roomId = getRoomId(session);
|
||||||
|
String userId = getUserId(session);
|
||||||
|
|
||||||
|
// 加入房间
|
||||||
|
roomSessions.computeIfAbsent(roomId, k -> new ConcurrentHashSet<>()).add(session);
|
||||||
|
userSessions.put(userId, session);
|
||||||
|
|
||||||
|
// 广播进入消息
|
||||||
|
broadcastMessage(roomId, createEnterMessage(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleTextMessage(WebSocketSession session, TextMessage message) {
|
||||||
|
String roomId = getRoomId(session);
|
||||||
|
LiveMessage liveMessage = JSON.parseObject(message.getPayload(), LiveMessage.class);
|
||||||
|
|
||||||
|
// 处理不同类型的消息
|
||||||
|
switch (liveMessage.getType()) {
|
||||||
|
case "CHAT":
|
||||||
|
broadcastMessage(roomId, message);
|
||||||
|
break;
|
||||||
|
case "GIFT":
|
||||||
|
handleGiftMessage(roomId, liveMessage);
|
||||||
|
break;
|
||||||
|
case "LIKE":
|
||||||
|
handleLikeMessage(roomId, liveMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||||
|
String roomId = getRoomId(session);
|
||||||
|
String userId = getUserId(session);
|
||||||
|
|
||||||
|
// 离开房间
|
||||||
|
roomSessions.get(roomId).remove(session);
|
||||||
|
userSessions.remove(userId);
|
||||||
|
|
||||||
|
// 广播离开消息
|
||||||
|
broadcastMessage(roomId, createLeaveMessage(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastMessage(String roomId, TextMessage message) {
|
||||||
|
Set<WebSocketSession> sessions = roomSessions.get(roomId);
|
||||||
|
if (sessions != null) {
|
||||||
|
sessions.forEach(session -> {
|
||||||
|
try {
|
||||||
|
session.sendMessage(message);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("发送消息失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue