【m】分窗渲染测试没问题

This commit is contained in:
2026-04-24 23:03:41 +08:00
parent b66b639df0
commit eba0d07c83
4 changed files with 403 additions and 303 deletions

View File

@@ -326,15 +326,15 @@ class CallStateManager {
{ googTypingNoiseDetection: true } // 启用打字噪声检测 { googTypingNoiseDetection: true } // 启用打字噪声检测
] ]
}; };
this.renderstreaming = new RenderStreaming(signaling, config); // 创建WebRTC连接管理实例 this.renderstreaming = new RenderStreaming(signaling, config);
this.renderstreaming.onNewPeer = (connectionId) => { this.renderstreaming.onNewPeer = (participantId) => {
console.log(`New peer created for ${connectionId}, adding local tracks`); console.log(`New peer created for ${participantId}, adding local tracks`);
if (this.state.localStream) { 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' }, participantId);
} }
this.setCodecPreferences(); this.setCodecPreferences(participantId);
} }
}; };
// 连接建立回调 // 连接建立回调
@@ -375,44 +375,39 @@ this.renderstreaming.onNewPeer = (connectionId) => {
}; };
// participant离开回调host收到房间仍然存在 // participant离开回调host收到房间仍然存在
this.renderstreaming.onParticipantLeft = (connectionId) => { this.renderstreaming.onParticipantLeft = (participantId) => {
console.log(`Participant left: ${connectionId}, room still active`); console.log(`Participant left: ${participantId}, room still active`);
// 更新远程用户状态,但不关闭房间
this.updateRemoteUserStatus('offline'); this.updateRemoteUserStatus('offline');
this.updateRemoteUserNetworkQuality('no_signal'); this.updateRemoteUserNetworkQuality('no_signal');
showNotification('对方已离开通话', 'warning'); showNotification('对方已离开通话', 'warning');
// 清理该 participant 的远端流 // 清理该 participant 的远端流
if (this.state.remoteStreams[connectionId]) { if (this.state.remoteStreams[participantId]) {
this.state.remoteStreams[connectionId].getTracks().forEach(track => track.stop()); this.state.remoteStreams[participantId].getTracks().forEach(track => track.stop());
delete this.state.remoteStreams[connectionId]; delete this.state.remoteStreams[participantId];
} }
// 同时清理单路远端流(兼容)
if (this.state.remoteStream) { if (this.state.remoteStream) {
this.state.remoteStream.getTracks().forEach(track => track.stop()); this.state.remoteStream.getTracks().forEach(track => track.stop());
this.state.remoteStream = null; this.state.remoteStream = null;
} }
// 通知UI更新 // 通知UI更新用participantId作为connectionId传给renderer
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: connectionId }); this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId });
}; };
// 轨道事件回调 // 轨道事件回调
this.renderstreaming.onTrackEvent = (data) => { this.renderstreaming.onTrackEvent = (data) => {
const direction = data.transceiver.direction; const direction = data.transceiver.direction;
if (direction == "sendrecv" || direction == "recvonly") { if (direction == "sendrecv" || direction == "recvonly") {
// 获取当前连接的远端 // 使用participantId区分不同participant的
const trackConnectionId = this.connectionId; const trackParticipantId = data.participantId || this.connectionId;
// Host端: 每个participant有独立的远端流
// Participant端: 只有一个host的远端流
const isHost = this.role === 'host'; const isHost = this.role === 'host';
// 获取或创建对应的远端流
let targetStream = null; let targetStream = null;
if (isHost) { if (isHost) {
// Host端: 按 connectionId 管理多路远端流 // Host端: 按 participantId 管理多路远端流
if (!this.state.remoteStreams[trackConnectionId]) { if (!this.state.remoteStreams[trackParticipantId]) {
this.state.remoteStreams[trackConnectionId] = new MediaStream(); this.state.remoteStreams[trackParticipantId] = new MediaStream();
} }
targetStream = this.state.remoteStreams[trackConnectionId]; targetStream = this.state.remoteStreams[trackParticipantId];
} else { } else {
// Participant端: 使用单一远端流 // Participant端: 使用单一远端流
if (this.state.remoteStream == null) { if (this.state.remoteStream == null) {
@@ -423,22 +418,19 @@ this.renderstreaming.onNewPeer = (connectionId) => {
// 检查是否已经有相同类型的轨道 // 检查是否已经有相同类型的轨道
const existingTracks = targetStream.getTracks().filter(track => track.kind === data.track.kind); const existingTracks = targetStream.getTracks().filter(track => track.kind === data.track.kind);
// 移除旧的轨道
existingTracks.forEach(track => { existingTracks.forEach(track => {
targetStream.removeTrack(track); targetStream.removeTrack(track);
console.log('Removed old track:', track.kind); console.log('Removed old track:', track.kind);
}); });
// 添加新的轨道
targetStream.addTrack(data.track); targetStream.addTrack(data.track);
console.log('Added new track:', data.track.kind, 'to stream:', trackConnectionId); console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId);
// 通知UI远程流已更新 // 通知UI远程流已更新
this.notify({ this.notify({
type: 'REMOTE_STREAM_OBTAINED', type: 'REMOTE_STREAM_OBTAINED',
stream: targetStream, stream: targetStream,
connectionId: trackConnectionId, connectionId: trackParticipantId,
isHost: isHost isHost: isHost
}); });
console.log('Notified UI about remote stream update'); console.log('Notified UI about remote stream update');
@@ -585,29 +577,24 @@ this.renderstreaming.onNewPeer = (connectionId) => {
/** /**
* 设置编解码器偏好 * 设置编解码器偏好
*/ */
setCodecPreferences() { setCodecPreferences(participantId) {
/** @type {RTCRtpCodecCapability[] | null} */
let selectedCodecs = null; let selectedCodecs = null;
// 获取视频编解码器能力
const { codecs } = RTCRtpSender.getCapabilities('video'); const { codecs } = RTCRtpSender.getCapabilities('video');
if (codecs && codecs.length > 0) { if (codecs && codecs.length > 0) {
// 优先选择H.264编解码器
const h264Codec = codecs.find(c => c.mimeType === 'video/H264'); const h264Codec = codecs.find(c => c.mimeType === 'video/H264');
if (h264Codec) { if (h264Codec) {
selectedCodecs = [h264Codec]; selectedCodecs = [h264Codec];
} }
} }
if (selectedCodecs == null) return;
if (selectedCodecs == null) {
return;
}
// 获取视频收发器并设置编解码器偏好
if (this.renderstreaming) { if (this.renderstreaming) {
const transceivers = this.renderstreaming.getTransceivers().filter(t => t.receiver.track.kind == "video"); const transceivers = this.renderstreaming.getTransceivers(participantId);
if (transceivers && transceivers.length > 0) { if (transceivers && transceivers.length > 0) {
transceivers.forEach(t => t.setCodecPreferences(selectedCodecs)); const videoTransceivers = transceivers.filter(t => t.receiver.track.kind == "video");
if (videoTransceivers && videoTransceivers.length > 0) {
videoTransceivers.forEach(t => t.setCodecPreferences(selectedCodecs));
}
} }
} }
} }

View File

@@ -5,26 +5,26 @@ function uuid4() {
var temp_url = URL.createObjectURL(new Blob()); var temp_url = URL.createObjectURL(new Blob());
var uuid = temp_url.toString(); var uuid = temp_url.toString();
URL.revokeObjectURL(temp_url); URL.revokeObjectURL(temp_url);
return uuid.split(/[:/]/g).pop().toLowerCase(); // remove prefixes return uuid.split(/[:/]/g).pop().toLowerCase();
} }
export class RenderStreaming { export class RenderStreaming {
/**
* @param signaling signaling class
* @param {RTCConfiguration} config
*/
constructor(signaling, config) { constructor(signaling, config) {
this._peer = null; this._peer = null; // participant端单一peer
this._peers = new Map(); // host端多peer Map (participantId → Peer)
this._connectionId = null; this._connectionId = null;
this.onConnect = function (connectionId) { Logger.log(`Connect peer on ${connectionId}.`); }; this._participantId = null; // 自己的participantId
this._isHost = false;
this.onConnect = function (connectionId, data) { Logger.log(`Connect peer on ${connectionId}.`); };
this.onDisconnect = function (connectionId) { Logger.log(`Disconnect peer on ${connectionId}.`); }; this.onDisconnect = function (connectionId) { Logger.log(`Disconnect peer on ${connectionId}.`); };
this.onGotOffer = function (connectionId) { Logger.log(`On got Offer on ${connectionId}.`); }; this.onGotOffer = function (connectionId) { Logger.log(`On got Offer on ${connectionId}.`); };
this.onGotAnswer = function (connectionId) { Logger.log(`On got Answer on ${connectionId}.`); }; this.onGotAnswer = function (connectionId) { Logger.log(`On got Answer on ${connectionId}.`); };
this.onTrackEvent = function (data) { Logger.log(`OnTrack event peer with data:${data}`); }; this.onTrackEvent = function (data) { Logger.log(`OnTrack event peer with data:${data}`); };
this.onAddChannel = function (data) { Logger.log(`onAddChannel event peer with data:${data}`); }; this.onAddChannel = function (data) { Logger.log(`onAddChannel event peer with data:${data}`); };
this.onMessage = function (data) { Logger.log(`On message: ${data}`); }; this.onMessage = function (data) { Logger.log(`On message: ${data}`); };
this.onParticipantLeft = function (connectionId) { Logger.log(`Participant left on ${connectionId}.`); }; this.onParticipantLeft = function (participantId) { Logger.log(`Participant left: ${participantId}.`); };
this.onNewPeer = function (connectionId) { Logger.log(`New peer created for ${connectionId}.`); }; this.onParticipantJoined = function (participantId) { Logger.log(`Participant joined: ${participantId}.`); };
this.onNewPeer = function (participantId) { Logger.log(`New peer created for ${participantId}.`); };
this._config = config; this._config = config;
this._signaling = signaling; this._signaling = signaling;
this._signaling.addEventListener('connect', this._onConnect.bind(this)); this._signaling.addEventListener('connect', this._onConnect.bind(this));
@@ -34,12 +34,21 @@ export class RenderStreaming {
this._signaling.addEventListener('candidate', this._onIceCandidate.bind(this)); this._signaling.addEventListener('candidate', this._onIceCandidate.bind(this));
this._signaling.addEventListener('on-message', this._onMessage.bind(this)); this._signaling.addEventListener('on-message', this._onMessage.bind(this));
this._signaling.addEventListener('participant-left', this._onParticipantLeft.bind(this)); this._signaling.addEventListener('participant-left', this._onParticipantLeft.bind(this));
this._signaling.addEventListener('participant-joined', this._onParticipantJoined.bind(this));
} }
async _onConnect(e) { async _onConnect(e) {
const data = e.detail; const data = e.detail;
if (this._connectionId == data.connectionId) { if (this._connectionId == data.connectionId) {
this._preparePeerConnection(this._connectionId, data.polite); this._participantId = data.participantId;
this._isHost = data.role === 'host';
if (!this._isHost) {
// participant端立即创建单一peer并开始协商
this._preparePeerConnection(this._connectionId, data.polite, null);
}
// host端不在connect时创建peer等participant加入后再创建
this.onConnect(data.connectionId, data); this.onConnect(data.connectionId, data);
} }
} }
@@ -52,50 +61,91 @@ export class RenderStreaming {
this._peer.close(); this._peer.close();
this._peer = null; this._peer = null;
} }
// 关闭所有host端peers
this._peers.forEach((peer, participantId) => {
peer.close();
});
this._peers.clear();
} }
} }
async _onOffer(e) { async _onOffer(e) {
const offer = e.detail; const offer = e.detail;
// 如果已有Peer但ICE连接已断开需要重建Peer const participantId = offer.participantId;
if (this._isHost) {
// host端为该participant创建或复用peer
let peer = this._peers.get(participantId);
if (!peer || (peer.pc && peer.pc.iceConnectionState === 'disconnected')) {
if (peer) peer.close();
peer = this._preparePeerConnection(this._connectionId, offer.polite, participantId);
}
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
try {
await peer.onGotDescription(this._connectionId, desc);
} catch (error) {
Logger.warn(`Error on GotDescription for participant ${participantId}: ${error}`);
}
} else {
// participant端使用单一peer
if (this._peer && this._peer.pc && this._peer.pc.iceConnectionState === 'disconnected') { if (this._peer && this._peer.pc && this._peer.pc.iceConnectionState === 'disconnected') {
Logger.log('ICE disconnected, resetting PeerConnection for new offer');
this._peer.close(); this._peer.close();
this._peer = null; this._peer = null;
} }
if (!this._peer) { if (!this._peer) {
this._preparePeerConnection(offer.connectionId, offer.polite); this._preparePeerConnection(offer.connectionId, offer.polite, null);
} }
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" }); const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
try { try {
await this._peer.onGotDescription(offer.connectionId, desc); await this._peer.onGotDescription(offer.connectionId, desc);
} catch (error) { } catch (error) {
Logger.warn(`Error happen on GotDescription that description.\n Message: ${error}\n RTCSdpType:${desc.type}\n sdp:${desc.sdp}`); Logger.warn(`Error on GotDescription: ${error}`);
return; }
} }
} }
async _onAnswer(e) { async _onAnswer(e) {
const answer = e.detail; const answer = e.detail;
const participantId = answer.participantId;
const desc = new RTCSessionDescription({ sdp: answer.sdp, type: "answer" }); const desc = new RTCSessionDescription({ sdp: answer.sdp, type: "answer" });
if (this._peer) {
if (this._isHost && participantId) {
// host端路由到对应participant的peer
const peer = this._peers.get(participantId);
if (peer) {
try {
await peer.onGotDescription(this._connectionId, desc);
} catch (error) {
Logger.warn(`Error on GotDescription answer for ${participantId}: ${error}`);
}
}
} else if (this._peer) {
// participant端
try { try {
await this._peer.onGotDescription(answer.connectionId, desc); await this._peer.onGotDescription(answer.connectionId, desc);
} catch (error) { } catch (error) {
Logger.warn(`Error happen on GotDescription that description.\n Message: ${error}\n RTCSdpType:${desc.type}\n sdp:${desc.sdp}`); Logger.warn(`Error on GotDescription answer: ${error}`);
return;
} }
} }
} }
async _onIceCandidate(e) { async _onIceCandidate(e) {
const candidate = e.detail; const candidate = e.detail;
const participantId = candidate.participantId;
const iceCandidate = new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex }); const iceCandidate = new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex });
if (this._peer) {
if (this._isHost && participantId) {
// host端路由到对应participant的peer
const peer = this._peers.get(participantId);
if (peer) {
await peer.onGotCandidate(this._connectionId, iceCandidate);
}
} else if (this._peer) {
// participant端
await this._peer.onGotCandidate(candidate.connectionId, iceCandidate); await this._peer.onGotCandidate(candidate.connectionId, iceCandidate);
} }
} }
// 在 RenderStreaming 类中添加
async _onMessage(e) { async _onMessage(e) {
const data = e.detail; const data = e.detail;
this.onMessage(data); this.onMessage(data);
@@ -103,13 +153,32 @@ export class RenderStreaming {
async _onParticipantLeft(e) { async _onParticipantLeft(e) {
const data = e.detail; const data = e.detail;
Logger.log(`Participant left: ${data.connectionId}`); const participantId = data.participantId;
this.onParticipantLeft(data.connectionId); Logger.log(`Participant left: ${participantId}`);
// 关闭该participant的peer
if (this._peers.has(participantId)) {
const peer = this._peers.get(participantId);
peer.close();
this._peers.delete(participantId);
} }
/**
* if not set argument, a generated uuid is used. this.onParticipantLeft(participantId);
* @param {string | null} connectionId }
*/
async _onParticipantJoined(e) {
const data = e.detail;
const participantId = data.participantId;
Logger.log(`Participant joined: ${participantId}`);
// host端为新participant创建peer
if (this._isHost && !this._peers.has(participantId)) {
this._preparePeerConnection(this._connectionId, false, participantId);
}
this.onParticipantJoined(participantId);
}
async createConnection(connectionId) { async createConnection(connectionId) {
this._connectionId = connectionId ? connectionId : uuid4(); this._connectionId = connectionId ? connectionId : uuid4();
await this._signaling.createConnection(this._connectionId); await this._signaling.createConnection(this._connectionId);
@@ -118,90 +187,106 @@ export class RenderStreaming {
async deleteConnection() { async deleteConnection() {
await this._signaling.deleteConnection(this._connectionId); await this._signaling.deleteConnection(this._connectionId);
} }
// 在 RenderStreaming 类中添加
_preparePeerConnection(connectionId, polite) { _preparePeerConnection(connectionId, polite, participantId) {
// host端多peer模式participantId标识目标participant
// participant端单peer模式participantId为null
const peer = new Peer(connectionId, polite, this._config);
// 保存peer
if (participantId) {
if (this._peers.has(participantId)) {
const oldPeer = this._peers.get(participantId);
oldPeer.close();
}
this._peers.set(participantId, peer);
} else {
if (this._peer) { if (this._peer) {
Logger.log('Close current PeerConnection');
this._peer.close(); this._peer.close();
this._peer = null; }
this._peer = peer;
} }
// Create peerConnection with proxy server and set up handlers // 事件处理附加participantId用于路由
this._peer = new Peer(connectionId, polite, this._config); peer.addEventListener('trackevent', (e) => {
// this._peer.addEventListener('disconnect', () => {
// this.onDisconnect(`Receive disconnect message from peer. connectionId:${connectionId}`);
// });
this._peer.addEventListener('trackevent', (e) => {
const data = e.detail; const data = e.detail;
data.participantId = participantId;
this.onTrackEvent(data); this.onTrackEvent(data);
}); });
this._peer.addEventListener('adddatachannel', (e) => {
peer.addEventListener('adddatachannel', (e) => {
const data = e.detail; const data = e.detail;
this.onAddChannel(data); this.onAddChannel(data);
}); });
this._peer.addEventListener('ongotoffer', (e) => {
peer.addEventListener('ongotoffer', (e) => {
const id = e.detail.connectionId; const id = e.detail.connectionId;
this.onGotOffer(id); this.onGotOffer(id);
}); });
this._peer.addEventListener('ongotanswer', (e) => {
peer.addEventListener('ongotanswer', (e) => {
const id = e.detail.connectionId; const id = e.detail.connectionId;
this.onGotAnswer(id); this.onGotAnswer(id);
}); });
this._peer.addEventListener('sendoffer', (e) => {
peer.addEventListener('sendoffer', (e) => {
const offer = e.detail; const offer = e.detail;
this._signaling.sendOffer(offer.connectionId, offer.sdp); this._signaling.sendOffer(offer.connectionId, offer.sdp, participantId);
}); });
this._peer.addEventListener('sendanswer', (e) => {
peer.addEventListener('sendanswer', (e) => {
const answer = e.detail; const answer = e.detail;
this._signaling.sendAnswer(answer.connectionId, answer.sdp); this._signaling.sendAnswer(answer.connectionId, answer.sdp, participantId);
}); });
this._peer.addEventListener('sendcandidate', (e) => {
peer.addEventListener('sendcandidate', (e) => {
const candidate = e.detail; const candidate = e.detail;
this._signaling.sendCandidate(candidate.connectionId, candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex); this._signaling.sendCandidate(candidate.connectionId, candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex, participantId);
}); });
this.onNewPeer(connectionId);
return this._peer; this.onNewPeer(participantId || connectionId);
return peer;
} }
/** async getStats(participantId) {
* @returns {Promise<RTCStatsReport> | null} if (this._isHost && participantId) {
*/ const peer = this._peers.get(participantId);
async getStats() { return peer ? await peer.getStats(this._connectionId) : null;
return await this._peer.getStats(this._connectionId); }
return this._peer ? await this._peer.getStats(this._connectionId) : null;
} }
/** createDataChannel(label, participantId) {
* @param {string} label if (this._isHost && participantId) {
* @returns {RTCDataChannel | null} const peer = this._peers.get(participantId);
*/ return peer ? peer.createDataChannel(this._connectionId, label) : null;
createDataChannel(label) { }
return this._peer.createDataChannel(this._connectionId, label); return this._peer ? this._peer.createDataChannel(this._connectionId, label) : null;
} }
/** addTrack(track, participantId) {
* @param {MediaStreamTrack} track if (this._isHost && participantId) {
* @returns {RTCRtpSender | null} const peer = this._peers.get(participantId);
*/ return peer ? peer.addTrack(this._connectionId, track) : null;
addTrack(track) { }
return this._peer.addTrack(this._connectionId, track); return this._peer ? this._peer.addTrack(this._connectionId, track) : null;
} }
/** addTransceiver(trackOrKind, init, participantId) {
* @param {MediaStreamTrack | string} trackOrKind if (this._isHost && participantId) {
* @param {RTCRtpTransceiverInit | null} init const peer = this._peers.get(participantId);
* @returns {RTCRtpTransceiver | null} return peer ? peer.addTransceiver(this._connectionId, trackOrKind, init) : null;
*/ }
addTransceiver(trackOrKind, init) { return this._peer ? this._peer.addTransceiver(this._connectionId, trackOrKind, init) : null;
return this._peer.addTransceiver(this._connectionId, trackOrKind, init);
} }
getTransceivers(participantId) {
/** if (this._isHost && participantId) {
* @returns {RTCRtpTransceiver[] | null} const peer = this._peers.get(participantId);
*/ return peer ? peer.getTransceivers(this._connectionId) : null;
getTransceivers() { }
return this._peer.getTransceivers(this._connectionId); return this._peer ? this._peer.getTransceivers(this._connectionId) : null;
} }
sendMessage(message) { sendMessage(message) {
@@ -209,6 +294,7 @@ export class RenderStreaming {
this._signaling.sendMessage(this._connectionId, message); this._signaling.sendMessage(this._connectionId, message);
} }
} }
async start() { async start() {
await this._signaling.start(); await this._signaling.start();
} }
@@ -218,6 +304,10 @@ export class RenderStreaming {
this._peer.close(); this._peer.close();
this._peer = null; this._peer = null;
} }
this._peers.forEach((peer) => {
peer.close();
});
this._peers.clear();
if (this._signaling) { if (this._signaling) {
await this._signaling.stop(); await this._signaling.stop();

View File

@@ -182,13 +182,13 @@ export class WebSocketSignaling extends EventTarget {
this.dispatchEvent(new CustomEvent('disconnect', { detail: msg })); this.dispatchEvent(new CustomEvent('disconnect', { detail: msg }));
break; break;
case "offer": case "offer":
this.dispatchEvent(new CustomEvent('offer', { detail: { connectionId: msg.from, sdp: msg.data.sdp, polite: msg.data.polite } })); this.dispatchEvent(new CustomEvent('offer', { detail: { connectionId: msg.from, sdp: msg.data.sdp, polite: msg.data.polite, participantId: msg.participantId } }));
break; break;
case "answer": case "answer":
this.dispatchEvent(new CustomEvent('answer', { detail: { connectionId: msg.from, sdp: msg.data.sdp } })); this.dispatchEvent(new CustomEvent('answer', { detail: { connectionId: msg.from, sdp: msg.data.sdp, participantId: msg.participantId } }));
break; break;
case "candidate": case "candidate":
this.dispatchEvent(new CustomEvent('candidate', { detail: { connectionId: msg.from, candidate: msg.data.candidate, sdpMLineIndex: msg.data.sdpMLineIndex, sdpMid: msg.data.sdpMid } })); this.dispatchEvent(new CustomEvent('candidate', { detail: { connectionId: msg.from, candidate: msg.data.candidate, sdpMLineIndex: msg.data.sdpMLineIndex, sdpMid: msg.data.sdpMid, participantId: msg.participantId } }));
break; break;
case "on-message": case "on-message":
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.data })); this.dispatchEvent(new CustomEvent('on-message', { detail: msg.data }));
@@ -196,8 +196,10 @@ export class WebSocketSignaling extends EventTarget {
case "participant-left": case "participant-left":
this.dispatchEvent(new CustomEvent('participant-left', { detail: msg })); this.dispatchEvent(new CustomEvent('participant-left', { detail: msg }));
break; break;
case "participant-joined":
this.dispatchEvent(new CustomEvent('participant-joined', { detail: msg }));
break;
case "broadcast": case "broadcast":
// 处理服务器广播的消息
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.message })); this.dispatchEvent(new CustomEvent('on-message', { detail: msg.message }));
break; break;
default: default:
@@ -231,28 +233,28 @@ export class WebSocketSignaling extends EventTarget {
this.websocket.send(sendJson); this.websocket.send(sendJson);
} }
sendOffer(connectionId, sdp) { sendOffer(connectionId, sdp, participantId) {
const data = { 'sdp': sdp, 'connectionId': connectionId }; const data = { 'sdp': sdp, 'connectionId': connectionId };
const sendJson = JSON.stringify({ type: "offer", from: connectionId, data: data }); const sendJson = JSON.stringify({ type: "offer", from: connectionId, data: data, participantId: participantId || '' });
Logger.log(sendJson); Logger.log(sendJson);
this.websocket.send(sendJson); this.websocket.send(sendJson);
} }
sendAnswer(connectionId, sdp) { sendAnswer(connectionId, sdp, participantId) {
const data = { 'sdp': sdp, 'connectionId': connectionId }; const data = { 'sdp': sdp, 'connectionId': connectionId };
const sendJson = JSON.stringify({ type: "answer", from: connectionId, data: data }); const sendJson = JSON.stringify({ type: "answer", from: connectionId, data: data, participantId: participantId || '' });
Logger.log(sendJson); Logger.log(sendJson);
this.websocket.send(sendJson); this.websocket.send(sendJson);
} }
sendCandidate(connectionId, candidate, sdpMLineIndex, sdpMid) { sendCandidate(connectionId, candidate, sdpMLineIndex, sdpMid, participantId) {
const data = { const data = {
'candidate': candidate, 'candidate': candidate,
'sdpMLineIndex': sdpMLineIndex, 'sdpMLineIndex': sdpMLineIndex,
'sdpMid': sdpMid, 'sdpMid': sdpMid,
'connectionId': connectionId 'connectionId': connectionId
}; };
const sendJson = JSON.stringify({ type: "candidate", from: connectionId, data: data }); const sendJson = JSON.stringify({ type: "candidate", from: connectionId, data: data, participantId: participantId || '' });
Logger.log(sendJson); Logger.log(sendJson);
this.websocket.send(sendJson); this.websocket.send(sendJson);
} }

