WebRTC瀏覽器播放實(shí)戰(zhàn)與避坑指南)
簡(jiǎn)介webrtc-streamer 是面向 WebRTC 實(shí)時(shí)音視頻開發(fā)者的開源流媒體服務(wù)器預(yù)編譯包適用于在線會(huì)議、遠(yuǎn)程教育、協(xié)同辦公等需要瀏覽器端低延遲音視頻傳輸?shù)膱?chǎng)景幫助開發(fā)者快速搭建可用的流媒體服務(wù)環(huán)境。壓縮包共 94 個(gè)文件約 7.29MB以 29 個(gè) js 腳本、23 個(gè) html 頁(yè)面、5 個(gè) css 樣式及 woff、svg、eot 等前端資源為主另含 wasm、tfjs、posenet、coco-ssd、blazeface、deeplab、body-pix 等模型文件以及 exe 可執(zhí)行程序、config.json 配置、Procfile 與說(shuō)明文檔覆蓋服務(wù)端運(yùn)行與瀏覽器端演示的完整結(jié)構(gòu)。已有 856 人學(xué)習(xí)下載。通過該包可了解 WebRTC 對(duì)等連接、信令交換、ICE/STUN/TURN 穿透與數(shù)據(jù)通道等核心機(jī)制并借助預(yù)置示例頁(yè)面與模型資源快速驗(yàn)證視頻流拉取、AI 視覺推理與前端交互效果為調(diào)試和優(yōu)化實(shí)時(shí)通信性能提供可復(fù)用的參考。1. 瀏覽器里跑起 RTSPwebrtc-streamer 到底解決了誰(shuí)的痛手頭有一批網(wǎng)絡(luò)攝像機(jī)RTSP 地址在 VLC 里播得好好的可一旦要嵌到瀏覽器頁(yè)面里就卡住了——Chrome 不支持 RTSPSafari 也不支持Flash 早就退役H5 的 video 標(biāo)簽只認(rèn) HTTP 漸進(jìn)下載和 HLS。這個(gè)場(chǎng)景下webrtc-streamer 就是那個(gè)把 RTSP 轉(zhuǎn)成 WebRTC 流、讓瀏覽器原生播放的中間件。它用 C 寫成內(nèi)部依賴 libwebrtc 做媒體協(xié)商對(duì)外暴露一個(gè)輕量 HTTP 服務(wù)前端引一個(gè) JS 文件就能把 RTSP 源拉進(jìn) video 標(biāo)簽。適合誰(shuí)做安防監(jiān)控面板、工業(yè)視覺看板、遠(yuǎn)程設(shè)備預(yù)覽的開發(fā)者尤其是那些不想在服務(wù)端做轉(zhuǎn)碼、又希望延遲壓到幾百毫秒以內(nèi)的場(chǎng)景。它不解決錄像存儲(chǔ)也不做 AI 分析只干一件事把 RTSP 流搬進(jìn)瀏覽器搬得穩(wěn)、搬得快。2. 拆開 webrtc-streamer信令、轉(zhuǎn)發(fā)與 ICE 的三段式結(jié)構(gòu)2.1 從 RTSP 到 WebRTC 的數(shù)據(jù)鏈路webrtc-streamer 的核心邏輯分三段。第一段是 RTSP 拉流進(jìn)程啟動(dòng)后按需連接攝像機(jī)用 FFmpeg 的 libavformat 解析 RTP 包拿到 H.264 或 H.265 裸流。第二段是媒體轉(zhuǎn)發(fā)把裸流喂給 libwebrtc 的 VideoTrack由它做 RTP 打包、時(shí)間戳對(duì)齊和 NACK/PLI 重傳。第三段是信令交換瀏覽器通過 WebSocket 發(fā) offerwebrtc-streamer 回 answer雙方走標(biāo)準(zhǔn) ICE 流程建立 PeerConnection。整個(gè)過程沒有轉(zhuǎn)碼H.264 進(jìn)來(lái)還是 H.264 出去CPU 占用主要花在 RTP 封包和 ICE 候選收集上。為什么選 WebRTC 而不是 HLSHLS 延遲通常在 5 到 15 秒切片越小延遲越低但請(qǐng)求數(shù)暴漲WebRTC 走 UDP配合 jitter buffer 能把延遲壓到 200 到 500 毫秒。代價(jià)是 WebRTC 對(duì)網(wǎng)絡(luò)抖動(dòng)更敏感丟包超過 10% 畫面就會(huì)花。常見做法是在內(nèi)網(wǎng)或?qū)>€環(huán)境用 WebRTC跨公網(wǎng)時(shí)配合 TURN 中繼但 TURN 會(huì)引入額外跳數(shù)延遲反而可能超過低延遲 HLS。所以選型前先問自己延遲要求到底是多少如果 3 秒能接受HLS 更省心如果必須 1 秒以內(nèi)WebRTC 是唯一選擇。2.2 編譯與啟動(dòng)參數(shù)怎么設(shè)才不翻車webrtc-streamer 官方提供預(yù)編譯二進(jìn)制但生產(chǎn)環(huán)境建議自己編因?yàn)槟J(rèn)編譯可能沒開 H.265 或沒帶特定硬件加速。編譯前先確認(rèn)系統(tǒng)有 CMake 3.10、GCC 7 和 libssl-dev。下面是我在 Ubuntu 20.04 上跑通的流程# 安裝依賴libssl 用于 WebSocket 的 wss 支持 sudo apt update sudo apt install -y cmake g libssl-dev libavcodec-dev libavformat-dev libavutil-dev # 拉源碼注意用 --recursive 拉子模塊libwebrtc 在子模塊里 git clone --recursive https://github.com/xxx/webrtc-streamer.git cd webrtc-streamer # 創(chuàng)建構(gòu)建目錄Release 模式開 -O2Debug 模式體積大且慢 mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease -DENABLE_H265ON make -j$(nproc)編譯完成后生成webrtc-streamer可執(zhí)行文件。啟動(dòng)時(shí)最關(guān)鍵的參數(shù)是-H指定 HTTP 監(jiān)聽端口-S指定 STUN 服務(wù)器地址。如果攝像機(jī)在同一個(gè)局域網(wǎng)STUN 可以填內(nèi)網(wǎng)地址跨網(wǎng)段則填公網(wǎng) STUN但注意不要用需要認(rèn)證的 STUN 服務(wù)webrtc-streamer 不支持帶憑據(jù)的 STUN。# 監(jiān)聽 8000 端口STUN 指向內(nèi)網(wǎng)一臺(tái)可達(dá)的服務(wù)器 ./webrtc-streamer -H 0.0.0.0:8000 -S stun:192.168.1.100:3478 # 如果攝像機(jī) RTSP 地址帶用戶名密碼在 URL 里直接寫 # rtsp://admin:pass123192.168.1.64:554/Streaming/Channels/101啟動(dòng)后訪問http://localhost:8000/webrtcstreamer.html?videortsp://...就能看到畫面。注意 URL 里的 RTSP 地址必須做 URL 編碼否則?和會(huì)被 HTTP 服務(wù)截?cái)?。我一般?Python 的urllib.parse.quote先處理一遍from urllib.parse import quote rtsp rtsp://admin:pass123192.168.1.64:554/Streaming/Channels/101 encoded quote(rtsp, safe) print(fhttp://localhost:8000/webrtcstreamer.html?video{encoded})參數(shù)說(shuō)明-H的0.0.0.0表示監(jiān)聽所有網(wǎng)卡生產(chǎn)環(huán)境建議綁具體 IP-S可以多次指定webrtc-streamer 會(huì)按順序嘗試-T指定 TURN 服務(wù)器格式是turn:user:passhost:port但 TURN 只在 ICE 直連失敗時(shí)啟用不要一上來(lái)就配 TURN否則所有流量都走中繼延遲反而高。2.3 前端集成video 標(biāo)簽與 JS API 的配合前端集成有兩種方式。最簡(jiǎn)單的是用官方提供的webrtcstreamer.js它封裝了 WebSocket 信令和 video 標(biāo)簽的綁定!DOCTYPE html html head script srchttp://localhost:8000/webrtcstreamer.js/script /head body video idvideo autoplay muted playsinline/video script // 實(shí)例化時(shí)傳入 webrtc-streamer 的服務(wù)地址 const webRtcServer new WebRtcStreamer(video, http://localhost:8000); // connect 方法觸發(fā)信令參數(shù)是 RTSP 地址和音頻開關(guān) webRtcServer.connect(rtsp://admin:pass123192.168.1.64:554/Streaming/Channels/101, null, stun:192.168.1.100:3478); /script /body /htmlconnect的第三個(gè)參數(shù)是 STUN 地址如果服務(wù)端啟動(dòng)時(shí)已經(jīng)指定了-S這里可以傳null。muted屬性必須加否則 Chrome 的自動(dòng)播放策略會(huì)攔截。playsinline在 iOS Safari 上必須加不然視頻會(huì)全屏播放。另一種方式是直接調(diào) WebSocket API適合需要自定義信令邏輯的場(chǎng)景。webrtc-streamer 的 WebSocket 端點(diǎn)默認(rèn)在ws://localhost:8000/ws消息格式是 JSON包含type、sdp、id等字段。自己實(shí)現(xiàn)的話需要處理 offer/answer 的 SDP 交換和 ICE 候選的 trickle。我一般不建議自己寫除非要集成到已有的信令框架里因?yàn)?libwebrtc 的 SDP 格式在不同版本間有差異webrtc-streamer 的 JS 庫(kù)已經(jīng)做了兼容。3. 避坑指南RTSP 地址編碼、H.265 兼容與 ICE 失敗排查3.1 坑一RTSP 地址里的特殊字符導(dǎo)致 404現(xiàn)象瀏覽器控制臺(tái)報(bào)Failed to load resource: the server responded with a status of 404但 RTSP 地址在 VLC 里能播。原因RTSP URL 里的?、、在 HTTP 查詢字符串里有特殊含義webrtc-streamer 的 HTTP 服務(wù)按?video后面的內(nèi)容解析如果 RTSP 地址本身帶?解析就會(huì)截?cái)唷=鉀Q對(duì) RTSP 地址做完整 URL 編碼quote(rtsp, safe)里的safe表示連/和:都編碼。編碼后的地址在服務(wù)端會(huì)自動(dòng)解碼不影響 RTSP 連接。3.2 坑二H.265 流在 Chrome 里黑屏現(xiàn)象webrtc-streamer 日志顯示流已連接但 Chrome 的 video 標(biāo)簽一直黑屏Safari 卻正常。原因Chrome 對(duì) H.265 的 WebRTC 支持不完整libwebrtc 默認(rèn)編譯沒開 H.265 的 RTP 打包。Safari 因?yàn)橄到y(tǒng)級(jí)支持 H.265 硬解所以能播。解決編譯時(shí)加-DENABLE_H265ON并且確認(rèn)攝像機(jī)輸出的是 H.265 的 baseline profile。如果攝像機(jī)只支持 main profileChrome 仍然可能解不了。最穩(wěn)的辦法是讓攝像機(jī)同時(shí)輸出 H.264 和 H.265 兩路流WebRTC 用 H.264錄像用 H.265。3.3 坑三ICE 候選收集失敗導(dǎo)致連接超時(shí)現(xiàn)象前端connect后一直停在connecting狀態(tài)webrtc-streamer 日志里沒有ICE connection state: connected。原因STUN 服務(wù)器不可達(dá)或者服務(wù)端和客戶端不在同一個(gè) NAT 后host 候選無(wú)法直連。解決先確認(rèn)-S指定的 STUN 地址能 ping 通。如果在內(nèi)網(wǎng)STUN 可以填服務(wù)端自己的 IPwebrtc-streamer 會(huì)返回 host 候選。跨公網(wǎng)時(shí)如果雙方都在對(duì)稱 NAT 后必須配 TURN。TURN 服務(wù)器可以用 coturn 自建配置里開lt-cred-mech和realmwebrtc-streamer 啟動(dòng)時(shí)用-T turn:user:passhost:port指定。3.4 坑四多路流并發(fā)時(shí) CPU 飆升現(xiàn)象單路流 CPU 占用 5%開到 10 路時(shí) CPU 跑到 80%畫面開始卡頓。原因webrtc-streamer 每路流獨(dú)立跑一個(gè) libwebrtc 的 PeerConnectionRTP 封包和 ICE ?;疃际菃尉€程。10 路流意味著 10 個(gè)線程在搶 CPU。解決一是開硬件加速編譯時(shí)加-DENABLE_VAAPION或-DENABLE_NVENCON讓 GPU 做 H.264 解碼和縮放二是限制單進(jìn)程的流數(shù)量超過 8 路就起多個(gè) webrtc-streamer 實(shí)例用 Nginx 做反向代理按路徑分流。我一般按每 6 路一個(gè)實(shí)例來(lái)部署留點(diǎn)余量給突發(fā)流量。3.5 坑五WebSocket 斷連后 video 標(biāo)簽不自動(dòng)重連現(xiàn)象網(wǎng)絡(luò)抖動(dòng)后 WebSocket 斷開video 標(biāo)簽停在最后一幀不會(huì)自動(dòng)恢復(fù)。原因webrtc-streamer 的 JS 庫(kù)沒有內(nèi)置重連邏輯connect只調(diào)一次。解決在oniceconnectionstatechange里監(jiān)聽disconnected和failed狀態(tài)手動(dòng)調(diào)webRtcServer.disconnect()再connect()。注意重連前要等 2 到 3 秒避免頻繁重連把服務(wù)端打掛。下面是我常用的重連封裝let retryCount 0; const maxRetry 5; function startStream() { const server new WebRtcStreamer(video, http://localhost:8000); server.connect(rtspUrl, null, stun:192.168.1.100:3478); // 監(jiān)聽 ICE 狀態(tài)失敗時(shí)觸發(fā)重連 const pc server.getPeerConnection(); pc.oniceconnectionstatechange () { if (pc.iceConnectionState failed || pc.iceConnectionState disconnected) { if (retryCount maxRetry) { retryCount; setTimeout(() { server.disconnect(); startStream(); }, 3000); } } }; }參數(shù)說(shuō)明maxRetry設(shè) 5 次超過后提示用戶手動(dòng)刷新setTimeout的 3000 毫秒是經(jīng)驗(yàn)值太短會(huì)導(dǎo)致服務(wù)端還沒釋放舊連接就建新連接太長(zhǎng)用戶等不及。4. 進(jìn)階用 API 動(dòng)態(tài)拉流與延遲實(shí)測(cè)4.1 通過 HTTP API 動(dòng)態(tài)添加和刪除流webrtc-streamer 除了 WebSocket 信令還暴露了一組 HTTP API可以在不重啟進(jìn)程的情況下動(dòng)態(tài)拉流。/api/add添加流/api/remove刪除流/api/list列出當(dāng)前所有流。這個(gè)在按需預(yù)覽的場(chǎng)景里很有用——用戶點(diǎn)哪個(gè)攝像機(jī)就拉哪路不點(diǎn)不拉省 CPU。# 添加一路流返回流的 id curl http://localhost:8000/api/add?urlrtsp://admin:pass123192.168.1.64:554/Streaming/Channels/101 # 列出所有流返回 JSON 數(shù)組 curl http://localhost:8000/api/list # 刪除指定 id 的流 curl http://localhost:8000/api/remove?id0注意/api/add的url參數(shù)同樣需要 URL 編碼。/api/list返回的 id 是自增整數(shù)刪除后不會(huì)復(fù)用。如果前端要維護(hù)流列表建議在添加時(shí)把返回的 id 存下來(lái)刪除時(shí)按 id 刪不要按 URL 刪因?yàn)橥粋€(gè) URL 可能被添加多次。4.2 延遲實(shí)測(cè)從攝像機(jī)到瀏覽器的端到端耗時(shí)延遲是 WebRTC 方案的核心指標(biāo)但很多人只測(cè)了“感覺”沒有量化。我一般用秒表法攝像機(jī)對(duì)著一個(gè)毫秒計(jì)時(shí)器瀏覽器截屏對(duì)比兩個(gè)時(shí)間差。更精確的做法是用getStats()拿 RTP 時(shí)間戳和本地渲染時(shí)間的差值。// 在 video 標(biāo)簽播放后調(diào)用獲取當(dāng)前流的統(tǒng)計(jì)信息 const pc webRtcServer.getPeerConnection(); pc.getStats().then(stats { stats.forEach(report { if (report.type inbound-rtp report.kind video) { // jitterBufferDelay 是抖動(dòng)緩沖引入的延遲單位秒 console.log(jitterBufferDelay:, report.jitterBufferDelay); // framesDecoded 和 framesReceived 的差值反映丟幀 console.log(framesDecoded:, report.framesDecoded); console.log(framesReceived:, report.framesReceived); } }); });實(shí)測(cè)數(shù)據(jù)參考內(nèi)網(wǎng)千兆環(huán)境H.264 1080p25fpsjitterBufferDelay 在 0.05 到 0.15 秒之間端到端延遲 200 到 350 毫秒??绻W(wǎng) 4G 環(huán)境jitterBufferDelay 漲到 0.3 到 0.8 秒端到端延遲 600 毫秒到 1.5 秒。如果 jitterBufferDelay 持續(xù)超過 1 秒說(shuō)明網(wǎng)絡(luò)抖動(dòng)嚴(yán)重要么加 TURN 中繼要么降碼率。4.3 一個(gè)容易被忽略的參數(shù)ICE 候選類型過濾webrtc-streamer 默認(rèn)會(huì)收集所有類型的 ICE 候選包括 host、srflx、relay。在純內(nèi)網(wǎng)環(huán)境host 候選就夠了srflx 和 relay 反而增加協(xié)商時(shí)間??梢栽趩?dòng)時(shí)加-i參數(shù)指定只收集 host 候選# 只收集 host 候選適合攝像機(jī)和服務(wù)端在同一局域網(wǎng) ./webrtc-streamer -H 0.0.0.0:8000 -i host如果服務(wù)端和客戶端跨網(wǎng)段但能直連用-i host,srflx。只有在必須走 TURN 時(shí)才加relay。這個(gè)參數(shù)能省 1 到 2 秒的 ICE 協(xié)商時(shí)間在按需拉流場(chǎng)景里體感很明顯。從那以后我每次部署 webrtc-streamer都強(qiáng)制走一遍“編碼 RTSP 地址 → 確認(rèn) ICE 候選類型 → 壓測(cè)并發(fā)路數(shù)”這三步少一步都可能在上線后翻車。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取