Files
deskflow/module/newWebRtcSync.js
2026-04-09 10:37:51 +08:00

610 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 【发起方】
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<MediaStream>} 本地媒体流
*/
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<RTCSessionDescription>} 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<RTCSessionDescription>} 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' });