iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
佛心分享-IT 人技術創業

Berry AI:從零開始打造全美第一的得來速 Vision AI系列 第 24 篇

瀏覽器裡的即時車道影像:門市 dashboard 為什麼從 RTMP + HTTP-FLV 換成 WebRTC?

  • 分享至 

  • xImage
  •  

昨天講的是怎麼從瀏覽器外面監控店內的 dashboard (Drive-thru Real-time Dashboard),今天講它螢幕上那條即時影像是怎麼來的。Day 07 介紹過 MediaMTX,當時的重點是「一條 RTSP 進來,多種協定出去」,這條影像就是它的其中一個下游。Day 01 提過,這塊掛在廚房裡的螢幕,價值在於即時:店員把等待時間的數字和影像對照,判斷現在該先處理哪一台車。

影像一旦延遲十秒,數字與畫面就對不上了:螢幕上寫著 A 車已經等了 90 秒,影像裡的 A 車卻還沒開到窗口。所以這個播放器的需求從第一天就很明確:延遲要低到店員感覺不出畫面和數字有落差,而且要在一台沒有滑鼠鍵盤、沒有人看顧、還同時在執行 AI 推理的 edge server 上,全天候穩定播放。

Day 02 那一百條裡有一條寫過這段歷史:「店內看板要播即時影像,從 RTMP、HLS 一路遷到 WebRTC,每一代協定都有自己的延遲、相容性跟防火牆問題」。今天就把它展開來講。先講結論:我們拆掉了 RTSP 與瀏覽器之間所有的中間層,現在瀏覽器直接向 MediaMTX 要 WebRTC 串流,追回落後的播放進度、處理掉幀這些事,全部交還給瀏覽器內建的機制。

瀏覽器不支援 RTSP,中間要換成什麼協定?

Day 07 提過,IP cam 送出的是 RTSP,而瀏覽器不支援 RTSP,中間一定要有元件轉換協定;那張表也比較過 HLS 與 WebRTC 的延遲和瀏覽器支援度。這裡只看 2022 年 dashboard 第一版上線時,瀏覽器播得動的三個選項,另外多問一件事:影像的緩衝與追趕由誰負責。

協定 瀏覽器怎麼播 延遲從哪裡來 誰負責緩衝與追趕
HLS 原生或 hls.js 切 segment 的時間,加上播放進度落後 live edge (最新的直播進度) 的時間,傳統 HLS 通常是三個 segment 長度 播放器,依 playlist 逐段下載
HTTP-FLV flv.js 之類的 JavaScript 函式庫 MSE (Media Source Extensions) buffer 的累積 播放器,追趕邏輯要自己寫
WebRTC 原生 RTCPeerConnection 瀏覽器的 jitter buffer (吸收封包到達時間抖動的緩衝區),動態調整 瀏覽器內建

我們最先試的是 HLS,只用了幾天就放棄。我們當時用的是傳統 HLS:它把影像切成一段一段的檔案,播放器要等 segment 寫完、再讀 playlist、再下載,延遲天生是「數個 segment 長度」起跳。把 segment 切到一秒、playlist 只留三段,還是有好幾秒的延遲,這對 Web 後台預覽夠用,對廚房裡的 dashboard 不夠。

WebRTC 在當時的評估裡是「延遲最低、但最陌生」的選項:需要 signaling (交換連線參數的機制)、ICE、DTLS 一整套流程,而 MediaMTX 的前身 rtsp-simple-server 要到 2022 年 12 月的 v0.21.0 才支援 WebRTC 讀取,當時走 WebRTC 得另外架設一套專門的伺服器。於是第一代選了中間那條路。

第一代:RTSP → RTMP → HTTP-FLV

RTMP (Real-Time Messaging Protocol) 是 Macromedia 在 Flash 時代設計的串流協定,預設 port 1935,後來由 Adobe 公開規格。它在直播產業沿用多年,OBS (開源的直播推流軟體) 把直播送上 YouTube、Twitch,至今仍以 RTMP 為主。但 Flash Player 在 2020 年底正式退役之後,沒有任何瀏覽器能直接播 RTMP。

