【m】远端视频开发

This commit is contained in:
2026-03-04 22:29:10 +08:00
parent a5d9368ae1
commit 957ab561bf
7 changed files with 156 additions and 57 deletions

View File

@@ -148,11 +148,12 @@
<div class="absolute inset-0 video-fade-in"> <div class="absolute inset-0 video-fade-in">
<!-- [DATA_FIELD: remoteUser.videoStream] [TYPE: MediaStream | null] [BIND: srcObject] --> <!-- [DATA_FIELD: remoteUser.videoStream] [TYPE: MediaStream | null] [BIND: srcObject] -->
<!-- [FALLBACK: remoteUser.avatar] [TYPE: string] [URL] --> <!-- [FALLBACK: remoteUser.avatar] [TYPE: string] [URL] -->
<img id="remoteVideo" <video id="remoteVideo"
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=1280&h=720&fit=crop"
alt="对方视频" alt="对方视频"
class="w-full h-full object-cover" class="w-full h-full object-cover"
autoplay
data-field="remoteUser.videoStream"> data-field="remoteUser.videoStream">
</video>
<!-- 远端信息覆盖层 --> <!-- 远端信息覆盖层 -->
<div class="absolute top-6 left-6 glass px-4 py-2 rounded-full flex items-center gap-3"> <div class="absolute top-6 left-6 glass px-4 py-2 rounded-full flex items-center gap-3">

View File

@@ -317,13 +317,6 @@ function bindDomEvents() {
} }
} }
/**
* 初始化WebRTC
*/
function initWebRTC() {
// 这里可以添加WebRTC初始化代码
console.log('Initializing WebRTC...');
}
// 页面加载完成后初始化 // 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener('DOMContentLoaded', async () => {
@@ -338,13 +331,16 @@ window.addEventListener('DOMContentLoaded', async () => {
} }
// 初始化 store // 初始化 store
store.init(); //await store.init();
// 加入通话
store.joinCall(connectionId);
// 初始化渲染器 // 初始化渲染器
const renderer = new UIRenderer(store); new UIRenderer(store);
// 加入通话
await store.joinCall(connectionId);
// 设置WebRTC连接
await store.setUp(connectionId);
// 绑定DOM事件 // 绑定DOM事件
bindDomEvents(); bindDomEvents();

View File

