【m】远端视频开发
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
// 更新本地媒体状态文本
|
// 更新本地媒体状态文本
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user