直播產業的解法是 HTTP-FLV:伺服器把透過 RTMP 收到的影音,原封不動裝進 FLV 容器,用一條 HTTP 長連線持續送給瀏覽器;瀏覽器端則用 Bilibili 在 2016 年開源的 flv.js 把 FLV 拆開 (demux)、重新封裝 (remux) 成 fragmented MP4,再透過 MSE 餵給 <video> 元素播放。整個串流架構如下:

上半部是第一代串流架構:IP cam 的 RTSP 進 MediaMTX,ffmpeg 從 MediaMTX 讀取 RTSP 串流,重新封裝成 RTMP 後推給 NGINX,NGINX 以 HTTP-FLV 送給瀏覽器,flv.js 在瀏覽器內完成 demux,資料經 MSE 緩衝後交給 video 元素播放;下半部是第二代串流架構:IP cam 的 RTSP 進 MediaMTX,瀏覽器以 WHEP 交換 SDP 後直接經 SRTP 收到 WebRTC 串流,中間不再有 ffmpeg 與 NGINX

上半部是第一代,下半部是第二代。方塊數量的差距,就是我們拆掉的中間層。

在 edge server 上,我們讓 ffmpeg 從 MediaMTX 讀 RTSP,不轉碼、只重新封裝 (remux) 成 RTMP,推給同一台機器上的 NGINX;NGINX 載入了一個串流模組,同時負責接收 RTMP 串流與送出 HTTP-FLV;dashboard 網頁再用 flv.js 從 NGINX 拉 HTTP-FLV。延遲勉強落在可接受的範圍。

這套架構運作了好幾年,問題也在這幾年裡慢慢浮現。

延遲會累積,追趕要自己寫。 MSE 的設計是「把資料餵進 buffer,瀏覽器自己排程播放」,它是為點播與 HLS 這種可以稍微落後的情境設計的。網路短暫抖動、機器短暫忙碌,buffer 裡就多累積了幾百毫秒的影像,播放進度也不會自動追上。所以播放器得定時量 buffered.end 與 currentTime 的差距,落後超過幾秒就把 playbackRate 調快追上,落後太多就直接重建播放器。這段邏輯我們改了很多版,每一版都是在跟延遲拉鋸。

凍結沒有任何訊號。 HTTP-FLV 就是一條 HTTP 連線,連線還在、影像卻停了,瀏覽器不會跳任何錯誤。要判斷畫面是不是凍結,得自己數解碼的幀數有沒有增加、buffer 有沒有往前,再決定要不要重連。在 edge server 負載高的時候 (它同時在執行 Vision AI 推理),這種凍結特別常見,最糟的情況會一直卡到隔天重啟瀏覽器才恢復。Day 23 那套監控的 alert 條件「同一路影像連續 paused 數分鐘」,就是為這種凍結設的,但監控只能發現問題,解決不了播放器本身的缺陷。

函式庫停止維護。 flv.js 的最後一版是 v1.6.2,發布於 2021 年 9 月,README 明白寫著 this project will become rarely maintained,並建議改用 mpegts.js。它的 demux 與 remux 全在 JavaScript 裡做,CPU 用量不低,我們也遇過記憶體洩漏。門市的 edge server 沒有網際網路,所以這個函式庫還得跟著 dashboard 一起打包進去,版本一鎖就是好幾年。

元件太多。 一路影像就是一支 ffmpeg process,它的生命週期要另外管理:沒人在看 dashboard 時就關掉 ffmpeg 省資源、重新打開 dashboard 時再啟動、ffmpeg 掛了就重啟。加上 NGINX 和它的串流模組,每一個環節都是獨立的故障點,出事時要查的 log 也分散在三個地方。

這四個問題有同一個根源:瀏覽器本來就內建的功能,我們用多個自行組裝的元件去模擬。

第二代:讓瀏覽器直接使用 WebRTC

Day 07 介紹 WebRTC 時只用了一段:瀏覽器原生支援、全程加密、延遲最低,代價是建立連線的流程比較複雜。這裡把它攤開來講,第二代能把中間層全部拿掉,靠的就是這個「複雜」。

WebRTC 是什麼

WebRTC (Web Real-Time Communication) 是一組瀏覽器內建的 API,加上一組 IETF 定義的協定,標準化歷程與 codec 限制 Day 07 講過,不再重複。這裡要補的是它的出身:視訊通話,延遲以毫秒計、雙向、點對點。我們的用法只是把「另一個瀏覽器」換成 MediaMTX,而且瀏覽器只收不送,整套協定一個都沒少。

