项目备份
This commit is contained in:
373
myRtc.py
Normal file
373
myRtc.py
Normal file
@@ -0,0 +1,373 @@
|
||||
'''
|
||||
通过python获取win屏幕媒体流传递给js
|
||||
'''
|
||||
import asyncio
|
||||
import ctypes
|
||||
import threading
|
||||
import numpy as np
|
||||
import win32gui
|
||||
import win32ui
|
||||
import win32con
|
||||
import webview
|
||||
from aiortc import MediaStreamTrack, RTCPeerConnection
|
||||
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription
|
||||
|
||||
# -------------------------- 1. Windows高DPI适配 --------------------------
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
||||
|
||||
# -------------------------- 2. 兼容VideoFrame导入 --------------------------
|
||||
try:from aiortc.contrib.media import VideoFrame,AudioFrame
|
||||
except ImportError:from aiortc import VideoFrame,AudioFrame
|
||||
|
||||
# -------------------------- 2. AIORTC屏幕采集轨道 --------------------------
|
||||
class ScreenVideoTrack(MediaStreamTrack):
|
||||
kind = "video"
|
||||
|
||||
def __init__(self, fps=30):
|
||||
super().__init__()
|
||||
self.fps = fps
|
||||
self.stop_flag = False
|
||||
self.pts = 0
|
||||
self.frame_interval = 1.0 / fps
|
||||
# 获取真实屏幕分辨率(适配高DPI)
|
||||
self.user32 = ctypes.windll.user32
|
||||
self.width = self.user32.GetSystemMetrics(0)
|
||||
self.height = self.user32.GetSystemMetrics(1)
|
||||
|
||||
def capture_screen(self):
|
||||
"""Windows原生GDI采集屏幕(低延迟)"""
|
||||
hdesktop = win32gui.GetDesktopWindow()
|
||||
hwnd_dc = win32gui.GetWindowDC(hdesktop)
|
||||
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
|
||||
save_dc = mfc_dc.CreateCompatibleDC()
|
||||
save_bitmap = win32ui.CreateBitmap()
|
||||
save_bitmap.CreateCompatibleBitmap(mfc_dc, self.width, self.height)
|
||||
save_dc.SelectObject(save_bitmap)
|
||||
save_dc.BitBlt((0, 0), (self.width, self.height), mfc_dc, (0, 0), win32con.SRCCOPY)
|
||||
|
||||
# 转换为numpy数组(BGR格式,适配aiortc)
|
||||
bmp_data = save_bitmap.GetBitmapBits(True)
|
||||
frame = np.frombuffer(bmp_data, dtype=np.uint8).reshape((self.height, self.width, 4))
|
||||
frame = frame[:, :, :3][:, :, ::-1] # BGRA → BGR
|
||||
|
||||
# 释放资源,避免内存泄漏
|
||||
win32gui.DeleteObject(save_bitmap.GetHandle())
|
||||
save_dc.DeleteDC()
|
||||
mfc_dc.DeleteDC()
|
||||
win32gui.ReleaseDC(hdesktop, hwnd_dc)
|
||||
return frame
|
||||
|
||||
async def recv(self):
|
||||
"""aiortc核心方法:返回视频帧"""
|
||||
if self.stop_flag:
|
||||
raise StopAsyncIteration
|
||||
await asyncio.sleep(self.frame_interval)
|
||||
frame_data = self.capture_screen()
|
||||
|
||||
# 转换为aiortc VideoFrame
|
||||
video_frame = VideoFrame.from_ndarray(frame_data, format="bgr24")
|
||||
video_frame.pts = int(self.pts)
|
||||
video_frame.time_base = np.array([1, self.fps])
|
||||
self.pts += 1
|
||||
return video_frame
|
||||
|
||||
def stop(self):
|
||||
"""停止屏幕采集"""
|
||||
self.stop_flag = True
|
||||
|
||||
# -------------------------- 3. WebRTC服务(异步线程运行) --------------------------
|
||||
class WebRTCService:
|
||||
def __init__(self):
|
||||
self.pc = None
|
||||
self.screen_track = None
|
||||
self.loop = asyncio.new_event_loop()
|
||||
self.thread = None
|
||||
|
||||
def start_async_loop(self):
|
||||
"""在子线程启动asyncio循环,避免阻塞pywebview UI"""
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_forever()
|
||||
|
||||
async def create_offer(self):
|
||||
"""生成Offer信令,供前端调用"""
|
||||
# 初始化PeerConnection(配置STUN服务器,解决NAT穿透)
|
||||
self.pc = RTCPeerConnection()
|
||||
self.pc.addIceServers([{"urls": "stun:stun.l.google.com:19302"}])
|
||||
|
||||
# 添加屏幕采集轨道
|
||||
self.screen_track = ScreenVideoTrack(fps=30)
|
||||
self.pc.addTrack(self.screen_track)
|
||||
print("✅ Python侧:已添加屏幕采集轨道")
|
||||
|
||||
# 监听连接状态
|
||||
@self.pc.on("connectionstatechange")
|
||||
def on_connectionstatechange():
|
||||
print(f"🔌 WebRTC连接状态:{self.pc.connectionState}")
|
||||
if self.pc.connectionState in ["failed", "closed", "disconnected"]:
|
||||
if self.screen_track:
|
||||
self.screen_track.stop()
|
||||
|
||||
# 生成Offer并设置本地SDP
|
||||
offer = await self.pc.createOffer()
|
||||
await self.pc.setLocalDescription(offer)
|
||||
|
||||
# 返回序列化的Offer(供前端解析)
|
||||
return {
|
||||
"sdp": self.pc.localDescription.sdp,
|
||||
"type": self.pc.localDescription.type
|
||||
}
|
||||
|
||||
async def set_answer(self, answer_data):
|
||||
"""接收前端的Answer信令并应用"""
|
||||
if not self.pc:
|
||||
raise Exception("PeerConnection未初始化")
|
||||
|
||||
# 反序列化Answer
|
||||
answer = RTCSessionDescription(
|
||||
sdp=answer_data["sdp"],
|
||||
type=answer_data["type"]
|
||||
)
|
||||
await self.pc.setRemoteDescription(answer)
|
||||
print("✅ Python侧:已应用前端Answer信令,P2P连接建立")
|
||||
|
||||
def start(self):
|
||||
"""启动WebRTC服务(子线程)"""
|
||||
self.thread = threading.Thread(target=self.start_async_loop, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""停止服务并清理资源"""
|
||||
if self.screen_track:
|
||||
self.screen_track.stop()
|
||||
if self.pc:
|
||||
asyncio.run_coroutine_threadsafe(self.pc.close(), self.loop)
|
||||
self.loop.stop()
|
||||
if self.thread:
|
||||
self.thread.join()
|
||||
|
||||
# -------------------------- 4. PyWebView API通信层 --------------------------
|
||||
class PyWebViewAPI:
|
||||
def __init__(self, webrtc_service):
|
||||
self.webrtc_service = webrtc_service
|
||||
|
||||
# 供前端JS调用:获取Offer信令
|
||||
async def get_offer(self):
|
||||
return await asyncio.wrap_future(
|
||||
asyncio.run_coroutine_threadsafe(self.webrtc_service.create_offer(), self.webrtc_service.loop)
|
||||
)
|
||||
|
||||
# 供前端JS调用:发送Answer信令到Python
|
||||
async def send_answer(self, answer_data):
|
||||
await asyncio.wrap_future(
|
||||
asyncio.run_coroutine_threadsafe(self.webrtc_service.set_answer(answer_data), self.webrtc_service.loop)
|
||||
)
|
||||
|
||||
# -------------------------- 5. 前端页面(向日葵风格) --------------------------
|
||||
HTML_CONTENT = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Windows屏幕共享</title>
|
||||
<style>
|
||||
* {margin: 0; padding: 0; box-sizing: border-box; font-family: "Microsoft YaHei", Arial;}
|
||||
body {background: #f5f7fa; color: #333;}
|
||||
|
||||
/* 顶部导航 */
|
||||
.header {
|
||||
background: #2d8cf0; color: white; padding: 12px 24px;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1); position: sticky; top: 0; z-index: 100;
|
||||
}
|
||||
.status-indicator {display: flex; align-items: center; gap: 6px;}
|
||||
.status-dot {
|
||||
width: 10px; height: 10px; border-radius: 50%; background: #ff4d4f;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
.status-dot.connected {background: #52c41a;}
|
||||
@keyframes pulse {0% {opacity: 1;} 50% {opacity: 0.5;} 100% {opacity: 1;}}
|
||||
|
||||
/* 主容器 */
|
||||
.container {max-width: 1600px; margin: 20px auto; padding: 0 20px;}
|
||||
|
||||
/* 控制栏 */
|
||||
.control-bar {
|
||||
background: white; padding: 12px 20px; border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 20px;
|
||||
display: flex; gap: 15px; align-items: center;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 16px; border: none; border-radius: 4px;
|
||||
cursor: pointer; font-size: 14px; transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {background: #2d8cf0; color: white;}
|
||||
.btn-primary:hover {background: #2b85e4;}
|
||||
.btn-danger {background: #ff4d4f; color: white;}
|
||||
.btn-danger:hover {background: #ff7875;}
|
||||
|
||||
/* 屏幕显示区 */
|
||||
.screen-container {
|
||||
background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
overflow: hidden; min-height: 600px; display: flex;
|
||||
align-items: center; justify-content: center; position: relative;
|
||||
}
|
||||
.loading {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(255,255,255,0.8); display: flex;
|
||||
flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 15px; z-index: 10;
|
||||
}
|
||||
.spinner {
|
||||
width: 40px; height: 40px; border: 4px solid #f0f2f5;
|
||||
border-top: 4px solid #2d8cf0; border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {0% {transform: rotate(0deg);} 100% {transform: rotate(360deg);}}
|
||||
#screen-video {width: 100%; height: auto; display: block;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<h2>Windows屏幕共享</h2>
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot" id="status-dot"></div>
|
||||
<span id="status-text">未连接</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="control-bar">
|
||||
<button class="btn btn-primary" id="connect-btn">开始共享屏幕</button>
|
||||
<button class="btn btn-danger" id="disconnect-btn" disabled>停止共享</button>
|
||||
</div>
|
||||
|
||||
<div class="screen-container">
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<div>正在建立连接...</div>
|
||||
</div>
|
||||
<video id="screen-video" autoplay muted playsinline></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
let pc = null; // 浏览器端PeerConnection
|
||||
const videoEl = document.getElementById('screen-video');
|
||||
const loadingEl = document.getElementById('loading');
|
||||
const connectBtn = document.getElementById('connect-btn');
|
||||
const disconnectBtn = document.getElementById('disconnect-btn');
|
||||
const statusDot = document.getElementById('status-dot');
|
||||
const statusText = document.getElementById('status-text');
|
||||
|
||||
// 更新连接状态
|
||||
function updateStatus(connected) {
|
||||
statusDot.className = connected ? 'status-dot connected' : 'status-dot';
|
||||
statusText.textContent = connected ? '已连接' : '未连接';
|
||||
loadingEl.style.display = connected ? 'none' : 'flex';
|
||||
connectBtn.disabled = connected;
|
||||
disconnectBtn.disabled = !connected;
|
||||
}
|
||||
|
||||
// 连接屏幕共享
|
||||
async function connectScreen() {
|
||||
try {
|
||||
updateStatus(false);
|
||||
|
||||
// 1. 调用Python API获取Offer信令
|
||||
const offer = await window.pywebview.api.get_offer();
|
||||
|
||||
// 2. 初始化浏览器端WebRTC
|
||||
pc = new RTCPeerConnection({
|
||||
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
|
||||
});
|
||||
|
||||
// 3. 监听媒体流,渲染到video标签
|
||||
pc.ontrack = (e) => {
|
||||
videoEl.srcObject = e.streams[0];
|
||||
updateStatus(true);
|
||||
};
|
||||
|
||||
// 4. 监听连接状态
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc.connectionState === 'disconnected') {
|
||||
disconnectScreen();
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 应用Offer并生成Answer
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
|
||||
// 6. 发送Answer到Python侧
|
||||
await window.pywebview.api.send_answer({
|
||||
sdp: pc.localDescription.sdp,
|
||||
type: pc.localDescription.type
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
alert('连接失败:' + e.message);
|
||||
updateStatus(false);
|
||||
console.error('连接错误:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 断开屏幕共享
|
||||
function disconnectScreen() {
|
||||
if (pc) {
|
||||
pc.close();
|
||||
pc = null;
|
||||
}
|
||||
videoEl.srcObject = null;
|
||||
updateStatus(false);
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
connectBtn.addEventListener('click', connectScreen);
|
||||
disconnectBtn.addEventListener('click', disconnectScreen);
|
||||
|
||||
// 页面关闭时清理
|
||||
window.addEventListener('unload', disconnectScreen);
|
||||
|
||||
// 初始状态
|
||||
updateStatus(false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# -------------------------- 6. 主程序入口 --------------------------
|
||||
if __name__ == "__main__":
|
||||
# 初始化WebRTC服务并启动异步线程
|
||||
webrtc_service = WebRTCService()
|
||||
webrtc_service.start()
|
||||
|
||||
# 初始化PyWebView API
|
||||
api = PyWebViewAPI(webrtc_service)
|
||||
|
||||
# 创建PyWebView窗口(必须用Edge Chromium引擎,支持WebRTC)
|
||||
window = webview.create_window(
|
||||
title="Windows屏幕共享",
|
||||
html=HTML_CONTENT,
|
||||
width=1400,
|
||||
height=900,
|
||||
resizable=True,
|
||||
confirm_close=True,
|
||||
easy_drag=False,
|
||||
js_api=api
|
||||
)
|
||||
|
||||
# 启动PyWebView(关键:指定edgechromium引擎)
|
||||
try:
|
||||
webview.start(
|
||||
gui='edgechromium', # 强制使用WebView2引擎
|
||||
private_mode=False, # WebRTC必需关闭私有模式
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 程序被手动终止")
|
||||
finally:
|
||||
# 清理资源
|
||||
webrtc_service.stop()
|
||||
print("✅ 资源已清理完成")
|
||||
Reference in New Issue
Block a user