@@ -89,6 +89,9 @@ class UIRenderer {
this.renderLocalStream(state.localStream); this.renderLocalStream(state.localStream);
this.renderLocalVideo(state.session.localUser, state.localStream); this.renderLocalVideo(state.session.localUser, state.localStream);
break; break;
case 'REMOTE_STREAM_OBTAINED':
this.renderRemoteStream(state.remoteStream);
break;
case 'REMOTE_MEDIA_CHANGE': case 'REMOTE_MEDIA_CHANGE':
this.renderRemoteVideo(state.session.remoteUser); this.renderRemoteVideo(state.session.remoteUser);
this.renderUserList(state.session.localUser, state.session.remoteUser); this.renderUserList(state.session.localUser, state.session.remoteUser);
@@ -189,6 +192,22 @@ class UIRenderer {
} }
} }
// 渲染远程视频流
renderRemoteStream(stream) {
if (this.elements.remoteVideo && stream) {
this.elements.remoteVideo.srcObject = stream;
this.elements.remoteVideo.autoplay = true;
console.log('Remote stream set successfully:', this.elements.remoteVideo.srcObject);
// 隐藏断开连接覆盖层
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.add('hidden');
}
} else {
console.error('Either remoteVideo element or stream is missing');
}
}
// 渲染本地用户状态 // 渲染本地用户状态
renderLocalUserStatus(localUser) { renderLocalUserStatus(localUser) {
// 更新本地媒体状态文本 // 更新本地媒体状态文本

View File

@@ -15,9 +15,9 @@ const defaultStreamHeight = 720;
class CallStateManager { class CallStateManager {
constructor() { constructor() {
let renderstreaming; // WebRTC连接管理实例 const renderstreaming=null; // WebRTC连接管理实例
let useWebSocket; // 是否使用WebSocket信令 const useWebSocket=null; // 是否使用WebSocket信令
let connectionId; // 连接ID const connectionId=null; // 连接ID
// 核心状态 // 核心状态
this.state = { this.state = {
session: { session: {
@@ -33,9 +33,6 @@ class CallStateManager {
// 监听器数组 // 监听器数组
this.listeners = []; this.listeners = [];
// 初始化
//this.init();
} }
// 订阅状态变化 // 订阅状态变化
@@ -52,20 +49,19 @@ class CallStateManager {
} }
// 初始化 // 初始化
init() { async init() {
// 启动通话时长计时器 // 启动通话时长计时器
this.durationInterval = setInterval(() => { this.durationInterval = setInterval(() => {
this.state.session.duration++; this.state.session.duration++;
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration }); this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
}, 1000); }, 1000);
// 初始化配置 // 初始化配置
this.setupConfig(); await this.setupConfig();
// 获取本地摄像头视频流 // 获取本地摄像头视频流
this.getLocalStream(); await this.getLocalStream();
// 模拟远端音频活动 (实际应由 WebRTC VAD 检测触发) // 模拟远端音频活动 (实际应由 WebRTC VAD 检测触发)
this.simulateRemoteActivity(); //this.simulateRemoteActivity();
// 模拟网络质量变化 // 模拟网络质量变化
this.simulateNetworkChange(); this.simulateNetworkChange();
@@ -176,26 +172,46 @@ class CallStateManager {
async setUp(connectionId) { async setUp(connectionId) {
//TODO //TODO
this.connectionId = connectionId; // 获取连接ID this.connectionId = connectionId; // 获取连接ID
codecPreferences.disabled = true; // 禁用编解码器选择
// 确保本地流已经初始化
if (!this.state.localStream) {
console.log('Local stream not available, waiting for initialization...');
// 等待localStream初始化
await new Promise((resolve) => {
const checkStream = () => {
if (this.state.localStream) {
resolve();
} else {
setTimeout(checkStream, 100);
}
};
checkStream();
});
}
// 创建信令实例 // 创建信令实例
const signaling = useWebSocket ? new WebSocketSignaling() : new Signaling(); const signaling = this.useWebSocket ? new WebSocketSignaling() : new Signaling();
const config = getRTCConfiguration(); // 获取RTC配置 const config = getRTCConfiguration(); // 获取RTC配置
this.renderstreaming = new RenderStreaming(signaling, config); // 创建WebRTC连接管理实例 this.renderstreaming = new RenderStreaming(signaling, config); // 创建WebRTC连接管理实例
// 连接建立回调 // 连接建立回调
this.renderstreaming.onConnect = () => { this.renderstreaming.onConnect = () => {
if (this.state.localStream) {
const tracks = this.state.localStream.getTracks(); // 获取本地媒体轨道 const tracks = this.state.localStream.getTracks(); // 获取本地媒体轨道
for (const track of tracks) { for (const track of tracks) {
this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }); // 添加发送轨道 this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }); // 添加发送轨道
} }
setCodecPreferences(); // 设置编解码器偏好 this.setCodecPreferences(); // 设置编解码器偏好
showStatsMessage(); // 显示统计信息 this.showStatsMessage(); // 显示统计信息
} else {
console.error('Local stream is not available');
showNotification('本地视频流不可用', 'error');
}
}; };
// 连接断开回调 // 连接断开回调
this.renderstreaming.onDisconnect = () => { this.renderstreaming.onDisconnect = () => {
hangUp(); // 挂断连接 this.hangUp(); // 挂断连接
}; };
// 轨道事件回调 // 轨道事件回调
@@ -206,6 +222,8 @@ class CallStateManager {
this.state.remoteStream = new MediaStream(); this.state.remoteStream = new MediaStream();
} }
this.state.remoteStream.addTrack(data.track); this.state.remoteStream.addTrack(data.track);
// 通知UI远程流已更新
this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: this.state.remoteStream });
} }
}; };
@@ -221,21 +239,21 @@ class CallStateManager {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async hangUp() { async hangUp() {
clearStatsMessage(); // 清除统计信息 this.clearStatsMessage(); // 清除统计信息
messageDiv.style.display = 'block'; this.messageDiv.style.display = 'block';
messageDiv.innerText = `Disconnect peer on ${connectionId}.`; this.messageDiv.innerText = `Disconnect peer on ${this.connectionId}.`;
// 删除连接并停止WebRTC // 删除连接并停止WebRTC
await renderstreaming.deleteConnection(); await this.renderstreaming.deleteConnection();
await renderstreaming.stop(); await this.renderstreaming.stop();
renderstreaming = null; this.renderstreaming = null;
remoteVideo.srcObject = null; // 清除远程视频源 this.remoteVideo.srcObject = null; // 清除远程视频源
connectionId = null; this.connectionId = null;
// 启用编解码器选择 // 启用编解码器选择
if (supportsSetCodecPreferences) { if (this.supportsSetCodecPreferences) {
codecPreferences.disabled = false; this.codecPreferences.disabled = false;
} }
} }
/** /**
@@ -245,8 +263,8 @@ class CallStateManager {
/** @type {RTCRtpCodecCapability[] | null} */ /** @type {RTCRtpCodecCapability[] | null} */
let selectedCodecs = null; let selectedCodecs = null;
if (supportsSetCodecPreferences) { if (this.supportsSetCodecPreferences) {
const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex]; const preferredCodec = this.codecPreferences.options[this.codecPreferences.selectedIndex];
if (preferredCodec.value !== '') { if (preferredCodec.value !== '') {
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' '); const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
const { codecs } = RTCRtpSender.getCapabilities('video'); const { codecs } = RTCRtpSender.getCapabilities('video');
@@ -261,7 +279,7 @@ class CallStateManager {
} }
// 获取视频收发器并设置编解码器偏好 // 获取视频收发器并设置编解码器偏好
const transceivers = renderstreaming.getTransceivers().filter(t => t.receiver.track.kind == "video"); const transceivers = this.renderstreaming.getTransceivers().filter(t => t.receiver.track.kind == "video");
if (transceivers && transceivers.length > 0) { if (transceivers && transceivers.length > 0) {
transceivers.forEach(t => t.setCodecPreferences(selectedCodecs)); transceivers.forEach(t => t.setCodecPreferences(selectedCodecs));
} }
@@ -310,26 +328,26 @@ class CallStateManager {
} }
// 加入通话 // 加入通话
joinCall(connectionId) { async joinCall(connectionId) {
this.state.session.status = 'connecting'; this.state.session.status = 'connecting';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
showNotification(`正在加入通话 (${connectionId})`); showNotification(`正在加入通话 (${connectionId})`);
// 初始化 // 初始化
this.init(); await this.init();
// 保存连接ID // 保存连接ID
this.connectionId = connectionId; this.connectionId = connectionId;
} }
// 创建通话 // 创建通话
createCall() { async createCall() {
this.state.session.status = 'connecting'; this.state.session.status = 'connecting';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
showNotification('正在创建通话...'); showNotification('正在创建通话...');
// 初始化 // 初始化
this.init(); await this.init();
} }
// 模拟远端活动 (开发测试用) // 模拟远端活动 (开发测试用)
@@ -364,6 +382,18 @@ class CallStateManager {
// socket.emit('media-state-changed', payload); // socket.emit('media-state-changed', payload);
} }
// 显示统计信息
showStatsMessage() {
console.log('Showing stats message');
// 这里可以添加显示统计信息的逻辑
}
// 清除统计信息
clearStatsMessage() {
console.log('Clearing stats message');
// 这里可以添加清除统计信息的逻辑
}
// Getters // Getters
getState() { return this.state; } getState() { return this.state; }
getLocalUser() { return this.state.session.localUser; } getLocalUser() { return this.state.session.localUser; }

View File

@@ -7,7 +7,7 @@ import Offer from './offer';
import Answer from './answer'; import Answer from './answer';
import Candidate from './candidate'; import Candidate from './candidate';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { onGetAllConnectionIds } from './websockethandler';
/** /**
* 断开连接记录类 * 断开连接记录类
* 用于记录断开连接的信息 * 用于记录断开连接的信息
@@ -1073,6 +1073,38 @@ function onGetConnections(req: Request, res: Response): void {
res.json({ rooms: rooms, totalRooms: rooms.length }); res.json({ rooms: rooms, totalRooms: rooms.length });
} }
/**
* @swagger
* /signaling/connection-ids:
* get:
* summary: 获取所有连接ID
* description: 获取所有当前活跃的连接ID
* security:
* - sessionAuth: []
* responses:
* 200:
* description: 成功获取连接ID列表
* content:
* application/json:
* schema:
* type: object
* properties:
* connectionIds:
* type: array
* items:
* type: string
* description: 连接ID
* totalCount:
* type: number
* description: 总连接数
*/
function getAllConnectionIds(req: Request, res: Response): void {
// 获取所有连接ID
const connectionIds = onGetAllConnectionIds();
// 返回JSON响应包含连接ID列表和总数量
res.json({ connectionIds: connectionIds, totalCount: connectionIds.length });
}
/** /**
* 导出HTTP处理器函数 * 导出HTTP处理器函数
*/ */
@@ -1091,5 +1123,6 @@ export {
postOffer, // 处理offer信令消息 postOffer, // 处理offer信令消息
postAnswer, // 处理answer信令消息 postAnswer, // 处理answer信令消息
postCandidate, // 处理candidate信令消息 postCandidate, // 处理candidate信令消息
onGetConnections // 获取房间和用户信息 onGetConnections, // 获取房间和用户信息
getAllConnectionIds // 获取所有连接ID
}; };

View File

@@ -60,7 +60,7 @@ function reset(mode: string): void {
*/ */
function add(ws: WebSocket): void { function add(ws: WebSocket): void {
// 为新连接创建空的连接ID集合 // 为新连接创建空的连接ID集合
var id = new Set<string>(); const id = new Set<string>();
clients.set(ws, id); clients.set(ws, id);
// 记录添加WebSocket连接的日志 // 记录添加WebSocket连接的日志
console.log(`Add WebSocket: ${id}`); console.log(`Add WebSocket: ${id}`);
@@ -347,7 +347,22 @@ function onCandidate(ws: WebSocket, message: any): void {
clearInterval((ws as any).heartbeatTimer); clearInterval((ws as any).heartbeatTimer);
} }
} }
/**
* 处理获取所有连接ID的请求
* @param ws WebSocket连接实例
*/
function onGetAllConnectionIds(): string[] {
// 获取所有connectionId
const connectionIds = Array.from(connectionPair.keys());
// 发送连接ID列表给客户端
// ws.send(JSON.stringify({
// type: "connection-ids",
// connectionIds: connectionIds
// }));
return connectionIds;
}
/** /**
* 导出WebSocket处理器函数 * 导出WebSocket处理器函数
*/ */
export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate,onCallConnectionId, onBroadcast, AddHeartbeat, RemoveHeartbeat }; export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate, onCallConnectionId, onBroadcast, onGetAllConnectionIds, AddHeartbeat, RemoveHeartbeat };

View File

@@ -2,6 +2,11 @@ import * as express from 'express';
import * as handler from'./class/httphandler'; import * as handler from'./class/httphandler';
const router: express.Router = express.Router(); const router: express.Router = express.Router();
// 不需要会话ID的路由
router.get('/connection-ids', handler.getAllConnectionIds);
// 需要会话ID的路由
router.use(handler.checkSessionId); router.use(handler.checkSessionId);
router.get('/connection', handler.getConnection); router.get('/connection', handler.getConnection);
router.get('/offer', handler.getOffer); router.get('/offer', handler.getOffer);