添加弹幕功能代码
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