跳至主要内容

本地摄像头实时推流至浏览器-基于WebRTC

· 閱讀時間約 12 分鐘
w0x7ce
MySelf

发布于 2025-08-01 20:32:17(微信公众号导出记录)。

本文来自公众号后台的“导出文章内容”功能。博客正文由导出长图进行本地 OCR 转写,并保留原始排版图用于逐段核对。

原文链接:查看原文

OCR 转写有效文字约 9333 字;代码、流程图和版式以文末原始排版图为准。

正文(本地 OCR 转写)

1.目标&理念

受 NVIDIA Isaac Sin 远程WebRTC访同方案启发,旨在实理一个现代化、低延退的程短流解决方 案,能够将任何一台计算机(PC或嵌人式设备)的摄像头面面,实时、安全地推送到一个标准的Web浏复 器中,且浏览器溯无需安装任何插件。 核心架构 此方案采用经典的WebRTC三组件架构,各部分职责分明:

  • 推流端(Streamer):负责从物理设备采集原始视频数据,进行编码,并通过WebRTC连接

将数据流发送出去。

  • 信令服务器(Signaling Server):不处理媒体数据。其功能是在推洗端和测览慰之同传

运“信令”(连接参数、网络候选地址等),以辅助两者建立直接的P2P连接。

  • 测览器端(Client):负责接收并解码体数据,最终将面面染在网页上。

ass dusjeuls

STUN server Data Channel

80 NAT

Signaling STUNServer STUN$e J8AJ

Signal Channel Peer A STUN Peer B Wto am I7 Symratsc NAT Chrme OfTer SOP Ofer SOP ICE carose ICE sanfle (A) CE cang ICE cancidte (0) Wto sm I7 213,51,61,325689 STUN Signal Channel Peer A Peer B

2.核心知识点:WebRTC技术选型分析

选择webRTC作为技术核心,是因其在实时互动场景中相较于传统嵌人式推流方案,具备根本性的优 势。 WebRTC的核心优势 1超低延退(ULtra-LowLatency):WebRTC优先使用UDP协议进行嫌体传输,并内置 了一系列为实时通信优化的拥塞控割和丢包重传算法。这使其端到站延退可以控制在508ms以 内,在良好网络下甚至能达到108ms级别。

  • 2原生测览器支持(Native Browser Support):所有现代浏览器(Chrome,

Firefox,Safari,Edge)均内置了webRTc标准。这意味着客户端无需安装任何插件 (如VLC、Flash),仅需一个网址即可双看视频。

  • 3强大的交互性(PowerfulInteractivity):WcbRTC提供了一个

RTCDataChannel(数通道),可用于传输任意二运制或文本数据,为远程控制(如键 盘、标、机爵人控制指令)等高级交互功能提供了原生支持。 3端到端加密(End-to-End Encryption):WebRTC强制要求所有通信都必须经过 DTLS和SRTP协议加密,确保了数据在传输过程中的安全性。

3.与传统嵌入式推流方案的对比

方案特性对比表 特性 WebRTC RTSP(安防常 RTMP(直播推流 HLS(网页直播常 常用) 用) 用) 3秒(较 典型延 < 580ms (极低) 16秒较 45+ 3- 10 参(巨大) 低) 高 退 浏览器 原生支持,无需播 极差,需VLC等插 极差,依翰Flas 原生支持,兼容性 h或rlv.js转码 件 极佳 兼容性 极强(内置数据通 交互性 弱(仅控制播故) 器(可封装少量 无(纯单向广播) 信令) 道) 较高(需要信令服 中等(需RTSP 较低(只需HTTP服 部暑复 中等(RTMP服 杂度 务器) 务器) 务器) 务器) 量适用 视频会议、远程通 局域网安防监控 向B站/斗鱼等平 体育赛事、新闻等 控、实时互动 大规模公开直播 场景 台推流 实现理念对比:WebRTC Vs.RTSP RTSP:解耦的“组件化“模型传统的RTSP方案在理念上遵循一种解属的、基于标准化组件的架 构。在此型中。各部分角色高度独立:推流端是一个无状态的数据生产者,其唯一职责是按 照RTP特议标准生成媒体流:RTSP服务是一个独立且有状态的服务,负责管理客户端会适和 流分发:客户端则是一个专门的媒体播放器(如VLC)。该模型的优势在于其模块化和互操作 性,在专业的、封闭的账体生态系统(如安防监控网络)中表现出色。然而,其代价是部暑和 管理的复杂性,且其组件化的特性造成了与现代web生志的巨大鸿沟-浏览器本易并不理解 RTSP这套语言。

  • WebRTC:集成的“左用蕴点~模型相比之下。本笔记实现的WebRTC方案在理念上是一和高度

