Berry AI 的得來速 AI 計時系統要判斷車輛的位置、數量、狀態與動線,這一切數據的源頭是門市車道上的 IP cameras (後面簡稱 IP cam) 所拍攝的影像。這些 IP cam 將畫面編碼成 H.264/H.265 之後以即時串流協定 (Real-Time Streaming Protocol, RTSP) 對外提供,只要將我們的 Vision AI 系統連上去,把每一幀 (frame) 送進 AI 模型,一條簡單的推理管線 (inference pipeline) 就成形了。
攝影機安裝好、串流接上、模型跑起來,事情看起來就結束了。直到第二個下游 (downstream) 應用出現…
第一個麻煩來自 IP cam 自己,它是一種嵌入式裝置,有限的運算資源大半都給了影像編碼,能同時服務的 RTSP 會話 (session) 數量往往只有個位數,各個品牌型號的上限不同,也不見得寫在規格書上。更討厭的是超過上限之後的故障症狀並不明確:可能是新的連線交握 (handshake) 失敗,也可能是既有的連線開始掉幀,造成 Vision AI 的準確度下降甚至停擺。
第二個麻煩來自下游應用的多樣性。Vision AI 運行在 edge server 上,與 IP cam 屬於同一個門市內網,直接讀 RTSP 沒有問題。但是營運端的 Web 後台要能預覽車道畫面、值班人員要在畫面上看到車輛排隊的情況、客服接到客訴時要調閱當時的錄影。這些應用都跑在瀏覽器裡,而瀏覽器並不支援 RTSP。
於是我們需要一個 media server,站在 IP cam 與多個下游應用之間,負責兩件事:一是分送 (fanout),對 IP cam 只維持一條連線,卻能同時服務多個下游;二是協定轉換,把同一份影像用各個下游支援的協定 (protocol) 送出去。

