昨天介紹的是雲端上的看板:Pigeon 把 Superset 看板轉成 PDF 寄到客戶信箱。今天回到門市,介紹另一種看板:掛在廚房牆上的即時看板 (Drive-thru Real-time Dashboard)。它執行在 Day 10 介紹過的 kiosk 上:一台沒有鍵盤滑鼠的 edge server,以 Ubuntu Frame 加 Chromium 全螢幕顯示一個網頁,全年不關機。畫面一側是車道上每台車的等待時間,另一側是車道的即時影像。
這類裝置的難處在於無人看管。伺服器開著、容器都在執行、網頁也載入了,螢幕上的畫面是否正常,卻只有店裡的人知道。過去我們常在幾個小時後,才從客服收到一句「畫面卡住了」。Day 02 的一百條挑戰中寫過:「監控不能只問『還開機嗎』,還要問『資料新鮮嗎』」;到了看板這一層,問題更直接:畫面到底有沒有在動?
我們的做法是:在瀏覽器外部放一個獨立的監控程式,透過 Chrome DevTools Protocol 連進正在執行的 Chromium,把「瀏覽器實際畫出了什麼」轉成 Prometheus metrics,送上雲端,在 Grafana 上呈現。
最直覺的做法是在看板網頁裡放一段 JavaScript,定時把播放狀態送回後端。這個做法可行,但有三種狀況它涵蓋不到:
<video> 元素仍會回報它正在播放。Google 的《Site Reliability Engineering》(SRE book) 把監控分成兩種:白箱監控 (white-box monitoring) 依據系統內部暴露的指標,黑箱監控 (black-box monitoring) 則從使用者的角度檢查外部可見的行為。頁面內回報屬於白箱監控,我們缺的是黑箱監控:像店員一樣,站在螢幕前看畫面。
因此這個監控程式必須滿足兩個條件:一是它必須是瀏覽器以外的另一個 process,瀏覽器當掉時它仍在執行;二是它必須能直接「看」到畫面,例如截圖。
能同時滿足這兩個條件的是 Chrome DevTools Protocol (CDP)。按 F12 開啟的 DevTools 與瀏覽器之間就是透過這套協定溝通:JSON 訊息經 WebSocket 傳遞,規格公開,Puppeteer 與 Playwright 驅動 Chromium 時底層用的也是它。
Chromium 啟動時加上 --remote-debugging-port=9222,就會在這個 port 上開啟一個 HTTP 服務。GET /json/list 列出這個瀏覽器裡的分頁 (以及 worker 等其他 target) 與各自的 WebSocket 位址。連上之後,DevTools 能做的事都能透過程式執行:執行 JavaScript、查詢 DOM、截圖、讀取視窗狀態。
Chrome 136 起,
--remote-debugging-port必須搭配--user-data-dir指向非預設的資料夾才會生效,目的是防止惡意程式透過這個 port 讀取預設 profile 裡的 cookie。kiosk 原本就使用獨立的 user data directory,不受這項規則影響;在自己的筆電上實驗時則要留意。
Playwright 有一個方法叫 connect_over_cdp()。平常使用 Playwright 時由它自行啟動瀏覽器;這個方法則是連上一個已經在執行的 Chromium (僅支援 Chromium 系列瀏覽器),取得它現有的 context 與 page。看板的 Chromium 正是接著螢幕的那個瀏覽器,監控程式要檢查的就是它本身,因此直接連進去,不另外開啟新的瀏覽器。這也是它與昨天 Pigeon 最大的差別:Pigeon 自己開啟 headless 瀏覽器,需要什麼畫面就自己渲染;今天的監控程式沒有自己的瀏覽器,檢查的是 kiosk 上正在顯示的那一個。

最左邊的螢幕以虛線標示:HDMI 線後面發生的事,這套機制看不到。
連上之後要檢查什麼?我們把檢查分成六層,由外而內,越往內越不依賴瀏覽器自己的回報。

