/** * 【发起方】 1. 创建 PeerConnection 2. 采集本地流并添加到 PeerConnection 3. 调用 createOffer() → 生成Offer 4. 调用 setLocalDescription(offer) → 开始收集ICE候选 5. 通过信令发送Offer给接收方 6. 监听 onicecandidate → 发送ICE候选给接收方 7. 接收接收方的Answer → 调用 setRemoteDescription(answer) 8. 接收接收方的ICE候选 → 调用 addIceCandidate(candidate) 【接收方】 1. 创建 PeerConnection 2. 接收发起方的Offer → 调用 setRemoteDescription(offer) 3. 调用 createAnswer() → 生成Answer 4. 调用 setLocalDescription(answer) → 开始收集ICE候选 5. 通过信令发送Answer给发起方 6. 监听 onicecandidate → 发送ICE候选给发起方 7. 接收发起方的ICE候选 → 调用 addIceCandidate(candidate) */ /** * WebRTC 核心封装类(ES6 模块化) * 支持音视频互通、自定义数据传输、外部流管理 * 开箱即用,无需额外依赖 */ export class WebRTC_ModuleSimple { /** * 构造函数 * @param {Object} options 初始化配置 * @param {HTMLElement} options.localVideo 本地视频播放元素 * @param {HTMLElement} options.remoteVideo 远端视频播放元素 * @param {string} options.dataChannelLabel 数据通道标签(默认 'chat') * @param {Function} receivepeerCallback 回调函数 */ constructor(options = {}) { this.current_time = new Date(); // 基础配置 this.config = { localVideo: options.localVideo, remoteVideo: options.remoteVideo, dataChannelLabel: options.dataChannelLabel || "chat", iceServers: [ { urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun.cloudflare.com:3478" }, ], }; // WebRTC 核心对象 this.peerConnection = null; this.dataChannel = null; // 媒体流管理 this.localStream = null; // 本地流(可外部传入/获取) this.remoteStream = null; // 远端流(可外部获取) // 事件回调(外部可自定义) this.callbacks = { onIceCandidate: (candidate) => {}, // ICE 候选生成 onConnectSuccess: () => {}, // 连接成功 onConnectFailed: () => {}, // 连接失败 onRemoteStream: (stream) => {}, // 收到远端流 onMessage: (data) => {}, // 收到自定义消息 onClose: () => {}, // 连接关闭 onState: (state) => {}, }; // 初始化 PeerConnection this._initPeerConnection(); } /** * 初始化 RTCPeerConnection 核心对象 * @private */ _initPeerConnection() { try { // 销毁旧实例 if (this.peerConnection) { this.peerConnection.close(); } // 浏览器兼容处理 const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; this.peerConnection = new RTCPeerConnection({ iceServers: this.config.iceServers, }); // 监听信令协商状态 this.signalingStateListener(); // 监听远程信息 this.peerConnectionLisioner(); } catch (error) { console.error("WebRTC 初始化失败:", error); this.callbacks.onConnectFailed(); } } // 监听信令状态变化(Offer/Answer/Ice 协商) signalingStateListener() { // 初始状态打印 console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}初始信令状态:`, this.peerConnection.signalingState, ); // 监听信令状态变化(Offer/Answer 协商) this.peerConnection.onsignalingstatechange = () => { /** * 信令状态枚举值(全阶段) * new 初始状态 * have-local-offer 本地已设置 Offer 发起者:已调用 createOffer() + setLocalDescription(offer) * have-remote-offer 远端已设置 Offer 应答者:已调用 setRemoteDescription(offer) * have-local-answer 本地已设置 Answer 应答者:已调用 createAnswer() + setLocalDescription(answer) * have-remote-answer 远端已设置 Answer 发起者:已调用 setRemoteDescription(answer) * stable 协商完成,稳定状态 * closed PeerConnection 已关闭 */ const state = this.peerConnection.signalingState; console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}初始信令变更:`, state, ); state && this.callbacks.onState(state); // 根据状态执行不同逻辑 switch (state) { case "new": console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}【状态】未开始协商,可创建 Offer`, ); break; case "have-local-offer": console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}【状态】发起者已发 Offer,等待应答者 Answer`, ); // 可触发 ICE 候选者发送逻辑 break; case "have-remote-offer": console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}【状态】应答者已收 Offer,可创建 Answer`, ); break; case "stable": console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}【状态】Offer/Answer 协商完成,进入 ICE 配对阶段`, ); break; case "closed": console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}【状态】PeerConnection 已关闭,协商终止`, ); break; default: console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}【状态】协商中:`, state, ); } }; // ICE 候选者收集 / 配对 / 连接 this.peerConnection.onicegatheringstatechange = () => { /** * ICE 连接状态(iceConnectionState) * new 初始状态,未开始连接 * checking 正在检查候选者配对 * connected 已建立连接(P2P 成功) * completed 连接完成(所有候选者检查完毕) * failed ICE 协商失败 * disconnected 连接断开(临时) * closed PeerConnection 已关闭 */ const state = this.peerConnection.iceGatheringState; console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}ICE 收集状态变更:`, state, ); state && this.callbacks.onState(state); switch (state) { case "gathering": console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}【ICE】开始收集候选者(host/srflx/relay)`, state, ); break; case "complete": console.warn( `[webRTC]${this.current_time.toLocaleTimeString()}【ICE】候选者收集完成`, state, ); break; } }; } peerConnectionLisioner() { if (!this.peerConnection) { return; } // 监听远端媒体流 this.peerConnection.ontrack = (e) => { this.remoteStream = e.streams[0]; // 绑定远端视频 if (this.config.remoteVideo) { this.config.remoteVideo.srcObject = this.remoteStream; this.config.remoteVideo.play().catch(() => {}); } this.callbacks.onRemoteStream(this.remoteStream); }; // 监听peer连接状态 this.peerConnection.onconnectionstatechange = () => { const state = this.peerConnection.connectionState; if (state === "connected") { this.callbacks.onConnectSuccess(); } else if (state === "failed") { this.callbacks.onConnectFailed(); } else if (state === "closed") { this.callbacks.onClose(); } }; // 监听远端创建的数据通道 this.peerConnection.ondatachannel = (e) => { this.dataChannel = e.channel; this.dataChannelLisioner(); }; // 监听 ICE 候选(需通过信令服务器发给对方) this.peerConnection.onicecandidate = (e) => { e.candidate && this.callbacks.onIceCandidate(e.candidate); }; // 监听 ICE 连接状态,排查失败原因 this.peerConnection.oniceconnectionstatechange = () => { const state = this.peerConnection.iceConnectionState; console.info("ICE 连接状态:", state); // 捕获 ICE failed 状态,给出明确提示 if (state === "failed") { console.error("ICE 协商失败!请检查 TURN 服务器配置或网络环境"); // 可选:触发重新协商 this.createOffer(); } }; // 新监听 ICE 候选者生成错误 this.peerConnection.onicecandidateerror = (event) => { console.error( "ICE 候选者生成失败:", event.errorText, "错误码:", event.errorCode, ); }; } /** * 初始化数据通道(自定义消息传输) * @private */ dataChannelLisioner() { if (!this.dataChannel) return; // 数据通道打开 this.dataChannel.onopen = () => console.info("数据通道已就绪"); // 接收自定义消息(自动解析JSON) this.dataChannel.onmessage = (e) => { let data = e.data; try { data = JSON.parse(data); } catch (err) {} this.callbacks.onMessage(data); this.dataChannelSendMsg(data); }; // 数据通道错误 this.dataChannel.onerror = (error) => console.error("数据通道异常:", error); // 数据通道关闭 this.dataChannel.onclose = () => console.info("数据通道关闭"); } /** * 采集本地音视频流(一键调用) * @param {Object} constraints 媒体约束(默认开启音视频) * @returns {Promise} 本地媒体流 */ async captureLocalStream(constraints = { video: true, audio: true }) { try { // getUserMedia 设备硬件:摄像头(视频)、麦克风(音频) // getDisplayMedia 屏幕 / 窗口 / 标签页:显示器内容(视频),可选采集系统音频 this.localStream = await navigator.mediaDevices.getDisplayMedia(constraints); // 绑定本地视频 if (this.config.localVideo) { this.config.localVideo.srcObject = this.localStream; this.config.localVideo.muted = true; // 静音避免回声 this.config.localVideo.play().catch(() => {}); } // 将流添加到连接 this.localStream.getTracks().forEach((track) => { this.peerConnection.addTrack(track, this.localStream); }); return this.localStream; } catch (error) { console.error("采集本地流失败:", error); throw new Error(`媒体权限不足或设备不可用: ${error.message}`); } } /** * 外部传入本地媒体流(支持屏幕共享等自定义流) * @param {MediaStream} stream 本地媒体流 */ setLocalStream(stream) { if (!stream) throw new Error("媒体流不能为空"); this.localStream = stream; // 绑定视频(可选) if (this.config.localVideo) { this.config.localVideo.srcObject = stream; this.config.localVideo.muted = true; this.config.localVideo.play().catch(() => {}); } // 添加到连接 // stream.getTracks().forEach(track => { // this.peerConnection.addTrack(track, stream); // }); } /** * 获取本地媒体流 * @returns {MediaStream|null} 本地流 */ getLocalStream() { return this.localStream; } /** * 获取远端媒体流 * @returns {MediaStream|null} 远端流 */ getRemoteStream() { return this.remoteStream; } /** * 发起方:创建 Offer 并开启数据通道 * @returns {Promise} Offer 对象 */ async createOffer() { // 重置数据通道 if (this.dataChannel) { this.dataChannel.close(); this.dataChannel = null; } // 创建数据通道(发起方主动创建) this.dataChannel = this.peerConnection.createDataChannel( this.config.dataChannelLabel, ); // 监听数据通道 this.dataChannelLisioner(); const offer = await this.peerConnection.createOffer(); // 设置本地offer await this.peerConnection.setLocalDescription(offer); return offer; } /** * 接收方:处理远端 Offer 并创建 Answer * @param {RTCSessionDescription} offer 远端 Offer * @returns {Promise} Answer 对象 */ async handleRemoteOffer(offer) { await this.peerConnection.setRemoteDescription( new RTCSessionDescription(offer), ); const answer = await this.peerConnection.createAnswer(); await this.peerConnection.setLocalDescription(answer); return answer; } /** * 发起方:处理远端 Answer * @param {RTCSessionDescription} answer 远端 Answer */ async handleAnswer(answer) { try { // 检查当前信令状态,仅在非 stable 时设置 if (this.peerConnection.signalingState !== "stable") { await this.peerConnection.setRemoteDescription( new RTCSessionDescription(answer), ); console.info("远端 Answer 设置成功"); } else { console.warn("PeerConnection 已处于稳定状态,无需重复设置 Answer"); } } catch (error) { console.error("设置远端 Answer 失败:", error); } } /** * 添加远端 ICE 候选 * @param {RTCIceCandidate} candidate ICE 候选对象 */ async addIceCandidate(candidate) { if (!candidate) return; try { if (this.peerConnection && candidate) { await this.peerConnection.addIceCandidate( new RTCIceCandidate(candidate), ); console.info("ICE 候选者添加成功", candidate); } else return; } catch (error) { console.info( "操作失败,需要先设置【远端的SDP】:步骤1:先设置远端 Offer(发起者的 SDP)步骤2:再添加 ICE 候选者(此时已有 remoteDescription)步骤3:创建并设置本地 Answer(依赖 remoteDescription)", ); console.error(error); if (error.message.includes("Unknown ufrag")) { // ufrag 不匹配,说明是旧会话候选者,直接丢弃 console.warn("ICE 候选者 ufrag 不匹配,丢弃:", candidate); } else { // 其他错误(如未设置 remoteDescription),缓存候选者 console.info("缓存 ICE 候选者,待会话就绪后处理"); } } } /** * 发送自定义数据(支持字符串/对象) * @param {string|Object} data 要发送的数据 */ dataChannelSendMsg(data) { if (!this.dataChannel || this.dataChannel.readyState !== "open") { throw new Error("数据通道未连接,无法发送消息"); } const sendData = typeof data === "object" ? JSON.stringify(data) : data; this.dataChannel.send(sendData); } /** * 关闭连接并清理所有资源 */ close() { // 关闭数据通道 if (this.dataChannel) { this.dataChannel.close(); this.dataChannel = null; } // 关闭 PeerConnection if (this.peerConnection) { this.peerConnection.close(); this.peerConnection = null; } // 停止媒体流 if (this.localStream) { this.localStream.getTracks().forEach((track) => track.stop()); this.localStream = null; } // 清空视频 if (this.config.localVideo) this.config.localVideo.srcObject = null; if (this.config.remoteVideo) this.config.remoteVideo.srcObject = null; this.callbacks.onClose(); console.log("WebRTC 连接已完全关闭"); } /** * 同步暂停指定毫秒数(阻塞主线程) 延迟一段时间运行 await this.delay(100); * @param {number} ms 暂停毫秒数 */ delayAsync(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * 同步暂停指定毫秒数(阻塞主线程) * @param {number} ms 暂停毫秒数 */ delaySync(ms) { const start = Date.now(); // 循环等待,直到时间差达到指定毫秒数 while (Date.now() - start < ms) { // 空循环,消耗时间 } } /** * 注册事件回调 * @param {Object} handlers 回调函数集合 * 示例:rtc.on({ onMessage: (data) => console.log(data) }) */ on(handlers) { Object.keys(handlers).forEach((key) => { if (this.callbacks[key] && typeof handlers[key] === "function") { this.callbacks[key] = handlers[key]; } }); } } // 封装websocket export class webSocket_ModuleSimple { constructor(url, receiveMessageCallback = null) { this.url = url; this.socket = null; this.heartbeatInterval = 30000; // 心跳间隔 30s this.reconnectInterval = 10000; // 重连间隔 10s this.maxReconnectAttempts = 5; this.reconnectAttempts = 0; this.heartbeatTimer = null; this.stopWs = false; this.receiveMessageCallback = receiveMessageCallback; // 接收消息回调函数 this.callbacks = { onMessage: (data) => {}, // 收到自定义消息 }; } connect() { if (this.socket && this.socket.readyState === WebSocket.OPEN) return; this.socket = new WebSocket(this.url); this.socket.onopen = () => { console.log("连接成功", this.url); this.reconnectAttempts = 0; this.startHeartbeat(); }; this.socket.onmessage = (event) => { // console.log('收到消息:', event.data); this.receiveMessage(event); this.startHeartbeat(); // 收到消息重置心跳 }; this.socket.onclose = () => { console.log("连接关闭", this.url); if (!this.stopWs) this.handleReconnect(); }; this.socket.onerror = () => { console.log("连接错误", this.url); this.closeHeartbeat(); }; } /** * 同步暂停指定毫秒数(阻塞主线程) 延迟一段时间运行 await this.delay(100); * @param {number} ms 暂停毫秒数 */ delayAsync(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * 同步暂停指定毫秒数(阻塞主线程) * @param {number} ms 暂停毫秒数 */ delaySync(ms) { const start = Date.now(); // 循环等待,直到时间差达到指定毫秒数 while (Date.now() - start < ms) { // 空循环,消耗时间 } } async send(message) { if (this.socket?.readyState === WebSocket.OPEN) { await this.delayAsync(500); this.socket.send(JSON.stringify(message)); } else { console.error("WebSocket 未连接"); } } startHeartbeat() { this.closeHeartbeat(); this.heartbeatTimer = setInterval(() => { if (this.socket?.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify({ type: "heartBeat" })); // console.log('发送心跳包'); } }, this.heartbeatInterval); } closeHeartbeat() { clearInterval(this.heartbeatTimer); } handleReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { setTimeout(() => { console.log(`尝试重连 (${++this.reconnectAttempts})`); this.connect(); }, this.reconnectInterval); } else { console.warn("达到最大重连次数,停止重连"); } } close() { this.stopWs = true; this.socket?.close(); this.closeHeartbeat(); } /** * 接收到消息 */ receiveMessage(event) { // 根据业务自行处理 // console.info('[webSocket_ModuleSimple]receiveMessage:', event.data) this.receiveMessageCallback && this.receiveMessageCallback(event.data); this.callbacks.onMessage(event.data); } /** * 注册事件回调 * @param {Object} handlers 回调函数集合 * 示例:rtc.on({ onMessage: (data) => console.log(data) }) */ on(handlers) { Object.keys(handlers).forEach((key) => { if (this.callbacks[key] && typeof handlers[key] === "function") { this.callbacks[key] = handlers[key]; } }); } } // ----------------------------------------------> // 验证与使用 // const receiveMessage = (res) => { // console.log('接收消息回调:', res) // } // const wsClient = new WebSocketClient('`ws://localhost:8000/ws/0/`', receiveMessage); // wsClient.connect(); // 发送业务消息 // wsClient.send({ type: 'message', data: 'Hello Server' });