iT邦幫忙

2026 iThome 鐵人賽

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

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

螢幕上真的有畫面嗎?使用 Chrome DevTools Protocol 確認門市看板的狀態

  • 分享至 

  • xImage
  •  

昨天介紹的是雲端上的看板: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,定時把播放狀態送回後端。這個做法可行,但有三種狀況它涵蓋不到:

  • 整個瀏覽器卡死。 記憶體不足或 renderer 當掉時,頁面裡的計時器跟著停止,回報也隨之中斷。從後端看,「網頁死了」和「網路斷了」無法區分。
  • JavaScript 認為一切正常,螢幕上卻不正常。 視窗從全螢幕退回一般大小、螢幕換成比例不同的型號、顯示卡負載過高導致畫面沒有更新,這些狀況網頁多半偵測不到,或不會視為異常回報;<video> 元素仍會回報它正在播放。
  • 播放器本身就是受檢對象。 明天會提到,我們的播放器裡原本就有一段「偵測凍結、自動重連」的邏輯。要檢驗這段邏輯是否正常運作,就不能再拿它自己的判斷當證據。

Google 的《Site Reliability Engineering》(SRE book) 把監控分成兩種:白箱監控 (white-box monitoring) 依據系統內部暴露的指標,黑箱監控 (black-box monitoring) 則從使用者的角度檢查外部可見的行為。頁面內回報屬於白箱監控,我們缺的是黑箱監控:像店員一樣,站在螢幕前看畫面。

因此這個監控程式必須滿足兩個條件:一是它必須是瀏覽器以外的另一個 process,瀏覽器當掉時它仍在執行;二是它必須能直接「看」到畫面,例如截圖。

從外部連進瀏覽器:Chrome DevTools Protocol

能同時滿足這兩個條件的是 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 上正在顯示的那一個。

監控架構:門市內網裡 Chromium 以 kiosk 全螢幕顯示看板網頁並輸出到螢幕,獨立的監控程式透過 CDP 的 WebSocket 連進 Chromium,Prometheus agent 每分鐘向監控程式 scrape 一次 /metrics 並 remote write 到雲端的 Mimir 與 Grafana,alert 送到 Slack,螢幕本身以虛線標示為監控看不到的部分

最左邊的螢幕以虛線標示:HDMI 線後面發生的事,這套機制看不到。

六層檢查:從「瀏覽器在嗎」到「畫面有東西嗎」

連上之後要檢查什麼?我們把檢查分成六層,由外而內,越往內越不依賴瀏覽器自己的回報。

六層檢查由左到右排成一列:瀏覽器在嗎、視窗對嗎、頁面活著嗎、播放器停了嗎、封包進來了嗎、畫面有東西嗎,下方一條箭頭標示從「相信瀏覽器怎麼說」漸進到「自己看螢幕上有什麼」,每層各有一個 _up 指標

越左邊的檢查成本越低,越右邊越接近店員親眼看到的畫面。

第一層:瀏覽器在嗎。 檢查 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,有內容的畫面則高出許多,門檻需要依實際畫面量測。這一層要發現的是「播放器回報正在播放,畫面卻是黑的」這類最難從內部察覺的狀況。

把檢查結果轉成 metrics

六層檢查的結果全部整理成 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 到 alert

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。

參考資料


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


上一篇
從 xlsx 樣板到 Superset 看板:營運報表如何轉成 PDF 寄到客戶信箱
下一篇
瀏覽器裡的即時車道影像:門市 dashboard 為什麼從 RTMP + HTTP-FLV 換成 WebRTC?
系列文
Berry AI:從零開始打造全美第一的得來速 Vision AI 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言