View File

@@ -112,32 +112,26 @@ function broadcastToGroup(connectionId: string, senderWs: WebSocket, message: an
* @param ws WebSocket连接实例 * @param ws WebSocket连接实例
*/ */
function remove(ws: WebSocket): void { function remove(ws: WebSocket): void {
// 获取连接的所有连接ID
const connectionIds = clients.get(ws); const connectionIds = clients.get(ws);
if (!connectionIds) return; if (!connectionIds) return;
// 遍历所有连接ID
connectionIds.forEach(connectionId => { connectionIds.forEach(connectionId => {
const group = connectionGroup.get(connectionId); const group = connectionGroup.get(connectionId);
if (group) { if (group) {
if (group.host === ws) { if (group.host === ws) {
// host断开连接通知所有participants房间已关闭
group.participants.forEach(participantWs => { group.participants.forEach(participantWs => {
participantWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId, reason: "host-left" })); participantWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId, reason: "host-left" }));
}); });
// 删除整个连接组
connectionGroup.delete(connectionId); connectionGroup.delete(connectionId);
} else { } else {
// participant断开连接从participants中移除并通知host
group.participants.delete(ws); group.participants.delete(ws);
group.host.send(JSON.stringify({ type: "participant-left", connectionId: connectionId })); // 包含participantId让host能识别是哪个participant离开
group.host.send(JSON.stringify({ type: "participant-left", connectionId: connectionId, participantId: (ws as any).participantId }));
} }
} }
// 记录删除连接ID的日志
console.log(`Remove connectionId: ${connectionId}`); console.log(`Remove connectionId: ${connectionId}`);
}); });
// 从客户端映射中删除
clients.delete(ws); clients.delete(ws);
} }
@@ -149,30 +143,27 @@ function remove(ws: WebSocket): void {
*/ */
function onConnect(ws: WebSocket, connectionId: string): void { function onConnect(ws: WebSocket, connectionId: string): void {
let polite = true; let polite = true;
// 处理私有模式 // 为每个WebSocket生成唯一的participantId
const participantId = (ws as any).participantId = (ws as any).participantId || `p_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
if (isPrivate) { if (isPrivate) {
if (connectionGroup.has(connectionId)) { if (connectionGroup.has(connectionId)) {
const group = connectionGroup.get(connectionId); const group = connectionGroup.get(connectionId);
// 已有host新连接作为participant加入
group.participants.add(ws); group.participants.add(ws);
console.log(`Participant joined connectionId: ${connectionId}, total participants: ${group.participants.size}`); console.log(`Participant ${participantId} joined connectionId: ${connectionId}, total participants: ${group.participants.size}`);
// 通知host有新participant加入
group.host.send(JSON.stringify({ type: "participant-joined", connectionId: connectionId, participantId: participantId }));
} else { } else {
// 第一个连接成为host
connectionGroup.set(connectionId, { host: ws, participants: new Set<WebSocket>() }); connectionGroup.set(connectionId, { host: ws, participants: new Set<WebSocket>() });
polite = false; polite = false;
console.log(`Host created connectionId: ${connectionId}`); console.log(`Host created connectionId: ${connectionId}`);
} }
} }
// 获取或创建连接ID集合
const connectionIds = getOrCreateConnectionIds(ws); const connectionIds = getOrCreateConnectionIds(ws);
// 添加连接ID
connectionIds.add(connectionId); connectionIds.add(connectionId);
// 发送连接成功消息(包含角色信息)
const role = polite ? 'participant' : 'host'; const role = polite ? 'participant' : 'host';
ws.send(JSON.stringify({ type: "connect", connectionId: connectionId, polite: polite, role: role })); ws.send(JSON.stringify({ type: "connect", connectionId: connectionId, polite: polite, role: role, participantId: participantId }));
//启用心跳包
//AddHeartbeat(ws, connectionId);
} }
/** /**
@@ -220,25 +211,35 @@ function onDisconnect(ws: WebSocket, connectionId: string): void {
* @param message 消息数据 * @param message 消息数据
*/ */
function onOffer(ws: WebSocket, message: any): void { function onOffer(ws: WebSocket, message: any): void {
// 获取连接ID
const connectionId = message.connectionId as string; const connectionId = message.connectionId as string;
// 创建新的offer
const newOffer = new Offer(message.sdp, Date.now(), false); const newOffer = new Offer(message.sdp, Date.now(), false);
// 处理私有模式
if (isPrivate) { if (isPrivate) {
if (connectionGroup.has(connectionId)) { if (connectionGroup.has(connectionId)) {
const group = connectionGroup.get(connectionId); const group = connectionGroup.get(connectionId);
const senderParticipantId = (ws as any).participantId;
const targetParticipantId = message.participantId;
if (group.host === ws) { if (group.host === ws) {
// host发送offer,转发给所有participants // host发送offer给特定participant多peer模式下按participantId路由
newOffer.polite = true; newOffer.polite = true;
if (targetParticipantId) {
// 路由到指定participant
group.participants.forEach(participantWs => { group.participants.forEach(participantWs => {
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer })); if ((participantWs as any).participantId === targetParticipantId) {
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer, participantId: targetParticipantId }));
}
}); });
} else { } else {
// participant发送offer转发给host // 兼容无目标时广播给所有participants
group.participants.forEach(participantWs => {
const pid = (participantWs as any).participantId;
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer, participantId: pid }));
});
}
} else {
// participant发送offer给host携带该participant的participantId
newOffer.polite = true; newOffer.polite = true;
group.host.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer })); group.host.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer, participantId: senderParticipantId }));
} }
} }
return; return;
@@ -264,30 +265,37 @@ function onOffer(ws: WebSocket, message: any): void {
* @param message 消息数据 * @param message 消息数据
*/ */
function onAnswer(ws: WebSocket, message: any): void { function onAnswer(ws: WebSocket, message: any): void {
// 获取连接ID
const connectionId = message.connectionId as string; const connectionId = message.connectionId as string;
// 获取或创建连接ID集合
const connectionIds = getOrCreateConnectionIds(ws); const connectionIds = getOrCreateConnectionIds(ws);
// 添加连接ID
connectionIds.add(connectionId); connectionIds.add(connectionId);
// 创建新的answer
const newAnswer = new Answer(message.sdp, Date.now()); const newAnswer = new Answer(message.sdp, Date.now());
// 检查连接组是否存在
if (!connectionGroup.has(connectionId)) { if (!connectionGroup.has(connectionId)) {
return; return;
} }
const group = connectionGroup.get(connectionId); const group = connectionGroup.get(connectionId);
const senderParticipantId = (ws as any).participantId;
// 从answer消息中获取目标participantIdhost回复时指定
const targetParticipantId = message.participantId;
if (group.host === ws) { if (group.host === ws) {
// host发送answer,转发给所有participants通常host不发送answer // host发送answer给特定participant
if (targetParticipantId) {
group.participants.forEach(participantWs => {
if ((participantWs as any).participantId === targetParticipantId) {
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer, participantId: targetParticipantId }));
}
});
} else {
// 兼容没有targetParticipantId时广播给所有participants
group.participants.forEach(participantWs => { group.participants.forEach(participantWs => {
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer })); participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer }));
}); });
}
} else { } else {
// participant发送answer,转发给host // participant发送answer给host携带自己的participantId
group.host.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer })); group.host.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer, participantId: senderParticipantId }));
} }
} }
@@ -298,28 +306,36 @@ function onAnswer(ws: WebSocket, message: any): void {
* @param message 消息数据 * @param message 消息数据
*/ */
function onCandidate(ws: WebSocket, message: any): void { function onCandidate(ws: WebSocket, message: any): void {
// 获取连接ID
const connectionId = message.connectionId; const connectionId = message.connectionId;
// 创建新的candidate
const candidate = new Candidate(message.candidate, message.sdpMLineIndex, message.sdpMid, Date.now()); const candidate = new Candidate(message.candidate, message.sdpMLineIndex, message.sdpMid, Date.now());
const senderParticipantId = (ws as any).participantId;
const targetParticipantId = message.participantId;
// 处理私有模式
if (isPrivate) { if (isPrivate) {
if (connectionGroup.has(connectionId)) { if (connectionGroup.has(connectionId)) {
const group = connectionGroup.get(connectionId); const group = connectionGroup.get(connectionId);
if (group.host === ws) { if (group.host === ws) {
// host发送candidate,转发给所有participants // host发送candidate给特定participant
if (targetParticipantId) {
group.participants.forEach(participantWs => {
if ((participantWs as any).participantId === targetParticipantId) {
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate, participantId: targetParticipantId }));
}
});
} else {
group.participants.forEach(participantWs => { group.participants.forEach(participantWs => {
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate })); participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
}); });
}
} else { } else {
// participant发送candidate,转发给host // participant发送candidate给host携带自己的participantId
group.host.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate })); group.host.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate, participantId: senderParticipantId }));
} }
} }
return; return;
} }
} }
function onCallConnectionId(ws: WebSocket, message: any): void { function onCallConnectionId(ws: WebSocket, message: any): void {
// 获取连接ID // 获取连接ID
const connectionId = message.connectionId; const connectionId = message.connectionId;
@@ -383,6 +399,7 @@ function onCandidate(ws: WebSocket, message: any): void {
}); });
} }
} }
function AddHeartbeat(ws: WebSocket, connectionId: string) { function AddHeartbeat(ws: WebSocket, connectionId: string) {
// 初始化心跳检测 // 初始化心跳检测
(ws as any).lastActivity = Date.now(); (ws as any).lastActivity = Date.now();
@@ -403,12 +420,14 @@ function onCandidate(ws: WebSocket, message: any): void {
} }
}, 3000); }, 3000);
} }
function RemoveHeartbeat(ws: WebSocket) { function RemoveHeartbeat(ws: WebSocket) {
// 清除心跳检测定时器 // 清除心跳检测定时器
if ((ws as any).heartbeatTimer) { if ((ws as any).heartbeatTimer) {
clearInterval((ws as any).heartbeatTimer); clearInterval((ws as any).heartbeatTimer);
} }
} }
/** /**
* 处理获取所有连接ID的请求 * 处理获取所有连接ID的请求
* @param ws WebSocket连接实例 * @param ws WebSocket连接实例
@@ -418,6 +437,7 @@ function onCandidate(ws: WebSocket, message: any): void {
const connectionIds = Array.from(connectionGroup.keys()); const connectionIds = Array.from(connectionGroup.keys());
return connectionIds; return connectionIds;
} }
/** /**
* 处理chat-message信令1对多模式 * 处理chat-message信令1对多模式
* host的消息转发给所有participantsparticipant的消息转发给host * host的消息转发给所有participantsparticipant的消息转发给host
@@ -441,6 +461,7 @@ function onCandidate(ws: WebSocket, message: any): void {
} }
} }
} }
/** /**
* 导出WebSocket处理器函数 * 导出WebSocket处理器函数
*/ */