這些協定由上到下的分層如下:

層 負責的事 規範
signaling 交換連線描述。WebRTC 刻意不規定怎麼交換,WHIP 與 WHEP 後來補上這一塊 RFC 9725、WHEP draft
SDP 用 offer 與 answer 描述 codec、收送方向、ICE 憑證、DTLS 指紋 RFC 3264、RFC 8829
ICE 列出所有可能的位址,測出走得通的那一條 RFC 8445、RFC 8489、RFC 8656
DTLS-SRTP 透過 DTLS 交握協商金鑰,SRTP 加密媒體,無法關閉 RFC 5764、RFC 3711
RTP/RTCP 媒體封包走 RTP,接收端用 RTCP 回報 packet loss、要求 keyframe (關鍵影格) RFC 3550、RFC 4585

逐層來看:

  • SDP (Session Description Protocol):雙方交換的「我有什麼、我要什麼」。一方送 offer、另一方回 answer,裡面寫著支援哪些 codec、這一路是只收還是只送、ICE 要用的帳號密碼、DTLS 憑證的指紋。dashboard 送出的 offer 只宣告一路 recvonly 的視訊。
  • ICE (Interactive Connectivity Establishment):找出兩端之間走得通的路徑。雙方各自列出所有可能的位址 (candidate):主機自己的 IP 是 host candidate,向 STUN 伺服器問到的對外位址是 server reflexive candidate,TURN 中繼站給的是 relay candidate。接著依優先順序兩兩配對測試,由 controlling agent (通常是送 offer 的那一端) 從能連通的配對裡選一組來用。在同一個內網裡,host candidate 就直接通了;隔著 NAT 才需要 STUN,最糟的情況才由 TURN 中繼全部流量。
  • DTLS 與 SRTP:路通了之後先跑一次 DTLS 交握 (handshake),透過這次交握協商出 SRTP 的金鑰,之後所有媒體封包都用 SRTP 加密。這一層沒有開關,WebRTC 不允許用明文傳送媒體。
  • RTP/RTCP 與 jitter buffer:媒體封包走 RTP over UDP,接收端有一個瀏覽器內建的 jitter buffer,深度依網路 jitter 動態調整,通常只有幾十到幾百毫秒。封包掉了,接收端用 RTCP 送 NACK 請對方補傳;補不回來或來不及,就送 PLI (Picture Loss Indication) 請對方給一張新的 keyframe 重新對齊。關鍵是播放時鐘不會為了等封包而停下來:掉一個封包影響的只有那一幀和參考它的後續幾幀,延遲靠丟幀與動態調整緩衝來控制,代價是畫面偶爾會短暫破圖或跳格。解碼負載或上游的 RTSP 緩衝仍可能拉高整體延遲,瀏覽器管得到的只有自己這一段,但它不會把落後的部分一路堆在 buffer 裡。這正是第一代要自己寫的那段追趕邏輯,而瀏覽器的實作比我們的成熟。

為什麼延遲天生比較低

我們用過的 HLS (hls.js) 與 HTTP-FLV (flv.js) 都走 HTTP over TCP、都經過 MSE。TCP 保證順序:一個封包掉了,後面所有已經到的資料都要排隊等它重傳,這段時間播放器只能消耗 buffer 裡已有的資料;資料夠的話畫面不會停,耗盡就停頓,而停頓過的那一段從此留在 buffer 裡,除非播放器主動加速或跳過。這就是第一代「延遲會累積」的機制來源,MSE 本身沒有任何自動追上即時進度的設計。WebRTC 走 UDP,一個封包掉了不會拖住整條連線,上一節講過的 NACK 與 PLI 負責補救,對車道監看來說,偶爾的破圖換來不累積的延遲,這樣的取捨是值得的。