一條 RTSP 進來,三種協定出去,另外存一份錄影。
我們先來認識一下三個常見的影像串流協定,與它們分別適合的應用場景:
| 協定 | 典型延遲 | 瀏覽器支援 | 防火牆相容性 | CDN | 適用場景 |
|---|---|---|---|---|---|
| RTSP | 數百毫秒到 2 秒 | 不支援 | 差 | 不支援 | IP cam、伺服器端 AI 推理 |
| HLS | 2 到 5 秒 | 原生或 hls.js | 好 | 支援 | Web 後台、多人同時觀看 |
| WebRTC | 小於 500 毫秒 | 原生 | 需要 STUN 或 TURN | 不支援 | 值班監看、需要即時反應的操作 |
RTSP 由 RFC 2326 定義於 1998 年,2.0 版是 2016 年的 RFC 7826,預設 port 554。它是一個控制協定,DESCRIBE、SETUP、PLAY 這些方法只負責處理要傳什麼、怎麼傳,實際的媒體 (media,泛指 video 與 audio) 資料則是由 RTP 與 RTCP 承載。RTSP 是 IP cam 廣泛支援的協定,可惜的是瀏覽器並不支援。
HLS 是 Apple 在 2009 年發表的,2017 年以 RFC 8216 記錄成文件。作法是把串流切成一段一段的小檔案,再配一份 .m3u8 playlist 當索引,客戶端 (client) 先讀取 playlist 再依序讀取片段。全程都是走 HTTP,所以對防火牆與 CDN 都相當友善。代價在延遲上:切片本身就需要一些累積時間,早期的實作動輒十秒以上。Apple 在 2019 年 WWDC 推出的 Low-Latency HLS 改用小於一秒的 partial segment,把延遲壓進個位數秒,稍微緩解了延遲的問題。
WebRTC 在 2021 年 1 月成為 W3C 正式 Recommendation,媒體傳輸與交握全程加密,分別由 SRTP 與 DTLS 負責。瀏覽器原生支援,不用外掛,延遲是三者中最低的。代價有兩項。第一是連線建立變複雜:WebRTC 靠 ICE 這套機制去試出雙方之間走得通的路徑,而兩端如果各自躲在 NAT 後面 (Day 06 講過的那個問題),還得準備 STUN 伺服器幫它們查出自己的對外位址,最糟的情況得靠 TURN 在中間代轉整條流量。第二是可用的 codec 選擇最窄,RFC 7742 規定瀏覽器必須支援 VP8 與 H.264,RFC 7874 規定必須支援 Opus 與 G.711,超出這幾個就看各瀏覽器實作。
上一節那張表列出了三個協定各自的位置,但是沒有回答一件事:誰負責把一份串流變成這三種格式分送出去?這就是 MediaMTX 最擅長的事情。它能夠以多種協定主動讀取或被動接收串流,重新封裝 (remux) 之後,再用多種協定分送出去。目前支援的協定有 RTSP、HLS、WebRTC、RTMP、SRT、MPEG-TS、RTP 與 Media-over-QUIC,是非常強大的多進多出 media router!
**只需要維持一條上游 (upstream) 連線。**不論影像是 MediaMTX 主動去拉、還是由外部推送進來,它對上游的串流來源都只維持一條連線;下游應用從一個增加到十個,對來源端都沒有差別。
**任何協定都能處理。**把一支串流推進去之後,在下游的 Vision AI 用 RTSP 讀、Web 後台用 HLS 讀、值班畫面用 WebRTC 讀。要換一種協定,讀取端換個 URL 就好,十分方便。
**單一執行檔,零依賴。**MediaMTX 是用 Go 寫的,一個執行檔加一份 YAML 就能跑,不需要額外安裝任何套件,搭配 systemd 部署起來非常輕鬆。
**Control API 串接自動化。**在設定檔裡把 api 打開之後,:9997 上就有四十多個端點可以用,我們最常用到的是這三個:
GET /v3/paths/list # 有哪些 path (串流來源)、各有幾個 reader
POST /v3/config/paths/add/{name} # 偵測到新攝影機,新增 path
DELETE /v3/config/paths/delete/{name} # 攝影機送修,移除 path
這些變更都是即時生效的,不用重啟服務,既有的串流也不會被打斷。
內建錄影功能。record 打開之後 MediaMTX 會把串流寫成 fMP4 或 MPEG-TS 分段,並根據設定自動輪替 (rotate) 檔案,避免佔滿儲存空間。客服要調閱畫面時,也都找得到紀錄啦!
但是有一個限制:MediaMTX 不轉碼 (transcode)。它是 router,進來什麼 codec 出去就是什麼 codec。攝影機產生 H.265,下游收到的就是 H.265,而市面上的瀏覽器對 H.265 的支援程度遠不如 H.264 完整。要讓客戶端在各種環境之下都能順利播放的話,官方文件的建議是在上游先用 ffmpeg 轉成 H.264 的 baseline profile 加 Opus。使用 baseline 是因為它不會產生 B-frame,而最挑剔的 WebRTC 並不支援 B-frame。但是轉碼,尤其是軟體轉碼非常消耗計算資源,所以最好還是把上下游的 codec 搭配好,會比較輕鬆唷!
講了這麼多,不如直接跑一次。這個 lab 只需要一台筆電,macOS 與 Linux 都可以:把內建的 webcam 影像推送進 MediaMTX,再同時用三種串流協定讀取出來。
# macOS (Apple Silicon)
brew install ffmpeg jq
curl -sLO https://github.com/bluenviron/mediamtx/releases/download/v1.20.1/mediamtx_v1.20.1_darwin_arm64.tar.gz
tar xzf mediamtx_v1.20.1_darwin_arm64.tar.gz
# Linux (Debian / Ubuntu, x86_64)
sudo apt install -y ffmpeg jq v4l-utils
curl -sLO https://github.com/bluenviron/mediamtx/releases/download/v1.20.1/mediamtx_v1.20.1_linux_amd64.tar.gz
tar xzf mediamtx_v1.20.1_linux_amd64.tar.gz
解開來就是一個 mediamtx 執行檔加一份 mediamtx.yml,沒有安裝程序、沒有相依套件,非常 portable!
mediamtx.yml 有好幾百行,絕大多數是註解。這個 lab 只要動 api、metrics、record 這三個設定,它們預設都是關的,我們來把它們開啟:
# macOS (BSD sed)
sed -i '' -E 's/^( *)(api|metrics|record): (no|false)/\1\2: true/' mediamtx.yml
# Linux (GNU sed)
sed -i -E 's/^( *)(api|metrics|record): (no|false)/\1\2: true/' mediamtx.yml
檢查一下是否都已經正確開啟:
grep -E '^ *(api|metrics|record):' mediamtx.yml
應該要得到:
api: true
metrics: true
record: true
record前面那兩格縮排是有意義的。api與metrics是全域設定,寫在檔案的第一層;record則掛在pathDefaults:底下,它的身分是「每一支 path 的預設值」,之後可以針對個別的影像來源覆寫。
其他部分都不用改,因為 paths 區塊預設就掛著一條 all_others:,也就是任何的 path 都接收,所以待會直接往 /webcam 推流就行了。正式環境裡則會在 paths 底下明確定義每一個影像來源 (IP cam, webcam, 靜態檔案等等),例如把 source 指向 rtsp://IP位址:554 讓 MediaMTX 主動去讀取。這個 lab 是用 ffmpeg 推流,雖然一拉一推,但兩者在下游看起來是完全一樣的。
然後把它跑起來:
./mediamtx
macOS 的擷取介面是 avfoundation,先問它有哪些裝置可以用:
# macOS
ffmpeg -f avfoundation -list_devices true -i ""
[AVFoundation indev @ 0xa8d018140] AVFoundation video devices:
[AVFoundation indev @ 0xa8d018140] [0] MacBook Pro Camera
[AVFoundation indev @ 0xa8d018140] [1] MacBook Pro Desk View Camera
[AVFoundation indev @ 0xa8d018140] [2] Capture screen 0
[AVFoundation indev @ 0xa8d018140] [3] Capture screen 1
[AVFoundation indev @ 0xa8d018140] AVFoundation audio devices:
[AVFoundation indev @ 0xa8d018140] [0] MacBook Pro Microphone
第一次執行的時候 macOS 會跳出 webcam 權限的詢問,記得按允許,不然 ffmpeg 只會拿到一片黑畫面。
Linux 沒有 avfoundation,擷取介面是 v4l2,所以要改用剛才裝好的 v4l2-ctl (在 v4l-utils 套件裡) 列出裝置:
# Linux
v4l2-ctl --list-devices
Integrated Camera: Integrated C (usb-0000:00:14.0-8):
/dev/video0
/dev/video1
/dev/media0
同一支 webcam 通常會對到好幾個節點,其中只有一個真的能取得畫面,一般是排在最前面的那個 /dev/video0。
macOS 環境下使用 [0] MacBook Pro Camera 這個裝置,也就是設定 -i "0":
# macOS
ffmpeg -f avfoundation -framerate 30 -video_size 1280x720 -i "0" \
-c:v libx264 -preset ultrafast -tune zerolatency -pix_fmt yuv420p \
-f rtsp rtsp://localhost:8554/webcam
Linux 則是把 -f avfoundation 換成 -f v4l2,輸入從裝置編號換成裝置路徑 -i /dev/video0,其餘參數一模一樣:
# Linux
ffmpeg -f v4l2 -framerate 30 -video_size 1280x720 -i /dev/video0 \
-c:v libx264 -preset ultrafast -tune zerolatency -pix_fmt yuv420p \
-f rtsp rtsp://localhost:8554/webcam
-c:v libx264 這段不能省。webcam 送出來的通常是未壓縮或者 MJPEG 的格式,而前面說過 MediaMTX 不轉碼,所以編碼成 H.264 這件事必須在 ffmpeg 這一側完成。-preset ultrafast 與 -tune zerolatency 則是為了把編碼器自己造成的延遲壓到最低,畢竟這個 lab 的重點之一就是比較延遲。
現在 webcam 的影像已經推送到 MediaMTX 裡了,三種協定各開一個客戶端:
# RTSP,這就是 Vision AI 輸入協定
ffplay -fflags nobuffer -flags low_delay rtsp://localhost:8554/webcam
ffplay 預設會先緩衝一小段影像才開始播,所以要加上 -fflags nobuffer 與 -flags low_delay 把那段緩衝拿掉,看到的才是 RTSP 本身的延遲,而不是播放器的延遲。
瀏覽器再開兩個分頁,MediaMTX 內建了播放頁面,直接輸入網址就有畫面:
http://localhost:8888/webcam
http://localhost:8889/webcam
把這兩個分頁並排放好,然後對著 webcam 揮手。WebRTC 幾乎是即時的,HLS 則明顯落後一截,差距用肉眼就分辨得出來。前面那張延遲對照表,到這裡就從數字變成體感了。
用 API 來確認一件事:來源端是一條串流進來,下游有三個客戶端同時在讀。在 MediaMTX 的 API 與指標裡,這些客戶端叫做 reader。
curl -s localhost:9997/v3/paths/list \
| jq '.items[] | {name, source: .source.type, readers: [.readers[].type]}'
{
"name": "webcam",
"source": "rtspSession",
"readers": [
"hlsSession",
"rtspSession",
"webRTCSession"
]
}
source 只有一條,是 ffmpeg 推進來的 rtspSession;readers 有三筆,類型各不相同。來源端的連線數沒有增加,下游卻同時拿到了三種協定。換成正式環境的 source: rtsp:// 之後,省下來的就是 IP cam 的直接連線數。

