Files
deskflow/docs/webrtc_demo_callee.html
2026-04-09 10:37:51 +08:00

340 lines
10 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebRTC 接收方</title>
<style>
.video-container {
display: flex;
gap: 20px;
margin: 20px 0;
}
video {
width: 400px;
height: 300px;
border: 1px solid #ccc;
}
.controls {
margin: 20px 0;
}
button {
padding: 10px 20px;
margin-right: 10px;
}
#dataChannelLog {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
height: 240px;
overflow-y: auto;
}
</style>
</head>
<body>
<h1>WebRTC 接收方</h1>
<div class="video-container">
<div>
<h3>本地视频</h3>
<video id="localVideo" autoplay muted></video>
</div>
<div>
<h3>远程视频</h3>
<video id="remoteVideo" autoplay></video>
</div>
</div>
<div class="controls">
<button id="startBtn">3.获取媒体信息</button>
<button id="answerBtn" disabled>4.接听</button>
<button id="sendBtn" disabled>发送消息</button>
<input type="text" id="messageInput" placeholder="输入消息" disabled>
</div>
<div>
<h3>数据通道日志</h3>
<div id="dataChannelLog"></div>
</div>
<script>
// 全局变量
let localStream;
let pc;
let dataChannel;
let clientCandidates = [];
let ws; // 改为let方便重新创建
let isWsConnected = false; // WebSocket连接状态标记
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const startBtn = document.getElementById('startBtn');
const answerBtn = document.getElementById('answerBtn');
const sendBtn = document.getElementById('sendBtn');
const messageInput = document.getElementById('messageInput');
const dataChannelLog = document.getElementById('dataChannelLog');
let offerData = null;
// 初始化WebSocket连接
function initWebSocket() {
// 先关闭旧连接
if (ws) {
try { ws.close() }
catch (e) { }
}
const user_id = 1;
const wsService = `wss://api.vlos.net/wsSignaling/${user_id}/`
// 创建新连接
// ws = new WebSocket(`ws://localhost:8000/wsSignaling/1/`);
ws = new WebSocket(wsService);
// WebSocket连接成功
ws.onopen = () => {
console.info('WebSocket已连接');
isWsConnected = true;
addToLog('信令通道已建立');
};
// WebSocket 响应消息处理
ws.onmessage = async (event) => {
if (!isWsConnected) return;
const message = JSON.parse(event.data);
console.info('收到信令消息:', message.type);
try {
switch (message.type) {
case 'offer':
if (pc) {
offerData = message.data;
console.info("offerData", offerData)
// 如果已初始化,直接启用接听按钮
if (pc) { answerBtn.disabled = false; }
}
break;
case 'candidate':
if (message.data && pc) {
await pc.addIceCandidate(new RTCIceCandidate(message.data));
}
console.info(`17,发起方响应响应方发送过来的远程候选`)
break;
case 'hangup':
handleHangup();
break;
}
} catch (error) {
console.error('处理信令消息出错:', error);
addToLog(`处理信令错误: ${error.message}`);
}
};
// WebSocket关闭
ws.onclose = () => {
console.info('WebSocket已关闭');
isWsConnected = false;
addToLog('信令通道已关闭');
// 自动重连逻辑(可选)
setTimeout(() => {
if (!isWsConnected) {
addToLog('尝试重新连接信令服务器...');
initWebSocket();
}
}, 3000);
};
// WebSocket错误
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
isWsConnected = false;
addToLog(`信令通道错误: ${error.message}`);
};
}
// 安全的WebSocket发送方法
function sendSignalingMessage(message) {
// 检查WebSocket状态
if (!ws || !isWsConnected || ws.readyState !== WebSocket.OPEN) {
addToLog('无法发送信令消息:信令通道未连接');
return false;
}
try {
ws.send(JSON.stringify(message));
return true;
} catch (error) {
console.error('发送信令消息失败:', error);
addToLog(`发送信令失败: ${error.message}`);
return false;
}
}
// 初始化WebSocket
initWebSocket();
// 开始按钮 - 获取本地媒体流
startBtn.addEventListener('click', async () => {
try {
// 获取本地音视频流
// localStream = await navigator.mediaDevices.getUserMedia({video: true,audio: true});
// 获取屏幕共享
localStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
localVideo.srcObject = localStream;
// 创建 PeerConnection
createPeerConnection();
// 如果已有 offer启用接听按钮
if (offerData) { answerBtn.disabled = false; }
console.info(`17,响应方响应发起方过来的offer`, offerData)
startBtn.disabled = true;
} catch (error) {
console.error('获取媒体流失败:', error);
alert('无法访问摄像头/麦克风,请检查权限');
}
});
// 接听按钮 - 应答呼叫
answerBtn.addEventListener('click', async () => {
try {
if (!offerData) return;
addToLog(`初始化answer`)
// 设置远程描述
await pc.setRemoteDescription(new RTCSessionDescription(offerData));
console.info(`18,响应方将发起方发送过来的offer设置到本地pc中的远程描述setRemoteDescription`)
// 创建 answer
const answer = await pc.createAnswer();
console.info(`19,响应方创建anser`)
await pc.setLocalDescription(answer);
console.info(`20,响应方设置answer到本地pc中setLocalDescription`)
// 发送 answer 到信令服务器
ws.send(JSON.stringify({ type: 'answer', data: answer }));
console.info(`21,响应方发送answer到发起方`)
// answerBtn.disabled = true;
sendBtn.disabled = false;
messageInput.disabled = false;
} catch (error) {
console.error('应答呼叫失败:', error);
}
});
// 发送数据通道消息
sendBtn.addEventListener('click', () => {
const message = messageInput.value.trim();
if (!message || !dataChannel || dataChannel.readyState !== 'open') return;
dataChannel.send(message);
addToLog(`响应方: ${message}`);
messageInput.value = '';
});
// 创建 PeerConnection
function createPeerConnection() {
// 配置 ICE 服务器
const config = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};
// 创建 PeerConnection
pc = new RTCPeerConnection(config);
console.info(`12,响应方创建自身PeerConnection`)
// 添加本地媒体流到 PeerConnection
localStream.getTracks().forEach(track => { pc.addTrack(track, localStream) });
console.info(`13,响应方添加本地媒体流到 PeerConnection`)
// 响应远程流
pc.ontrack = (event) => { remoteVideo.srcObject = event.streams[0] };
console.info(`14,响应方响应远程流`)
// 响应数据通道
pc.ondatachannel = (event) => {
dataChannel = event.channel;
setupDataChannel();
};
console.info(`15,响应方响应数据通道`)
// 响应 ICE 候选
pc.onicecandidate = (event) => {
if (event.candidate) {
let ws_json = JSON.stringify({ type: 'candidate', data: event.candidate });
ws.send(ws_json);
console.info(`响应 ICE 候选->${ws_json}`)
}
};
console.info(`16,响应方响应响应方自身ICE候选,并发送到发起方`)
// 响应连接状态变化
pc.oniceconnectionstatechange = () => {
console.info('ICE 连接状态:', pc.iceConnectionState);
if (pc.iceConnectionState === 'disconnected') {
handleHangup();
}
};
}
// 设置数据通道
function setupDataChannel() {
// 数据通道打开
dataChannel.onopen = () => {
addToLog('数据通道已打开');
};
// 收到数据通道消息
dataChannel.onmessage = (event) => {
addToLog(`对方: ${event.data}`);
};
// 数据通道关闭
dataChannel.onclose = () => {
addToLog('数据通道已关闭');
sendBtn.disabled = true;
messageInput.disabled = true;
};
// 数据通道错误
dataChannel.onerror = (error) => {
console.error('数据通道错误:', error);
addToLog(`数据通道错误: ${error.message}`);
};
}
// 处理挂断
function handleHangup() {
if (pc) {
pc.close();
pc = null;
}
if (localStream) {
localStream.getTracks().forEach(
track => track.stop()
);
localStream = null;
}
localVideo.srcObject = null;
remoteVideo.srcObject = null;
startBtn.disabled = false;
answerBtn.disabled = true;
sendBtn.disabled = true;
messageInput.disabled = true;
offerData = null;
addToLog('连接已断开');
}
// 添加日志
function addToLog(message) {
const time = new Date().toLocaleTimeString();
dataChannelLog.innerHTML += `[${time}] ${message}<br>`;
dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
}
// 页面关闭时清理
window.addEventListener('beforeunload', () => {
ws.send(JSON.stringify({ type: 'hangup' }));
if (pc) pc.close();
if (localStream) localStream.getTracks().forEach(track => track.stop());
});
</script>
</body>
</html>