B-frame 被排除的理由也在這裡。B-frame (雙向預測幀) 要參考它後面的幀才能解碼,解碼器得先等未來的幀到齊,等於內建了一段固定延遲,與「收到就播」的設計直接衝突。WebRTC 規定瀏覽器必須支援的 H.264 profile (RFC 7742) 是不含 B-frame 的 Constrained Baseline,而 MediaMTX 又不轉碼,IP cam 送什麼、瀏覽器就收什麼。所以 Day 10 那一步「把相機主串流從 H.265 改成 H.264」在這裡是必要條件,而且只改編碼格式還不夠:Main 或 High profile 的 H.264 一樣可能帶 B-frame,相機那端得選不帶 B-frame 的設定。在 dashboard 用的 Chromium 上,這兩件事少做一件,畫面就是一片黑。

WebRTC 還有一個從一開始就存在的缺口:標準刻意不規定 signaling,也就是 offer 與 answer 要透過什麼方式交換。視訊通話軟體各自用 WebSocket 或自訂協定實作,串流伺服器也各有各的做法,播放器與伺服器之間沒有互通性可言。

WHEP:把 signaling 變成一個 HTTP POST

IETF 的 WISH (WebRTC Ingest Signaling over HTTPS) 工作小組把這件事標準化了:推送 (publish) 用 WHIP (WebRTC-HTTP Ingestion Protocol),2025 年成為 RFC 9725;播放 (play) 用 WHEP (WebRTC-HTTP Egress Protocol),目前仍是 Internet-Draft,但 MediaMTX、Cloudflare 等主流實作都已支援。WHEP 的流程簡單到可以用一段動畫講完:

WHEP 流程動畫:瀏覽器以 HTTP POST 把 SDP offer 送到 MediaMTX 的 whep endpoint,MediaMTX 回 201 Created、Location 標頭與 SDP answer,雙方完成 ICE 連線檢查與 DTLS 交握後,MediaMTX 以 SRTP 持續送出媒體封包,結束時瀏覽器送 DELETE

一個 POST 換 SDP,之後全是瀏覽器內建的流程。

  1. 瀏覽器建立 RTCPeerConnection,宣告只收不送 (recvonly),用 createOffer() 產生 SDP offer,再呼叫 setLocalDescription(offer) 把它設成本地端的描述。ICE candidate 可以等收集完一起送,這時 POST 的內容要拿 pc.localDescription.sdp,createOffer() 最初回傳的那份不會自動補進後來的 candidate;也可以先送出 offer,之後用 PATCH 補送。
  2. 用 HTTP POST 把 offer 送到 /{path}/whep,Content-Type 是 application/sdp。
  3. 伺服器回 201 Created,body 是 SDP answer,Location 標頭是這個 session 的資源位址。
  4. 瀏覽器呼叫 setRemoteDescription(answer),ontrack 事件會提供遠端的 track,接到 <video> 上;接下來 ICE、DTLS、SRTP 全由瀏覽器接手,等連線建立、封包進來,畫面才會開始播放。
  5. 要結束就對 Location 送 DELETE。

對照第一代的 ffmpeg process 管理、RTMP 模組、flv.js 與追趕邏輯,第二代的「播放器」只剩下這幾步,而且每一步都是瀏覽器的標準 API。想實際觀察的話,在 Day 07 的 lab 打開 :8889 的播放頁,再開 DevTools 的 Network 分頁:一個 OPTIONS、一個回 201 的 POST、幾個補送 ICE candidate 的 PATCH,就是上面那段動畫的前兩步;之後 Network 分頁不再有新請求,因為媒體封包走 UDP,不再經過 HTTP。

MediaMTX 原本就在 edge server 上

Day 07 講過,我們的 edge server 上本來就執行著 MediaMTX,由它與 IP cam 維持唯一一條 RTSP 連線,Vision AI 從它讀 RTSP,Day 10 的 Camera Grid 從它讀 HLS。而 MediaMTX 的 WebRTC 伺服器預設就是開著的:port 8889 上,http://<host>:8889/<path> 是內建的播放頁,http://<host>:8889/<path>/whep 就是 WHEP 的 endpoint。也就是說,第二代在伺服器端的改動幾乎是零,只是讓 dashboard 換一個 port、換一種協定去讀同一條 path。ffmpeg process、NGINX 和它的串流模組,全部移除。