ffmpeg 負責編碼,MediaMTX 負責分送,三種協定各對應一個 port 與一個客戶端。
門市與攝影機的數量往上長之後,「哪一台攝影機斷線了」這個問題就得靠監控回答。metrics 跟 api 一樣預設是關的,打開之後 MediaMTX 會在 :9998/metrics 提供 Prometheus 格式的輸出。Prometheus 出自 SoundCloud,2018 年從 CNCF 畢業,採用的是 pull model,由它自己定期上門抓資料。
完整的輸出有一百多行,每種協定的 session 都有自己的一組。/metrics 支援 ?type= 與 ?path= 查詢參數,先只看掛在 path 上的那一組:
curl -s 'localhost:9998/metrics?type=paths'
# Paths
paths{name="webcam",state="ready"} 1
paths_readers{name="webcam",readerType="hlsSession",state="ready"} 1
paths_readers{name="webcam",readerType="rtspSession",state="ready"} 1
paths_readers{name="webcam",readerType="webRTCSession",state="ready"} 1
paths_inbound_bytes{name="webcam",state="ready"} 123075477
paths_outbound_bytes{name="webcam",state="ready"} 70449969
paths_inbound_frames_in_error{name="webcam",state="ready"} 0
# Paths (deprecated)
paths_bytes_received{name="webcam",state="ready"} 123075477
paths_bytes_sent{name="webcam",state="ready"} 70449969
path 數量多起來之後,這個過濾功能可以讓 Prometheus 每次只抓取真正需要的部分。
存取權限則有一個預設值要留意:
authInternalUsers預設只允許來自127.0.0.1與::1的請求存取 API 與 metrics。在本機 curl 沒有問題,但是要讓另一台機器上的 Prometheus 過來抓,就得把那筆規則的ips放寬,或者改設一組帳號密碼。
這一組裡實務上最常用的是這幾項:
| 指標 (metric) | 用途 |
|---|---|
paths{name,state} |
這個 path 存不存在、狀態是 ready 還是還在等來源 |
paths_readers{name,state,readerType} |
有幾個客戶端在讀、分別是什麼類型 |
paths_inbound_bytes、paths_outbound_bytes |
進來與送出的位元組數 |
paths_inbound_frames_in_error |
收到的壞幀數量 |
最後那一項對我們的應用場景特別有價值。門市的網路品質參差不齊,當 IP cam 開始掉封包的時候,這個計數器就會往上跑,而且它比「模型的辨識率好像變差了」這種回報要早得多。
MediaMTX 沒有直接提供 bitrate 這種 metric,不過有 paths_inbound_bytes 這個持續累加的計數器 (counter),我們再用 PromQL 換算就可以了:
# 目前的入向 bitrate,單位 bit/s
rate(paths_inbound_bytes[1m]) * 8
同樣可以用 PromQL 撰寫告警規則:
# 攝影機沒有推流進來:五分鐘內完全沒有位元組
rate(paths_inbound_bytes[5m]) == 0
# 壞幀持續累積:門市網路或攝影機該檢查了
rate(paths_inbound_frames_in_error[5m]) > 0
# 沒有任何下游在取用:五分鐘內完全沒有送出位元組
rate(paths_outbound_bytes[5m]) == 0
另外,輸出裡還留著一批標示 deprecated 的舊名稱,例如
paths_bytes_received與paths_bytes_sent,數值跟paths_inbound_bytes、paths_outbound_bytes一模一樣。記得挑新的那組,免得哪天舊名稱被移除。
MediaMTX 目前有兩萬顆星、MIT 授權,v1.20.1 發布於 2026 年 8 月,更新相當勤快。一個執行檔加一份 YAML,分送、協定轉換、錄影與監控四件事全部包含在內,最強的 media server,不用嗎?
本系列由 Berry AI 工程團隊出品。更多工程實戰紀錄都在 Berry AI 技術部落格。