越左邊的檢查成本越低,越右邊越接近店員親眼看到的畫面。
第一層:瀏覽器在嗎。 檢查 CDP 能否連上,以及連上後能否找到看板網址所在的分頁。這是監控程式自己最外層的 up。它是 0 的時候,後面五層都不必看;而且這個 0 是監控程式明確回報的值,可以與「沒有資料」區分。
第二層:視窗對嗎。 CDP 的 Browser.getWindowForTarget 可以取得視窗的位置、大小與狀態。kiosk 的視窗應該始終是全螢幕並蓋滿整個螢幕;門市換了一台比例不同的螢幕,或視窗因故退回一般大小,這一層就會發現。
第三層:頁面活著嗎。 看板右上角有一個時鐘,前端每秒更新一次。監控程式讀出時鐘上的時間,與系統時間相減:差距在幾秒內,代表至少負責更新時鐘的那段 JavaScript 還在執行;差距持續擴大,代表頁面已經卡住,即使瀏覽器 process 還在。店員判斷系統是否正常,看的也是這個時鐘:只要它還在走,就視為系統還活著。看板的版本號也用同樣的方式讀取,各門市執行的版本可以直接在 Grafana 上查詢。
第四層:播放器停了嗎。 <video> 元素有三個標準屬性可以查詢:paused 表示播放器是否處於暫停狀態;readyState 表示已緩衝的資料是否足以播放,從 HAVE_NOTHING 到 HAVE_ENOUGH_DATA 共五級;networkState 表示媒體資源的載入狀態,共四種。其中 alert 最常使用的是 paused:一路影像連續幾分鐘都處於 paused,多半就是店員口中的「畫面卡住」。
第五層:封包進來了嗎。 播放器的 paused 是 false、也有資料,畫面就一定在動嗎?不一定。相機那端停止送出影像但連線仍在時,<video> 會停在最後一個影格,paused 卻仍是 false。因此監控程式會再確認新的影像資料是否持續進來:資料停止增加,就代表畫面沒有更新,無論 paused 的值為何。
第六層:畫面有東西嗎。 前五層都還是在詢問瀏覽器。最後一層直接檢查畫面:以 CDP 的 Page.captureScreenshot 截下畫面,找出影像所在的區塊,再以影像演算法計算其中的邊緣與細節量。全黑、全灰、單色的畫面數值接近 0,有內容的畫面則高出許多,門檻需要依實際畫面量測。這一層要發現的是「播放器回報正在播放,畫面卻是黑的」這類最難從內部察覺的狀況。
六層檢查的結果全部整理成 Prometheus metrics。Day 07 提過 Prometheus 採用 pull model,由它定期向目標抓取資料。因此監控程式不需要自己排程:它是一個小型 HTTP 服務,每次被 scrape 時執行一輪檢查,scrape 的頻率就是檢查的頻率。edge server 上的 Prometheus 以 agent 模式執行,只負責 scrape 與轉送,不在本機提供查詢與 alert:每分鐘向監控程式 scrape 一次,再以 remote write (Prometheus 把資料推送到遠端儲存的機制) 送到雲端。六層檢查依序執行,因為它們都與同一個頁面通訊,同時執行會互相干擾;每一層各有逾時設定,整輪必須在 Prometheus 的 scrape timeout 內完成。
輸出如下 (metric 名稱與數值為示意):
# HELP dashboard_video_paused Whether the video element is paused (1 = paused).
# TYPE dashboard_video_paused gauge
dashboard_video_paused{video="A"} 0
dashboard_video_paused{video="B"} 1
dashboard_video_paused_up 1
dashboard_video_frames_decoded_total{video="A"} 1827345
dashboard_video_frame_detail_score{video="B"} 2.3
dashboard_clock_lag_seconds 1
dashboard_browser_window_state{state="fullscreen"} 1
設計上有兩點需要說明。
每一層都有自己的 _up。 「檢查無法執行」與「檢查出來的值異常」是兩件事。paused 是 1 代表播放器停了;如果是監控程式找不到 <video> 元素,或 CDP 指令逾時,那是檢查本身失敗。這時我們讓 paused 維持上一次的值,並把 paused_up 設為 0。這與 Prometheus 為每個 scrape 目標附上 up 是同一個道理,只是往下多細分了一層。少了這個區分,alert 規則會把「監控壞了」誤判成「看板恢復正常」,或者反過來。
關店後 metrics 不刪除,改設為 -1。 門市打烊後,看板會切換到關店頁面,頁面上沒有串流,<video> 元素也隨之消失。最直覺的做法是一併移除這些影像的 metrics,但這會讓時序資料每晚出現空缺,每一條 PromQL (Prometheus 的查詢語言) 都要處理「沒有資料」的情況。我們選擇保留這些 label,把 gauge 的值設為 -1 (counter 不適用,累計值只能遞增),查詢端過濾掉負值即可。
metrics 經 remote write 送到雲端的 Mimir (Grafana Labs 開源的 Prometheus 長期儲存後端) 之後,我們在 Grafana 上建立看板並設定 alert:同一路影像連續 paused 超過數分鐘,就發送通知到 Slack。
「哪些店、幾點、哪一路影像、卡了多久」成為可查詢的時序資料之後,我們才第一次能回答「畫面凍結到底多常發生」,也才能在整個播放器替換之後,判斷它是否真的改善。
<video> 的狀態第一步,在自己的電腦上以獨立的 user data directory 啟動 Chrome 並開啟 remote debugging,再開啟任何一個含有 <video> 的頁面。以下使用 Day 07 lab 中 MediaMTX 內建的 WebRTC 播放頁,前提是 Day 07 的 MediaMTX 仍在執行、webcam 仍持續推流到 webcam 這個 path:
# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/cdp-lab \
http://localhost:8889/webcam
# Linux (Debian / Ubuntu, x86_64)
google-chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/cdp-lab \
http://localhost:8889/webcam
Linux 上安裝的若是 Chromium,把 google-chrome 換成 chromium。
第二步,從外部查看它。另開一個終端機:
curl -s localhost:9222/json/list
每個分頁一筆,只需要看這兩個欄位 (節錄):
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/8F3A...",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/8F3A..."
webSocketDebuggerUrl 供程式連線;devtoolsFrontendUrl 供人操作:把它接在 http://localhost:9222 後面,在另一個瀏覽器視窗開啟,就會得到一個完整的 DevTools 視窗,操作的對象卻是第一個視窗裡的那個分頁。在它的 Console 輸入 document.querySelector('video').paused,回傳 false;再輸入 document.querySelector('video').pause(),重新查詢就會變成 true,第一個視窗的影像也隨之停止。第四層檢查做的就是這件事。
第三步,改用程式查詢。Playwright 以 connect_over_cdp() 連上之後,平常的 Playwright 操作都能使用;截圖、視窗狀態這類 CDP 原生指令,則透過 new_cdp_session() 直接送出。監控程式的核心就是以下幾行 (示意,省略錯誤處理):
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 連上既有的瀏覽器
browser = p.chromium.connect_over_cdp("http://localhost:9222")
page = next(pg for ctx in browser.contexts for pg in ctx.pages if "webcam" in pg.url)
cdp = page.context.new_cdp_session(page)
# 第 4 層:<video> 的狀態
for video in page.query_selector_all("video"):
print(video.evaluate("v => ({paused: v.paused, readyState: v.readyState})"))
# 第 6 層:截圖
png_base64 = cdp.send("Page.captureScreenshot", {"format": "png"})["data"]
connect_over_cdp() 只需要以 pip 安裝的 playwright 套件,不需要執行 playwright install 下載瀏覽器,因為它不會自行啟動瀏覽器。把這段包成一個小型 HTTP 服務,收到 GET /metrics 就執行一輪檢查,再搭配 Prometheus 的 scrape 設定,就是我們門市裡那個監控程式的骨架。
可以再做一個實驗:用手遮住 webcam 的鏡頭。paused 仍是 false、readyState 多半仍是 4,播放器認為一切正常,截下來的畫面卻接近全黑,這正是第六層要發現的狀況。
監控程式要放在被監控對象的外部,這是既有的監控原則,我們把它應用在瀏覽器上。CDP 讓我們幾乎不必修改網頁,也不必在 kiosk 上額外安裝軟體,只要 Chromium 啟動時多帶一個參數,就能從另一個 process 檢查瀏覽器。六層檢查由外而內,從「連得上嗎」到「畫面有東西嗎」,每一層各有一個 _up,把「檢查失敗」與「被檢查的對象異常」分開。檢查結果送上雲端,在 Grafana 上呈現並設定 alert。
這套做法也有幾項限制。它只看得到瀏覽器內部的畫面,HDMI 線後面那台螢幕是否亮著、是否被店員關掉,這套機制無從得知。每分鐘一次的取樣,也難以發現幾秒鐘的卡頓。此外,監控程式本身也在同一台 edge server 上占用 CPU 與記憶體,每一層檢查都有成本,其中截圖最昂貴,這也是它排在最後一層的原因。
有了這些數據,我們才決定把播放器換掉。明天介紹我們如何把播放器從 RTMP + FLV 換成 WebRTC。
up metric)
本系列由 Berry AI 工程團隊出品。更多工程實戰紀錄都在 Berry AI 技術部落格。