播放器換完、全面上線之後,最直接的改變有三個:

  • 延遲明顯下降,上線後也沒再遇到越拖越長的問題。 jitter buffer 是瀏覽器的事,追趕邏輯整段刪掉。
  • 斷線與凍結分得開了。 RTCPeerConnection 有 connectionState 與 iceConnectionState,連線斷了會變成 disconnected 或 failed,這部分是明確的狀態機,重連邏輯寫的是「狀態變成 failed 就重建連線」。至於相機不送影像、連線卻還活著的凍結,仍然要看 getStats() 裡 framesDecoded 這類計數器有沒有停住。差別在於這些是標準 API 提供的計數器,第一代只能從 buffer 的變化推測。Day 23 第五層確認影像資料是否持續進來,看的就是這類計數器。
  • CPU 使用率降下來了。 demux 與 remux 不再由 JavaScript 做,edge server 上也少了每路一支的 ffmpeg process。解碼一直都是瀏覽器自己在做,這點沒變,不過我們也在同一時期把 Chromium 換成能用硬體解碼的版本,兩件事加起來,dashboard 占用的 edge server 資源少了不少。

換成 WebRTC 之後要注意的幾件事

內網不需要 STUN 與 TURN。 dashboard 與 MediaMTX 在同一個門市內網,host candidate 就直接連通,iceServers 可以是空陣列,不用架任何額外的伺服器。反過來說,如果哪天要從門市外面看這條串流,Day 06 那個 NAT 問題會再次出現:MediaMTX 預設只把自己網卡上的 IP 送給客戶端,到不了的話要用 webrtcAdditionalHosts 補上對外的 IP 或網域名稱,UDP 被擋則靠 webrtcICEServers2 加上 STUN 或 TURN。

一對多是有成本的。 HLS 分送的是靜態檔案,一百個人看也只是多一百個 HTTP 下載,還能丟給 CDN;WebRTC 則是每個觀看者各一組獨立的 PeerConnection,各自維護 ICE 與 DTLS 狀態、各自加密一份 SRTP,伺服器的 CPU 與記憶體用量隨觀看人數線性增加。dashboard 一間店只有一台,這對我們不是問題;但要開放很多人同時看,就得回頭考慮 HLS,Day 07 那張表的「適用場景」欄講的就是這件事。

kiosk 的既有限制沒變。 Day 10 提過 Chromium 對自動播放的限制,我們的 kiosk 靠的是靜音自動播放,<video> 上的 muted 與 autoplay 換成 WebRTC 之後一樣缺一不可。另一個沒變的是門市沒有網際網路,但這次反而變成優點:WebRTC 是瀏覽器內建的,第一代得跟著 dashboard 一起打包的 flv.js,到了第二代就不再需要了。

小結

回頭看,第一代不算選錯,它是那個時間點能穩定上線的方案:RTMP 與 HTTP-FLV 有成熟的伺服器與播放器,我們用一支 ffmpeg、一個 NGINX 串流模組、一個 JavaScript 函式庫把它們接起來,換到勉強可接受的延遲。代價是追播放進度、偵測凍結、啟動與關閉 ffmpeg process 的邏輯全部要自己寫,而且每一個自己拼的零件都會老化。第二代做的事情只有一件:把「即時」交還給瀏覽器內建的 WebRTC,用 WHEP 讓 signaling 縮成一個 HTTP POST,用早就在那裡的 MediaMTX 省掉所有中介元件。

它當然也有限制。WebRTC 對 codec 挑剔,相機端的設定必須配合;每一路影像都是一條有狀態的連線,重連邏輯省不掉;跨出門市內網就得面對 NAT 與防火牆;WHEP 至今仍是 draft,各家實作的細節仍可能有出入。

到這裡,Software Team 的部分講完了:從內部工具、BI Dashboard、Data Pipelines、報表寄送、dashboard 監控,到今天的即時影像。這些系統在上千間門市裡每天執行,會不會一直穩定地執行下去,就不是單一團隊能回答的問題了。明天開始,換 SRE 上場。

參考資料


本系列由 Berry AI 工程團隊出品。更多工程實戰紀錄都在 Berry AI 技術部落格。


上一篇
螢幕上真的有畫面嗎?使用 Chrome DevTools Protocol 確認門市看板的狀態
下一篇
上萬台實體設備的可靠性:Berry AI 的 SRE 有什麼與眾不同之處?
系列文
Berry AI:從零開始打造全美第一的得來速 Vision AI 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言