添加弹幕功能代码
parent
2a8c4d6805
commit
d38d33f578
@ -0,0 +1,22 @@
|
||||
package com.supervision.config;
|
||||
|
||||
import com.supervision.service.danmaku.DanmakuWebSocketHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
private final DanmakuWebSocketHandler webSocketHandler;
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(webSocketHandler, "/ws/danmaku")
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package com.supervision.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.supervision.domain.LivetalkingChatDTO;
|
||||
import com.supervision.dto.DigitalHumanDTO;
|
||||
import com.supervision.dto.DigitalHumanVoiceDTO;
|
||||
import com.supervision.dto.R;
|
||||
import com.supervision.service.danmaku.DigitalHumanManageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 数字人控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/digitalHuman")
|
||||
@RequiredArgsConstructor
|
||||
public class DigitalHumanController {
|
||||
|
||||
private final DigitalHumanManageService digitalHumanManageService;
|
||||
|
||||
/**
|
||||
* 分页查询数字人列表
|
||||
* @param page 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/pageList")
|
||||
public R<IPage<DigitalHumanDTO>> pageList(@RequestParam(name = "page", required = false,defaultValue = "1") Integer page,
|
||||
@RequestParam (name = "pageSize", required = false,defaultValue = "10") Integer pageSize) {
|
||||
|
||||
IPage<DigitalHumanDTO> paged = digitalHumanManageService.pageList(page, pageSize);
|
||||
return R.ok(paged);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换数字人语音
|
||||
* @param digitalHumanVoiceDTO 包含语音ID和数字人ID的DTO
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/switchVoice")
|
||||
public R<Void> setVoice(@RequestBody DigitalHumanVoiceDTO digitalHumanVoiceDTO) {
|
||||
digitalHumanManageService.setVoice(digitalHumanVoiceDTO);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/livetalking/chatCallBack")
|
||||
public R<Void> chatCallBack(@RequestBody LivetalkingChatDTO digitalHumanVoiceDTO) {
|
||||
digitalHumanManageService.chatCallBack(digitalHumanVoiceDTO);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.supervision.domain;
|
||||
|
||||
import com.supervision.dto.DanmakuMessage;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LivetalkingChatDTO {
|
||||
|
||||
/**
|
||||
* 消息id
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 数字人id
|
||||
*/
|
||||
private String humanId;
|
||||
|
||||
|
||||
/**
|
||||
* 房间id
|
||||
*/
|
||||
private String roomId;
|
||||
|
||||
/**
|
||||
* 问题
|
||||
*/
|
||||
private String query;
|
||||
|
||||
/**
|
||||
* 回答
|
||||
*/
|
||||
private String answer;
|
||||
|
||||
public LivetalkingChatDTO() {
|
||||
}
|
||||
|
||||
public LivetalkingChatDTO(DanmakuMessage danmakuMessage) {
|
||||
this.roomId = danmakuMessage.getRoomId();
|
||||
this.query = danmakuMessage.getContent();
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.supervision.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DigitalHumanVoiceDTO {
|
||||
|
||||
private String voiceId;
|
||||
|
||||
private String digitalHumanId;
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.supervision.service;
|
||||
|
||||
import com.supervision.domain.LivetalkingChatDTO;
|
||||
|
||||
/**
|
||||
* 数字人直播管理服务接口
|
||||
*/
|
||||
public interface LivetalkingService {
|
||||
|
||||
|
||||
void chat(LivetalkingChatDTO livetalkingChatDTO);
|
||||
|
||||
|
||||
void setVoice(String voiceId, String digitalHumanId);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.supervision.service.danmaku;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.supervision.domain.LivetalkingChatDTO;
|
||||
import com.supervision.dto.DigitalHumanDTO;
|
||||
import com.supervision.dto.DigitalHumanVoiceDTO;
|
||||
|
||||
public interface DigitalHumanManageService {
|
||||
|
||||
/**
|
||||
* 分页查询数字人信息
|
||||
* @param page 页码
|
||||
* @param pageSize 每页大小
|
||||
* @return
|
||||
*/
|
||||
IPage<DigitalHumanDTO> pageList(Integer page, Integer pageSize);
|
||||
|
||||
|
||||
void setVoice(DigitalHumanVoiceDTO digitalHumanVoiceDTO);
|
||||
|
||||
void chatCallBack(LivetalkingChatDTO digitalHumanVoiceDTO);
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.supervision.service.danmaku;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* WebSocket会话管理器
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketSessionManager {
|
||||
// 房间ID -> 会话列表
|
||||
private final ConcurrentMap<String, Set<WebSocketSession>> roomSessions = new ConcurrentHashMap<>();
|
||||
|
||||
public void addSession(String roomId, WebSocketSession session) {
|
||||
roomSessions.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(session);
|
||||
}
|
||||
|
||||
public void removeSession(String roomId, WebSocketSession session) {
|
||||
Set<WebSocketSession> sessions = roomSessions.get(roomId);
|
||||
if (sessions != null) {
|
||||
sessions.remove(session);
|
||||
if (sessions.isEmpty()) {
|
||||
roomSessions.remove(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Set<WebSocketSession> getSessionsByRoom(String roomId) {
|
||||
return roomSessions.getOrDefault(roomId, Collections.emptySet());
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.supervision.service.impl;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.supervision.domain.LivetalkingChatDTO;
|
||||
import com.supervision.domain.DigitalHuman;
|
||||
import com.supervision.domain.DigitalHumanDialogueLog;
|
||||
import com.supervision.domain.VoiceInfo;
|
||||
import com.supervision.dto.DanmakuMessage;
|
||||
import com.supervision.dto.DigitalHumanDTO;
|
||||
import com.supervision.dto.DigitalHumanVoiceDTO;
|
||||
import com.supervision.service.DigitalHumanDialogueLogService;
|
||||
import com.supervision.service.LivetalkingService;
|
||||
import com.supervision.service.VoiceInfoService;
|
||||
import com.supervision.service.danmaku.DanmakuPublisher;
|
||||
import com.supervision.service.danmaku.DigitalHumanManageService;
|
||||
import com.supervision.service.DigitalHumanService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DigitalHumanManageServiceImpl implements DigitalHumanManageService {
|
||||
|
||||
private final DigitalHumanService digitalHumanService;
|
||||
|
||||
private final LivetalkingService livetalkingService;
|
||||
|
||||
private final VoiceInfoService voiceInfoService;
|
||||
|
||||
private final DigitalHumanDialogueLogService dialogueLogService;
|
||||
@Override
|
||||
public IPage<DigitalHumanDTO> pageList(Integer page, Integer pageSize) {
|
||||
|
||||
Page<DigitalHuman> paged = digitalHumanService.page(Page.of(page, pageSize));
|
||||
return paged.convert(DigitalHumanDTO::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVoice(DigitalHumanVoiceDTO digitalHumanVoiceDTO) {
|
||||
|
||||
VoiceInfo voiceInfo = voiceInfoService.getById(digitalHumanVoiceDTO.getVoiceId());
|
||||
Assert.notNull(voiceInfo, "语音信息不存在");
|
||||
livetalkingService.setVoice(voiceInfo.getVoiceCode(), digitalHumanVoiceDTO.getDigitalHumanId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatCallBack(LivetalkingChatDTO digitalHumanVoiceDTO) {
|
||||
// 这里可以添加处理聊天回调的逻辑
|
||||
log.info("Received chat callback: {}", JSONUtil.toJsonStr(digitalHumanVoiceDTO));
|
||||
|
||||
DigitalHumanDialogueLog dialogueLog = new DigitalHumanDialogueLog();
|
||||
dialogueLog.setId(digitalHumanVoiceDTO.getMessageId());
|
||||
dialogueLog.setDigitalHumanId(digitalHumanVoiceDTO.getHumanId());
|
||||
dialogueLog.setUserInput(digitalHumanVoiceDTO.getQuery());
|
||||
dialogueLog.setSystemOut(digitalHumanVoiceDTO.getAnswer());
|
||||
dialogueLogService.updateById(dialogueLog);
|
||||
|
||||
// 消息推送到弹幕系统
|
||||
DanmakuMessage danmakuMessage = new DanmakuMessage();
|
||||
danmakuMessage.setContent(digitalHumanVoiceDTO.getAnswer());
|
||||
danmakuMessage.setRoomId(digitalHumanVoiceDTO.getRoomId());
|
||||
danmakuMessage.setUserId(digitalHumanVoiceDTO.getHumanId());
|
||||
danmakuMessage.setNickname("智能助手");
|
||||
danmakuMessage.setType("1");
|
||||
DanmakuPublisher.getInstance().publish(danmakuMessage);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package com.supervision.service.impl;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.supervision.domain.LivetalkingChatDTO;
|
||||
import com.supervision.service.LivetalkingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LivetalkingServiceImpl implements LivetalkingService {
|
||||
|
||||
@Value("${livetalking.url}")
|
||||
private String livetalkingUrl;
|
||||
|
||||
@Value("${livetalking.session.id:0}")
|
||||
private int sessionId;
|
||||
|
||||
@Override
|
||||
public void chat(LivetalkingChatDTO livetalkingChatDTO) {
|
||||
String url = livetalkingUrl + "/human";
|
||||
log.info("Sending chat request to: " + url);
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("text", livetalkingChatDTO.getQuery());
|
||||
param.put("type", "chat");
|
||||
param.put("interrupt",false);
|
||||
param.put("sessionid", sessionId);
|
||||
|
||||
// 添加额外的参数
|
||||
livetalkingChatDTO.setQuery(null);
|
||||
param.put("extra", livetalkingChatDTO);
|
||||
HttpRequest request = HttpUtil.createPost(url)
|
||||
.body(JSONUtil.toJsonStr(param))
|
||||
.header("Content-Type", "application/json");
|
||||
try (HttpResponse response = request.execute()) {
|
||||
String body = response.body();
|
||||
log.info("Chat response: {}", body);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVoice(String voiceId, String digitalHumanId) {
|
||||
Assert.notEmpty(voiceId, "语音ID不能为空");
|
||||
String url = livetalkingUrl + "/set_voice?voiceId=" + voiceId;
|
||||
HttpRequest request = HttpUtil.createGet(url);
|
||||
try (HttpResponse execute = request.execute()){
|
||||
String body = execute.body();
|
||||
log.info("设置语音结果: {}", body);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
class DanmakuClient {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.socket = null;
|
||||
this.roomId = null;
|
||||
this.tracks = [];
|
||||
this.initTracks();
|
||||
}
|
||||
|
||||
initTracks() {
|
||||
const height = this.container.clientHeight;
|
||||
const trackHeight = height / 5;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
this.tracks.push({
|
||||
top: i * trackHeight,
|
||||
inUse: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connect(wsUrl, roomId) {
|
||||
if (this.socket) this.disconnect();
|
||||
|
||||
this.roomId = roomId;
|
||||
this.socket = new WebSocket(`${wsUrl}?roomId=${roomId}`);
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
document.getElementById('connect').disabled = true;
|
||||
document.getElementById('disconnect').disabled = false;
|
||||
};
|
||||
|
||||
this.socket.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
document.getElementById('connect').disabled = false;
|
||||
document.getElementById('disconnect').disabled = true;
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
const danmaku = JSON.parse(event.data);
|
||||
this.displayDanmaku(danmaku);
|
||||
};
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
sendDanmaku(content, user, style) {
|
||||
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
||||
alert('请先连接到弹幕服务器');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content || content.trim() === '') {
|
||||
alert('弹幕内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
roomId: this.roomId,
|
||||
content,
|
||||
userId: user || 'anonymous',
|
||||
color: style?.color || '#ffffff',
|
||||
size: style?.size || 24
|
||||
};
|
||||
|
||||
this.socket.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
displayDanmaku(danmaku) {
|
||||
const danmakuElement = document.createElement('div');
|
||||
danmakuElement.className = 'danmaku';
|
||||
danmakuElement.textContent = danmaku.content;
|
||||
danmakuElement.style.color = danmaku.color || '#ffffff';
|
||||
danmakuElement.style.fontSize = `${danmaku.size || 24}px`;
|
||||
|
||||
const track = this.findAvailableTrack();
|
||||
if (!track) return;
|
||||
|
||||
danmakuElement.style.top = `${track.top}px`;
|
||||
this.container.appendChild(danmakuElement);
|
||||
|
||||
const startX = this.container.clientWidth;
|
||||
const endX = -danmakuElement.clientWidth;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const progress = (timestamp - startTime) / 10000;
|
||||
|
||||
if (progress < 1) {
|
||||
const x = startX + (endX - startX) * progress;
|
||||
danmakuElement.style.transform = `translateX(${x}px)`;
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
this.container.removeChild(danmakuElement);
|
||||
track.inUse = false;
|
||||
}
|
||||
};
|
||||
|
||||
track.inUse = true;
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
findAvailableTrack() {
|
||||
return this.tracks.find(track => !track.inUse);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化客户端
|
||||
const container = document.getElementById('danmaku-container');
|
||||
const client = new DanmakuClient(container);
|
||||
|
||||
// 绑定事件
|
||||
document.getElementById('connect').addEventListener('click', () => {
|
||||
const roomId = document.getElementById('roomId').value;
|
||||
client.connect('ws://' + window.location.host + '/ai-platform/ws/danmaku', roomId);
|
||||
});
|
||||
|
||||
document.getElementById('disconnect').addEventListener('click', () => {
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
document.getElementById('send').addEventListener('click', () => {
|
||||
const content = document.getElementById('message').value;
|
||||
const color = document.getElementById('color').value;
|
||||
const size = document.getElementById('size').value;
|
||||
|
||||
client.sendDanmaku(content, null, { color, size });
|
||||
document.getElementById('message').value = '';
|
||||
});
|
||||
|
||||
document.getElementById('message').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
document.getElementById('send').click();
|
||||
}
|
||||
});
|
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>弹幕系统 - Spring Boot</title>
|
||||
<style>
|
||||
#danmaku-container {
|
||||
width: 800px;
|
||||
height: 500px;
|
||||
background: #000;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.danmaku {
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-shadow: 1px 1px 2px #000;
|
||||
will-change: transform;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>弹幕系统</h1>
|
||||
<div>
|
||||
<label>房间ID: <input id="roomId" value="room1"></label>
|
||||
<button id="connect">连接</button>
|
||||
<button id="disconnect" disabled>断开</button>
|
||||
</div>
|
||||
|
||||
<div id="danmaku-container"></div>
|
||||
|
||||
<div>
|
||||
<input id="message" placeholder="输入弹幕内容">
|
||||
<button id="send">发送</button>
|
||||
<div>
|
||||
<label>颜色: <input type="color" id="color" value="#ffffff"></label>
|
||||
<label>大小:
|
||||
<select id="size">
|
||||
<option value="16">小</option>
|
||||
<option value="24" selected>中</option>
|
||||
<option value="32">大</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="danmaku.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue