數位看板 — 顯示器 + 廣告
場館中的螢幕不是網頁。它會在一臺廉價的電視棒上無人看管地運行數月,沒有人盯著主控臺,故障模式就是一個凍結的畫面,可能一週都沒人注意到。以下每一個設計決策都是為了在這種環境下存活。
這個技能包含一個完整的模組:媒體和播放清單的管理 CRUD、顯示器註冊、PIN 碼和佈建 URL 配對、全螢幕播放器,以及讓它可管理化的運作層(健康檢查、預覽、遠端控制)。
使用時機
在建置或擴充在實體空間中循環播放媒體的螢幕時使用,基於 Next.js App Router 搭配 Firestore 或 Supabase/Postgres。
不該使用時機
- 網路廣告投放(曝光數、競價、追蹤像素、第三方標籤)— 這是不同的問題,需要不同的基礎設施。
- 互動式資訊站,使用者點擊進行交易。看板是單向的。
- 行銷頁面上的單一內嵌影片。使用
<video>標籤即可。 - 假設已有通用的 Next.js、CMS 或身份驗證設定。
架構
管理後台 (瀏覽器) 伺服器 裝置 (電視)
───────────────── ────── ───────────
媒體上傳 ──────────────► 物件儲存 ◄──── 公開/簽署 URL ──────┐
廣告 CRUD ─────────┐ │
播放清單編輯 ─────┼──► /api/admin/* ──► 廣告 + 顯示器 ──┐ │
發出指令 ────────┘ (員工認證) (範圍限定) │ │
▼ │
配對螢幕 ◄──── PIN ────────────► /api/display/pair │ │
產生每個顯示器的 │ │
專屬 Token ▼ │
/api/display/playlist ─── poll ───┤
▲ 解析 adIds → 廣告 │
└── 上傳遙測資料, │
下傳指令 播放器迴圈
一種請求類型支撐了整個運行時期:裝置輪詢 /api/display/playlist,上傳遙測資料並接收內容和指令。所有運作都依賴這個通道。
重要事實 — 開始設計前請先閱讀
- 播放清單是顯示器上的一個有序陣列,而非獨立實體。
display.adIds: string[]就是播放清單 — 順序是自由的,不需要聯結,一次讀取。只有在相同內容需要在多個螢幕上執行時,才引入獨立的播放清單實體;請參閱 data-model.md 了解權衡取捨,以及 extensions.md 了解遷移方式。 - 使用輪詢,不要串流。 30–60 秒的輪詢成本更低,能應對休眠的網路堆疊,且會自動重新連線。即時更新是選配的升級功能,並不是基本標配。使用 ETag 快取回應,否則每個螢幕每次輪詢都要支付一次完整讀取的成本。
- 媒體上傳直接從瀏覽器到儲存空間,完全不經過 API 路由。路由處理器有主體大小限制,且會消耗運算資源來代理傳輸位元組。
- 裝置是不可信任且無人看管的。 它會在
localStorage中保存長期有效的憑證,而任何人都可以走到硬體前操作。該憑證必須是每個顯示器專屬且可撤銷的。 - 播放器絕不能停止。 每個媒體元素都必須有超時設定、錯誤處理器和跳過機制。損壞的素材要能繼續播放,不能卡住。
嚴格規則
絕對不要根據渲染狀態來決定輪詢間隔。 在穩定的間隔進行輪詢,並從 ref 讀取幻燈片狀態。一個依賴幻燈片索引的計時器會在每次切換幻燈片時被銷毀並重新建立,因此若重新整理的間隔比幻燈片顯示時間還長,這個計時器就永遠不會觸發;行為契約將輪詢的時間點固定在標準時鐘時間上。
絕對不要用一個布林值同時代表「暫停」和「已刪除」。 使用
active來表示操作者意圖,並使用獨立的deletedAt來表示生命週期,否則已刪除的項目會在使用者將其重新開啟時再次出現。
絕對不要信任客戶端提供的租戶/位置範圍。 在伺服器端從已驗證的工作人員 session 或 Token 解析到的顯示器資料列中推導出來,並在每個
[id]路由上強制執行 — 回傳 404,而不是 403,這樣就無法探測 ID。
絕對不要為每個螢幕發行一個共用 Token。 在配對時為每個顯示器產生專屬 Token,只儲存其雜湊值,並且只需更新一個資料列即可撤銷。
絕對不要在
onLoad之前啟動圖片顯示時間的計時器。 在速度較慢的電視上,幻燈片會在可見之前就過期。
絕對不要在一次性指令中攜帶持久化的螢幕狀態。 「空白」和「接管」模式必須能撐過裝置的每日重新載入和電源循環 — 將它們作為每個輪詢回應中的狀態(
mode)傳遞,並將確認清除的指令通道保留給真正的一次性指令,例如重新載入。任何效果是重新載入的指令,必須在重新載入之前先持久化其確認狀態,否則伺服器會永遠重新傳送該指令。
快速入門
- 為你的應用程式填寫銜接合約,並確認領域重新命名 — adaptation.md。
- 建立實體模型,並選擇陣列或 junction 表 — data-model.md。
- 建立資料表/集合、索引、安全規則和媒體儲存桶 — firestore-backend.md 或 supabase-backend.md。
- 建置裝置和管理端點,包含配對和 Token 驗證 — api-routes.md。
- 配對一個螢幕 — pairing.md — 然後加入播放器迴圈 — player-runtime.md。
- 建置後台介面:媒體庫、顯示器列表、播放清單編輯器 — admin-ui.md。
- 在正式上線前加入健康檢查、預覽和遠端控制 — operations.md。
- 對照 player-runtime.md 中的行為契約表進行驗證 — 拔掉網路線、在循環播放中刪除目前播放的廣告、發出重新載入指令 — 並將 player-machine 測試作為回歸覆蓋。
參考目錄
載入與觸發關鍵字匹配的參考文件。對於全新設計,請先閱讀 data-model.md。
api-routes.md 和 operations.md 中的程式碼是以 Firestore 作為標準後端撰寫的;supabase-backend.md 定義了所有替代方案 — 取代 adIds 的 junction 表,以及服務功能的 SQL 等效項。
| 場景 | 觸發關鍵字 | 參考文件 |
|---|---|---|
| 將此整合到現有應用程式中 | adapt, rename, seam, integrate, tenant, host app | adaptation.md |
| 實體、欄位、播放清單結構 | schema, model, Ad, Display, playlist, junction, soft delete | data-model.md |
| Firestore/Firebase 後端 | Firestore, firebase-admin, security rules, composite index, Firebase Storage | firestore-backend.md |
| Supabase/Postgres 後端 | Supabase, Postgres, RLS, migration, storage bucket, Realtime | supabase-backend.md |
| 端點、配對、Token、快取 | API route, pairing, PIN, token, ETag, scope check, zod | api-routes.md |
| 配對一個螢幕 | pairing, PIN, provisioning URL, hub, credential, unpair | pairing.md |
| 電視播放器迴圈 | player, loop, crossfade, video stall, rotation, portrait, wake lock, offline | player-runtime.md |
| 後台介面 | admin, upload, media library, playlist editor, provisioning URL | admin-ui.md |
| 健康檢查、預覽、遠端控制 | heartbeat, last seen, offline, preview, reload, blank, emergency takeover, audit log | operations.md |
| 排程、重用、報告 | dayparting, start date, campaign, shared playlist, proof of play, precache, multi-zone | extensions.md |
| 範本與原始移植的差異原因 | provenance, ledger, rationale, kept, added | provenance.md |