From 02d7c485573cf2c85fd82343c1fc814c2b89137d Mon Sep 17 00:00:00 2001 From: wsy182 <2392948297@qq.com> Date: Mon, 1 Jun 2026 23:51:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=94=B9=E7=94=A8?= =?UTF-8?q?=20aiortc=20=E5=AE=9E=E7=8E=B0=20WebRTC=20=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=B5=81=E6=8E=A5=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handle_client.py: 替换原始 JPEG WebSocket 为 aiortc RTCPeerConnection - 实现 SDP offer/answer 协商和 ICE candidate 交换 - 通过 track.recv() 接收 RTP 视频帧并 cv2.imshow 显示 - 服务端口改为 8765 匹配 Android 端 - 新增 requirements.txt: aiortc, websockets, opencv-python 等 - .gitignore 添加 __pycache__ --- .gitignore | 3 +- handle_client.py | 118 +++++++++++++++++++++++++++++++++++------------ requirements.txt | 5 ++ 3 files changed, 96 insertions(+), 30 deletions(-) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 0949605..31dc02d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .venv/ -.idea/ \ No newline at end of file +.idea/ +__pycache__/ \ No newline at end of file diff --git a/handle_client.py b/handle_client.py index 2d4e114..d69069e 100644 --- a/handle_client.py +++ b/handle_client.py @@ -1,61 +1,121 @@ import asyncio +import json +import re import websockets import cv2 -import numpy as np from loguru import logger +from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate async def handle_client(websocket): client = websocket.remote_address logger.info(f"Client connected: {client}") - frame_count = 0 + pc = RTCPeerConnection() + video_task = None + + def parse_ice(data): + match = re.match( + r'candidate:(\S+) (\d) (\S+) (\d+) (\S+) (\d+) typ (\S+)(?: raddr (\S+) rport (\d+))?', + data["candidate"] + ) + if not match: + return None + g = match.groups() + cand = RTCIceCandidate( + foundation=g[0], + component=int(g[1]), + protocol=g[2].lower(), + priority=int(g[3]), + ip=g[4], + port=int(g[5]), + type=g[6], + relatedAddress=g[7], + relatedPort=int(g[8]) if g[8] else None, + ) + cand.sdpMid = data.get("sdpMid") + cand.sdpMLineIndex = data.get("sdpMLineIndex", 0) + return cand + + async def receive_video(track): + logger.info("Start receiving video frames") + frame_count = 0 + try: + while True: + frame = await track.recv() + frame_count += 1 + img = frame.to_ndarray(format="bgr24") + cv2.imshow("Android Camera (WebRTC)", img) + + if frame_count % 100 == 0: + logger.info(f"Received {frame_count} frames, shape={img.shape}") + + if cv2.waitKey(1) & 0xFF == 27: + logger.info("ESC pressed, closing display") + break + except asyncio.CancelledError: + logger.info("Video receive task cancelled") + except Exception as e: + logger.error(f"Video receive error: {e}") + + @pc.on("track") + async def on_track(track): + logger.info(f"Track received: kind={track.kind}") + if track.kind == "video": + nonlocal video_task + video_task = asyncio.ensure_future(receive_video(track)) + + @pc.on("iceconnectionstatechange") + async def on_iceconnectionstatechange(): + logger.info(f"ICE state: {pc.iceConnectionState}") + if pc.iceConnectionState in ("failed", "closed", "disconnected"): + await pc.close() try: async for message in websocket: - frame_count += 1 + data = json.loads(message) + msg_type = data.get("type") - data = np.frombuffer(message, dtype=np.uint8) - frame = cv2.imdecode(data, cv2.IMREAD_COLOR) + if msg_type == "offer": + offer = RTCSessionDescription(sdp=data["sdp"], type="offer") + await pc.setRemoteDescription(offer) - if frame is None: - logger.warning(f"Decode frame failed from client={client}") - continue + answer = await pc.createAnswer() + await pc.setLocalDescription(answer) - if frame_count % 100 == 0: - logger.info(f"Received frames={frame_count}, client={client}, size={len(message)} bytes") + await websocket.send(json.dumps({ + "type": "answer", + "sdp": pc.localDescription.sdp, + })) - cv2.imshow("Android Camera", frame) - - if cv2.waitKey(1) & 0xFF == 27: - logger.info("ESC pressed, closing display") - break + elif msg_type == "candidate": + cand = parse_ice(data) + if cand: + await pc.addIceCandidate(cand) except websockets.ConnectionClosed: logger.info(f"Client disconnected: {client}") - except Exception as e: - logger.exception(f"WebSocket error, client={client}, error={e}") - + logger.exception(f"Error: {e}") finally: - logger.info(f"Connection closed: client={client}, total_frames={frame_count}") + if video_task: + video_task.cancel() + try: + await video_task + except asyncio.CancelledError: + pass + await pc.close() cv2.destroyAllWindows() + logger.info(f"Connection closed: {client}") async def main(): host = "0.0.0.0" port = 8765 - - logger.info(f"WebSocket server started: ws://{host}:{port}") - - async with websockets.serve( - handle_client, - host, - port, - max_size=10 * 1024 * 1024 - ): + logger.info(f"WebRTC signaling server: ws://{host}:{port}") + async with websockets.serve(handle_client, host, port, max_size=10 * 1024 * 1024): await asyncio.Future() if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0311b44 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +aiortc>=1.9.0 +websockets>=13.0 +opencv-python>=4.10.0 +numpy>=2.0.0 +loguru>=0.7.0