项目备份

This commit is contained in:
林觅
2026-04-09 10:37:51 +08:00
parent 7ed8d2dcb4
commit 4eab443148
32 changed files with 9468 additions and 0 deletions

373
myRtc.py Normal file
View 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("✅ 资源已清理完成")