集成的、以应用为中心的端点架构。推流端(Python脚本)不再仅仅是一个简单的数据生产 者,而是一个智能且有状态的应用端点,它主动参与了与浏览器端点之间复杂的对等网络协商 (信令交互和ICE流程),集媒体处理、网络协商、会话管理于一体。该模型将所有复杂性都 内聚在应用程序内部,从而为外部世界提供了一个级其管单的接口——一个标准的Wcb浏览器。 其设计世学是优先保证终端用户的访问便利性和实时互动能力,而非组件间的松散展合。

4.最终可运行的完整代码

以下为项目三个组件的最终可运行代码。

4.1.信令服务器(signaling_server.py)

Python inport asyncio 1nport websockets inport logging inport json fron collections Inport defaultdict Logging.basicConfig(level=logging INFo, farmat= %(asctime)s | %(leveln roons = defaultdict(set) async def handler(websocket, path): try: room_id = path.strip(/′) if not roon_id: logging.warning(f客户端{websocket,renate_address} 连接时未 ahalt websocket.close(code=1eo8, reason="Room ID 1s requin return rooms[ roon_id] add{websocket] logging.info(f"客产迪{websocket.remote_address} 加A房司‘{roon_ async for nessage in websocket: peers_to_broadcast = {peer far peer in rooms [roon_id] if p If peers_to_broadcast: analt asyncio.gather(*(peer.send(nessage) for peer 1n except websockets,exceptions.ConnectianClosed as e: logging.debug(f"连接已关闭:[websocket.remote_address]

  • e.code

except Exception as e: lagging-error[f"处理客户{websocket.remate_address} 时发生错误: finally: for roon_id, cllents in List(rooms.itens()): if websocket in clients: clients,remove(websocket) logging.info(f客户端[websocket.renote_address}离开房间 if not rlients: del rooms[roon_id] logging.info(f"房间‘{raom_id}已空,已覆关闭。“) break async def main(): port = 8765 logging.info(f*信令服务器已启动,监听端口{port)...) async with websockets,serve(handler, "g.9.ee", port): awalt asynclo.Future() == *nain": if name_ asyncio,run(nain())

4.2.Web 客户端(web_client/)

index.html HTML <!DoCTYPE html> <,u,=uel [u4s cheadx cneta charset=UTF-B"> cEeta nanesviewport" content=width=device-vidth, initial -scale=1 ctitle>WebRTC Viever</titles cstyles body, htnl ( font-family: -apple-systen, BlinkMacSystenFont, Pvideo-container { width: 96%; max-width: 1288px; background: videa ( display: block; idth: 1egs; height: auto; } #status { positian: absolute; top: 15px; left: 15px; backgroun c/styles 2/heads sbody> <dlv Id-status>Initializing...</divs <dfv id=video-container's <videa id=renotevidea autoplay playsinline muted>s/videns c/d1vx <script sre=*nain.js" deterss/seripts </bodyx </htnl> main.js Java5cript const statusElenent - document-getElementById(status'); let pc = null; let ws = null; function updatestatus(text){ statusElenent.textContent = text; console. log(text); function connect() { const roon = parans,get(roon); const signaler = params.get(*signaler') 11 *

127.8.9.1:8765';

if (1raon){ updatestatus("Error: No roon specified. Please add '7roon-your. return; {u00]$/[aneut}$//:5, = 1n sm 1500 updatestatus('Connerting ta signaling server: ${ws_url}); ws = new Mebsocket [ws_urtl); ws.onopen = () => { updatestatus("connected to signaling server, waiting for strcal }: ws.onmessage = asynr {event) => { const nessage = JsoN.parsc(event.data); } (,Jajja, == adf dps afessau yy dpsafessau) J! if (pc) ( console.warn(*Peer Connection already exists. Closing pc.close() : updatestatust"otter received, creating peer connection..." pc = new RTCPeerConnection({ iceServers: [{ urls: *stun:st pc.ontrack = (event) => { :(niueauns oapia futatasay. )snspisaiepdn VldeoELement.srcobject = event.streams[0]; ); pc.onicecandidate = e => { if (c,candidate) ( ws,send{JsoN.stringify({ *candidate ; pc-onconnectionstatechange =() => { if (pc){ updatestatus(*Media connection state: $fpc.connect.

aMait pc.setRemoteDescription(new RTcsessionDescription(ne const answer = await pc.createAnsuer(); aMait pc.setLocatDescription(ansver); ws.send(JsoN.stringify(f 'sdp': pe.localDescription >): updateStatus("Answer sent."); }else If (nessage.candidate &s pc){ aMait pc.addIceCandidate[new RTCIceCandidate[message.candi { }: ws.onclose =() => { updatestatus(*Disconnected from signaling server. Reconnecting if (pc) ( pc.close(); pc = mull; ) videoElenent.srcobiect - null; setTineout(connect, 500e); ws,onerror = (err) => { updatestatus(*websocket Error. Please check if the signaling 5 cunsale.error('wehsacket Error!', err); w.onlaad = connect;

4.3.Pc摄像头推流端(pc_camera_opencv.py)

Python Inport argparse onoufse liedu 1mport 1son inport logging 1nport cv2 fron av inport VideoFrame inport websackets fron alortc Inport RTCPeerConnection, RTCSessionDescription, MediaStr Logging.basicconfig(level=togging.INFo, fornat=4[asctime)s I (level class CamerastreanTrack(MediastreanTrack] : kind - "video* init_(self, device_index=e, width=l280, height=720, fps=36 def super()-_init() self .cap = cv2 . VideoCapture(device_index, cv2 CAP_DSHow) If not self,cap.isopened(): raise ConnectionError(f"无运打开摄像头。请橙查设备素引号[devic self .cap.Set (cv2 CAP_PROP_FRAME_HEIGHT, he1ght) self-cap-5et [cv2.CAP_PRoP_FPs, fps) h = int (self.cap .get(cv2.CAP_PROP_FRAME_HEIGHT)) logging.info(t"摄做头{device_index] 已打开.其际参数:{Mx<h} @ self.target_fps = fps self.franc_interval = 1.θ / sclf.target_fps self._start_time = time.tine() self._frane_count = async def recv(self): ret, frane = self.cap.read() if not ret: logging.error(摄像头读取灾数!) aMalt asyncio.sleep(self.frane_interval) return VideoFrame(width=int (self.cap-get [cv2.CAP_PRoP_FRAl current_tine = time,tine() pts = Int((current_tine - self._start_tine) * 1000) ncw_franc = videoFrane.from_ndarray(frane, format=bgr24) new_trane.pts = pts new_frane.tine_base = 1888 self._frane_count += 1 expected tine = self._start_time + self._frame_count α self.f slecp_duration = max(θ, cxpccted_tinc - time,time()) awalt asyncio.sleep(sLeep_duratlon) return new_frame def stop(self): super () -stop() if self.cap.isopened(): self.cap.release() logging.info(“摄像头已释放。“} async def run_streoner_session(pc, signaler_url, room_id, track): async def on iceconnectionstatechange(): logging.info(f"ICE 连接状态:(pc.iceCannectionState}") 1f pc.iceconnectionState 1n [falled*, disconnected, close if track: ws_url - f(signaler_url)/(roon_id) try: async with websockets.connect(ws_url) as vs: logging.info{f已连接到信令照务器,房间:“{room_1d}") pc. adeTrack(track) offer = amait pc.createoffer() aMalt pc.setLocalDescription(offer) aMaIt ws .send(json.dumps({"sdp*: {sdpa: pc. localDescript. logging.info{已发送 offer) async for msg in ws: nessage = json.loads (msg) if message.get(sdp", {}] -get(type") = ansuer: logging .info(u到 Answer) await pc.setRenoteDescription(RTCSessionDescripti elif “candidate" in message: pa55 eXcept (websockets exceptions .ConnectionCLosed, ConnectionRefused lagging.error[f"无法连接到信令服务器或连接中断:{e}) finally: logging.1nfo(“会括结票,正在清理所有阅源...) track.stop() if pc.connectionstate != closed: Mait pc.close() ureu". -- —oweu— ↓T parser = argparse.ArgumentParser(description=PC WebRTC OpenCV Cal parser.add_argument(--signaler", default=vs://127.8.8.1=8765, parser.add_argunent(--roon", default-"pc_can_test", help-"要加入的 parser.add_argunent(--device-index", type=int, default=0, help=" parser.add_argunent(--height, typc=int, default=72θ,help=视额 parser.add_argunent(--fps*, type=int。 default=38, help=花频畅率*) args = parser. parse_args() while True: track = None try: track = CameraStreanTrack(device_index=args device_index, pC = RTCPeerConnection(1 10 1 asyncio.run{run_streaner_sessian(pc, args signaler, args. CXcept KeyboardInterrupt: logging.info(“用户手动停止推流程序。") 104 if track: track.stop() 106 break except ConnectionError as e: logging.error(f"启动失败:{e}") 108 logging.info("5秒后重试...") time.sleep(5) 110 except Exception as e: logging.error(f"发生未知错误:{e}") if track: track.stop() 113 logging.warning("推流会话已中断,5秒后尝试重启...") 115 time.sleep(5)

5.运行

1.环境依赖:系统需预装Python3.8+,并已通过pip安装aiortc,websockets,

opencv-python 库。

2.启动信令服务器:在指定服务器部署signaling_server.py并执行python

signaling_server.py。

3.启动Web客户端:将web_client文件夹部署于HTTP服务器。本地测试可进入该目录后执

行 python -mhttp.server 80o0。

4.启动推流端:在推流设备上执行python pc_camera_opencv.py,并通过命令行参数指

定信令服务器地址、房间号及摄像头参数。

5.访问:在浏览器中打开Web客户端地址,并通过URL参数指定房间号和信令服务器地址。

http://127.0.0.1:8000/?room=my_r0om&amp;signaler=&lt;信令服务器IP&gt;:8765

原始排版图

本地摄像头实时推流至浏览器-基于WebRTC:微信公众号导出原始排版图