项目备份
This commit is contained in:
29
docs/README.md
Normal file
29
docs/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# deskflow
|
||||
|
||||
#### 介绍
|
||||
|
||||
deskflow
|
||||
|
||||
### 激活windows虚拟环境
|
||||
|
||||
- & .venv\Scripts\Activate.ps1
|
||||
|
||||
#### 软件架构
|
||||
|
||||
- [https://pywebview.idepy.com/]
|
||||
|
||||
#### 安装库
|
||||
|
||||
- docs/venv.md
|
||||
|
||||
#### 安装教程
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### 使用说明
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
108
docs/app_JsToPy.html
Normal file
108
docs/app_JsToPy.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>PyWebView 双向通信</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#log {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Python ↔ JS 双向通信</h2>
|
||||
|
||||
<!-- JS 调用 Python 函数(同步) -->
|
||||
<div>
|
||||
<input type="text" id="nameInput" placeholder="输入姓名">
|
||||
<button onclick="callPythonSync()">调用 Python 同步函数</button>
|
||||
</div>
|
||||
|
||||
<!-- JS 调用 Python 异步函数 -->
|
||||
<div>
|
||||
<input type="number" id="durationInput" placeholder="异步任务耗时(秒)" value="2">
|
||||
<button onclick="callPythonAsync()">调用 Python 异步函数</button>
|
||||
</div>
|
||||
|
||||
<!-- 关闭窗口 -->
|
||||
<button onclick="callPythonCloseWindow()">关闭窗口</button>
|
||||
|
||||
<!-- 日志显示区域 -->
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---------------------- 1. JS 调用 Python 函数 ----------------------
|
||||
// 同步调用
|
||||
async function callPythonSync() {
|
||||
const name = document.getElementById('nameInput').value || '匿名用户';
|
||||
try {
|
||||
// 核心:通过 window.pywebview.api 调用 Python 函数
|
||||
const result = await window.pywebview.api.python_hello(name);
|
||||
addLog(`[JS] 调用 Python 同步函数成功:${result}`);
|
||||
} catch (e) {
|
||||
addLog(`[JS] 调用 Python 同步函数失败:${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 异步调用(Python 执行完后主动回调 JS)
|
||||
async function callPythonAsync() {
|
||||
const duration = document.getElementById('durationInput').value;
|
||||
try {
|
||||
const result = await window.pywebview.api.python_async_task(duration);
|
||||
addLog(`[JS] 调用 Python 异步函数成功:${result}`);
|
||||
} catch (e) {
|
||||
addLog(`[JS] 调用 Python 异步函数失败:${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 Python 关闭窗口
|
||||
async function callPythonCloseWindow() {
|
||||
await window.pywebview.api.python_close_window();
|
||||
}
|
||||
|
||||
// ---------------------- 2. 供 Python 调用的 JS 函数 ----------------------
|
||||
function js_receive_python_msg(msg) {
|
||||
addLog(`[JS] 收到 Python 消息:${msg}`);
|
||||
}
|
||||
|
||||
// ---------------------- 辅助函数:添加日志 ----------------------
|
||||
function addLog(text) {
|
||||
const logDiv = document.getElementById('log');
|
||||
const time = new Date().toLocaleTimeString();
|
||||
logDiv.innerHTML += `[${time}] ${text}<br>`;
|
||||
// 自动滚动到底部
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
|
||||
// 页面加载完成后提示
|
||||
window.onload = () => {
|
||||
addLog("页面加载完成,可开始双向通信测试");
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
77
docs/app_JsToPy.py
Normal file
77
docs/app_JsToPy.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import webview
|
||||
import threading
|
||||
import time,os
|
||||
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# ---------------------- 1. 定义供 JS 调用的 Python 函数 ----------------------
|
||||
class Api:
|
||||
"""暴露给 JS 的 Python 接口类(所有方法都会被 JS 访问)"""
|
||||
def __init__(self):
|
||||
self.window = None # 保存窗口实例,用于 Python 主动调用 JS
|
||||
|
||||
def set_window(self, window):
|
||||
"""初始化时绑定窗口实例"""
|
||||
self.window = window
|
||||
|
||||
def python_hello(self, name):
|
||||
"""JS 调用的同步函数:接收参数,返回结果"""
|
||||
print(f"[Python] 收到 JS 调用,参数:{name}")
|
||||
return f"Hello {name}! 这是 Python 返回的消息"
|
||||
|
||||
def python_async_task(self, duration):
|
||||
"""JS 调用的异步函数:模拟耗时任务(不阻塞前端)"""
|
||||
def async_task():
|
||||
print(f"[Python] 开始异步任务,耗时 {duration} 秒")
|
||||
time.sleep(duration)
|
||||
# 异步任务完成后,主动调用 JS 函数通知结果
|
||||
self.window.evaluate_js(f'js_receive_python_msg("异步任务完成!耗时 {duration} 秒")')
|
||||
print(f"[Python] 异步任务结束")
|
||||
|
||||
# 启动子线程执行异步任务,避免阻塞 UI
|
||||
threading.Thread(target=async_task, daemon=True).start()
|
||||
return "异步任务已启动,请等待结果通知"
|
||||
|
||||
def python_close_window(self):
|
||||
"""JS 调用关闭窗口"""
|
||||
print("[Python] 收到关闭窗口请求")
|
||||
self.window.destroy()
|
||||
|
||||
# ---------------------- 2. Python 主动调用 JS 函数 ----------------------
|
||||
def python_call_js_periodically(window):
|
||||
"""Python 定时调用 JS 函数(模拟主动推送数据)"""
|
||||
count = 0
|
||||
while window.is_alive():
|
||||
count += 1
|
||||
# 执行 JS 函数,传递参数
|
||||
window.evaluate_js(f'js_receive_python_msg("Python 主动推送:第 {count} 条消息")')
|
||||
time.sleep(3) # 每 3 秒推送一次
|
||||
if count >= 5:
|
||||
break
|
||||
|
||||
# ---------------------- 3. 启动窗口 ----------------------
|
||||
if __name__ == "__main__":
|
||||
# 创建 API 实例
|
||||
api = Api()
|
||||
|
||||
# 加载本地 HTML 文件(也可加载远程 URL:url="https://xxx.com")
|
||||
window = webview.create_window(
|
||||
title="PyWebView 双向通信示例",
|
||||
# url="app_JsToPy.html", # 前端页面路径
|
||||
url=f"file:///{CURRENT_DIR}/app_JsToPy.html",
|
||||
resizable=True,
|
||||
js_api=api # 关键:暴露 Python API 给 JS
|
||||
)
|
||||
|
||||
# 绑定窗口实例到 API(供异步任务调用 JS)
|
||||
api.set_window(window)
|
||||
|
||||
# 启动 Python 主动调用 JS 的线程(非阻塞)
|
||||
threading.Thread(target=python_call_js_periodically, args=(window,), daemon=True).start()
|
||||
|
||||
# 运行窗口(阻塞主线程)
|
||||
webview.start(
|
||||
private_mode=False, # WebRTC必需关闭私有模式
|
||||
debug=True, # 开发环境开启调试,生产环境关闭
|
||||
http_server=True # 启用内置 HTTP 服务器(加载本地 HTML 必需)
|
||||
)
|
||||
17
docs/releaseNotes.md
Normal file
17
docs/releaseNotes.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 版本日志
|
||||
|
||||
## 2026.03.
|
||||
|
||||
## 2026.03.16
|
||||
### 各端口规划
|
||||
- exe:windows端口,作为被控端
|
||||
- apk:安卓端,作为主控端
|
||||
- web:websocket接口和主控端
|
||||
### 功能
|
||||
- 全局
|
||||
- 三端都用手机号生成唯一的uuid, 已这个uuid作为控制唯一设备码
|
||||
- 剪切板同步;
|
||||
- 局部
|
||||
- exe:用python获取windows屏幕和系统音频转化之后转换未webrtc媒体流
|
||||
- web:web接口,生成 websocket通信/ice交换接口
|
||||
- apk:用python获取Android屏幕和系统音频转化之后转换未webrtc媒体流
|
||||
14
docs/venv.md
Normal file
14
docs/venv.md
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
# 环境安装
|
||||
|
||||
- pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade loguru pywebview aiortc fastapi pyinstaller uvicorn[standard] requests pystray pillow pywin32 buildozer opencv-python numpy mss
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 涉及文档
|
||||
|
||||
- [https://pywebview.idepy.com/guide/usage.html]
|
||||
- [https://segmentfault.com/a/1190000046888066]
|
||||
340
docs/webrtc_demo_callee.html
Normal file
340
docs/webrtc_demo_callee.html
Normal file
@@ -0,0 +1,340 @@
|
||||
<!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>
|
||||
339
docs/webrtc_demo_caller.html
Normal file
339
docs/webrtc_demo_caller.html
Normal file
@@ -0,0 +1,339 @@
|
||||
<!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">1.获取媒体信息</button>
|
||||
<button id="callBtn" disabled>2.呼叫</button>
|
||||
<button id="sendBtn" disabled>发送消息</button>
|
||||
<input type="text" id="messageInput" placeholder="输入消息">
|
||||
</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 callBtn = document.getElementById('callBtn');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const dataChannelLog = document.getElementById('dataChannelLog');
|
||||
|
||||
// 初始化WebSocket连接
|
||||
function initWebSocket() {
|
||||
// 先关闭旧连接
|
||||
if (ws) {
|
||||
try { ws.close() }
|
||||
catch (e) { }
|
||||
isWsConnected = false;
|
||||
}
|
||||
const user_id = 0;
|
||||
const wsService = `wss://api.vlos.net/wsSignaling/${user_id}/`
|
||||
// 创建新连接
|
||||
// ws = new WebSocket(`ws://localhost:8000/wsSignaling/0/`);
|
||||
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 'answer':
|
||||
if (pc) {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.data));
|
||||
console.info(`22,请求方监听对方发送过来的answer到本地setRemoteDescription`)
|
||||
}
|
||||
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 {
|
||||
|
||||
console.info(`2,请求方捕获媒体文件`)
|
||||
// 获取本地音视频流
|
||||
// localStream = await navigator.mediaDevices.getUserMedia({video: true,audio: true});
|
||||
// 获取屏幕共享
|
||||
localStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
|
||||
localVideo.srcObject = localStream;
|
||||
addToLog(`捕获本地媒体`);
|
||||
// 创建 PeerConnection
|
||||
createPeerConnection();
|
||||
addToLog(`创建 PeerConnection`);
|
||||
|
||||
// 启用呼叫按钮
|
||||
startBtn.disabled = true;
|
||||
callBtn.disabled = false;
|
||||
} catch (error) {
|
||||
console.error('获取媒体流失败:', error);
|
||||
alert('无法访问摄像头/麦克风,请检查权限');
|
||||
}
|
||||
});
|
||||
|
||||
// 呼叫按钮 - 请求呼叫
|
||||
callBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
// 创建数据通道
|
||||
dataChannel = pc.createDataChannel('chat');
|
||||
setupDataChannel();
|
||||
addToLog(`创建dataChannel`);
|
||||
console.info(`7,请求方创建dataChannel`)
|
||||
|
||||
// 创建 offer
|
||||
const offer = await pc.createOffer();
|
||||
console.info(`8,请求方创建offer`)
|
||||
await pc.setLocalDescription(offer);
|
||||
console.info(`9,请求方保存自己生成的信令`)
|
||||
addToLog(`创建offer`);
|
||||
|
||||
|
||||
// 发送 offer 到信令服务器
|
||||
ws.send(JSON.stringify({ type: 'offer', data: offer }));
|
||||
addToLog(`发送offer`);
|
||||
console.info(`10,请求方发送信令(offer)到远程端`)
|
||||
|
||||
// callBtn.disabled = true;
|
||||
sendBtn.disabled = false;
|
||||
messageInput.disabled = false;
|
||||
} catch (error) {
|
||||
console.error('请求呼叫失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 发送数据通道消息
|
||||
sendBtn.addEventListener('click', () => {
|
||||
const message = messageInput.value.trim();
|
||||
addToLog(`dataChannel.readyState-> ${dataChannel.readyState}`);
|
||||
if (!message || !dataChannel || dataChannel.readyState !== 'open') {
|
||||
console.info(`message:${message}`)
|
||||
console.info(`dataChannel:${dataChannel}`)
|
||||
console.info(`dataChannel.readyState:${dataChannel.readyState}`)
|
||||
return
|
||||
};
|
||||
|
||||
dataChannel.send(message);
|
||||
addToLog(`请求方: ${message}`);
|
||||
messageInput.value = '';
|
||||
});
|
||||
|
||||
// 创建 PeerConnection
|
||||
async function createPeerConnection() {
|
||||
// 配置 ICE 服务器 (使用免费的谷歌服务器)
|
||||
const config = {
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||
};
|
||||
|
||||
// 创建 PeerConnection
|
||||
pc = new RTCPeerConnection(config);
|
||||
console.info(`3,创建请求方PeerConnection`)
|
||||
|
||||
|
||||
// 监听 ICE 候选
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
// 使用安全发送方法
|
||||
clientCandidates.push(event.candidate);
|
||||
// sendSignalingMessage({type: 'candidate',data: event.candidate});
|
||||
}
|
||||
};
|
||||
console.info(`监听ICE 候选 ->${clientCandidates}`);
|
||||
console.info(`6,请求方监听请求方自己信令ice,并发送给监听方`)
|
||||
|
||||
|
||||
// 添加本地媒体流到PeerConnection
|
||||
localStream.getTracks().forEach(track => { pc.addTrack(track, localStream) });
|
||||
console.info(`4,请求方添加本地媒体到PeerConnection`)
|
||||
|
||||
// 监听远程流
|
||||
pc.ontrack = (event) => { remoteVideo.srcObject = event.streams[0] };
|
||||
console.info(`5,请求方监听远程媒体remoteVideo`)
|
||||
|
||||
// 监听连接状态变化
|
||||
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;
|
||||
callBtn.disabled = true;
|
||||
sendBtn.disabled = true;
|
||||
messageInput.disabled = true;
|
||||
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>
|
||||
Reference in New Issue
Block a user