【m】增加多用户连接

This commit is contained in:
2026-04-23 15:22:24 +08:00
parent cb32582c98
commit 852a169c30
8 changed files with 797 additions and 155 deletions

View File

@@ -174,6 +174,10 @@ class UIRenderer {
// 通话结束 - 渲染通话结束界面
this.renderCallEnded();
break;
case 'PARTICIPANT_LEFT':
// participant离开 - 更新UI但房间仍然存在
this.renderParticipantLeft(changes.connectionId);
break;
}
}
@@ -892,6 +896,19 @@ class UIRenderer {
window.location.href = './endcall/endcall.html';
}
// 渲染participant离开host端房间仍然存在
renderParticipantLeft(connectionId) {
console.log(`Participant left: ${connectionId}, updating UI`);
// 更新远程用户状态显示为离线
if (this.elements.remoteNetworkIndicator) {
this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full';
}
// 显示断开连接的遮罩层(如果存在)
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.remove('hidden');
}
}
// 获取状态文本
getStatusText(status) {
const statusMap = {

View File

@@ -8,15 +8,9 @@ import { RenderStreaming } from "../../module/renderstreaming.js"; // WebRTC连
import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务器配置和RTC配置
import { showNotification, generateId } from './utils.js'; // 导入通知函数
import chatMessage from './chatmessage.js';
// 默认视频流尺寸
const defaultStreamWidth = 1280;
const defaultStreamHeight = 720;
class CallStateManager {
constructor() {
const renderstreaming = null; // WebRTC连接管理实例
const useWebSocket = null; // 是否使用WebSocket信令
const connectionId = null; // 连接ID
// 核心状态
this.state = {
id: generateId(),
@@ -332,9 +326,23 @@ class CallStateManager {
]
};
this.renderstreaming = new RenderStreaming(signaling, config); // 创建WebRTC连接管理实例
this.renderstreaming.onNewPeer = (connectionId) => {
console.log(`New peer created for ${connectionId}, adding local tracks`);
if (this.state.localStream) {
const tracks = this.state.localStream.getTracks();
for (const track of tracks) {
this.renderstreaming.addTransceiver(track, { direction: 'sendonly' });
}
this.setCodecPreferences();
}
};
// 连接建立回调
this.renderstreaming.onConnect = () => {
this.renderstreaming.onConnect = (connectionId, data) => {
// 保存角色信息host/participant
if (data && data.role) {
this.role = data.role;
console.log(`Connected as ${this.role}`);
}
// 连接建立后更新状态为ongoing
this.state.session.status = 'ongoing';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' });
@@ -347,21 +355,39 @@ class CallStateManager {
});
if (this.state.localStream) {
const tracks = this.state.localStream.getTracks(); // 获取本地媒体轨道
for (const track of tracks) {
this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }); // 添加发送轨道
}
this.setCodecPreferences(); // 设置编解码器偏好
this.showStatsMessage(); // 显示统计信息
// const tracks = this.state.localStream.getTracks();
// for (const track of tracks) {
// this.renderstreaming.addTransceiver(track, { direction: 'sendonly' });
// }
// this.setCodecPreferences();
this.showStatsMessage();
} else {
console.error('Local stream is not available');
showNotification('本地视频流不可用', 'error');
}
};
// 连接断开回调
// 连接断开回调(收到服务器的 disconnect 消息,通常是 host 离开导致房间关闭)
this.renderstreaming.onDisconnect = () => {
this.hangUp(); // 挂断连接
console.log('Received disconnect from server, host left or room closed');
this.hangUp(); // 房间已关闭,挂断连接
};
// participant离开回调host收到房间仍然存在
this.renderstreaming.onParticipantLeft = (connectionId) => {
console.log(`Participant left: ${connectionId}, room still active`);
// 更新远程用户状态,但不关闭房间
this.updateRemoteUserStatus('offline');
this.updateRemoteUserNetworkQuality('no_signal');
showNotification('对方已离开通话', 'warning');
// 清理远端流重置Peer连接为新participant加入做准备
if (this.state.remoteStream) {
this.state.remoteStream.getTracks().forEach(track => track.stop());
this.state.remoteStream = null;
}
this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: null });
// 通知UI更新
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: connectionId });
};
// 轨道事件回调
@@ -473,6 +499,8 @@ class CallStateManager {
/**
* 挂断WebRTC连接
* Host挂断房间删除通知所有participants
* Participant挂断仅自己离开房间保留
* @async
* @returns {Promise<void>}
*/
@@ -484,11 +512,17 @@ class CallStateManager {
clearInterval(this.durationInterval);
this.durationInterval = null;
}
console.log(`Disconnect peer on ${this.connectionId}.`);
const isHost = this.role === 'host';
console.log(`Disconnect peer on ${this.connectionId}. Role: ${this.role}`);
// 删除连接并停止WebRTC
if (this.renderstreaming) {
try {
// 发送断开连接信令给服务器
// 服务器会根据角色决定:
// - host断开通知所有participants删除房间
// - participant断开仅通知host保留房间
await this.renderstreaming.deleteConnection();
await this.renderstreaming.stop();
} catch (error) {
@@ -501,8 +535,9 @@ class CallStateManager {
this.updateRemoteUserStatus('offline');
this.updateRemoteUserNetworkQuality('no_signal');
this.connectionId = null;
this.role = null;
this.state.session.status = 'ended';
this.notify({ type: 'CALL_ENDED' });
this.notify({ type: 'CALL_ENDED', reason: isHost ? 'host_hangup' : 'participant_hangup' });
}
/**
@@ -570,18 +605,14 @@ class CallStateManager {
this.state.session.remoteUser.networkQuality = networkQuality;
this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
}
// 结束通话
endCall() {
if (this.durationInterval) {
clearInterval(this.durationInterval);
this.durationInterval = null;
}
this.state.session.status = 'ended';
this.notify({ type: 'CALL_ENDED' });
// 发送结束通话请求到服务器
// [API_CALL: POST /api/call/:callId/leave]
// [WEBSOCKET_EMIT: leave-call]
// 结束通话(用户主动点击挂断按钮)
async endCall() {
console.log(`endCall called. Role: ${this.role}`);
// 调用 hangUp() 正确关闭 WebRTC 连接并发送断开信令
// hangUp 内部会根据角色区分:
// - host: 通知所有participants删除房间
// - participant: 仅自己离开,房间保留
await this.hangUp();
}
// 加入通话

View File

@@ -42,7 +42,7 @@ export default class Peer extends EventTarget {
this.pc.oniceconnectionstatechange = () => {
_this.log(`iceConnectionState changed:${_this.pc.iceConnectionState}`);
if (_this.pc.iceConnectionState === 'disconnected') {
if (_this.pc.iceConnectionState === 'failed') {
this.dispatchEvent(new Event('disconnect'));
}
};

View File

@@ -23,6 +23,8 @@ export class RenderStreaming {
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.onMessage = function (data) { Logger.log(`On message: ${data}`); };
this.onParticipantLeft = function (connectionId) { Logger.log(`Participant left on ${connectionId}.`); };
this.onNewPeer = function (connectionId) { Logger.log(`New peer created for ${connectionId}.`); };
this._config = config;
this._signaling = signaling;
this._signaling.addEventListener('connect', this._onConnect.bind(this));
@@ -31,13 +33,14 @@ export class RenderStreaming {
this._signaling.addEventListener('answer', this._onAnswer.bind(this));
this._signaling.addEventListener('candidate', this._onIceCandidate.bind(this));
this._signaling.addEventListener('on-message', this._onMessage.bind(this));
this._signaling.addEventListener('participant-left', this._onParticipantLeft.bind(this));
}
async _onConnect(e) {
const data = e.detail;
if (this._connectionId == data.connectionId) {
this._preparePeerConnection(this._connectionId, data.polite);
this.onConnect(data.connectionId);
this.onConnect(data.connectionId, data);
}
}
@@ -54,6 +57,12 @@ export class RenderStreaming {
async _onOffer(e) {
const offer = e.detail;
// 如果已有Peer但ICE连接已断开需要重建Peer
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 = null;
}
if (!this._peer) {
this._preparePeerConnection(offer.connectionId, offer.polite);
}
@@ -91,6 +100,12 @@ export class RenderStreaming {
const data = e.detail;
this.onMessage(data);
}
async _onParticipantLeft(e) {
const data = e.detail;
Logger.log(`Participant left: ${data.connectionId}`);
this.onParticipantLeft(data.connectionId);
}
/**
* if not set argument, a generated uuid is used.
* @param {string | null} connectionId
@@ -114,9 +129,9 @@ export class RenderStreaming {
// Create peerConnection with proxy server and set up handlers
this._peer = new Peer(connectionId, polite, this._config);
this._peer.addEventListener('disconnect', () => {
this.onDisconnect(`Receive disconnect message from peer. connectionId:${connectionId}`);
});
// this._peer.addEventListener('disconnect', () => {
// this.onDisconnect(`Receive disconnect message from peer. connectionId:${connectionId}`);
// });
this._peer.addEventListener('trackevent', (e) => {
const data = e.detail;
this.onTrackEvent(data);
@@ -145,6 +160,7 @@ export class RenderStreaming {
const candidate = e.detail;
this._signaling.sendCandidate(candidate.connectionId, candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex);
});
this.onNewPeer(connectionId);
return this._peer;
}

View File

@@ -193,6 +193,9 @@ export class WebSocketSignaling extends EventTarget {
case "on-message":
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.data }));
break;
case "participant-left":
this.dispatchEvent(new CustomEvent('participant-left', { detail: msg }));
break;
case "broadcast":
// 处理服务器广播的消息
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.message }));