跳到主要內容

公開 API

用你自己的軟體把內容送上螢幕 — 權杖、圖層堆疊、端點、限制與稽核紀錄。

更新於 以 Markdown 檢視

一個 API,兩種憑證。主控台和任何程式一樣,都是這個 API 的使用者: 人用 session cookie,程式用 bearer 權杖,背後是同一組路由。所有路徑都在 /api/v1 底下;這一頁是我們建議整合程式依賴的精選子集。

取得權杖

以管理員身分到 團隊 → API 權杖 → 新增權杖。密鑰只顯示一次,系統只 保存它的雜湊值。撤銷立即生效。

權杖擁有成員的權限 — 它改變螢幕顯示什麼,永遠不動螢幕本身。它不能 認領螢幕、重新開機、重新設定螢幕、邀請任何人,也不能再核發權杖;所有 標示管理員的操作一律回應 403

每個請求都帶上它:

Authorization: Bearer anysign_<64 hex characters>

錯誤在所有地方都用同一種格式:機器可讀的 reason、一句英文 message, 以及在句子提到限制數值時附上的 params

模型:堆疊,不是插槽

螢幕的內容是圖層堆疊,螢幕顯示的是最高一層有東西的圖層:

emergency      全組織強制接管
takeover       「現在就播這個」— 操作人員的,或你的
source         由整合程式持有的圖層
scheduled      分時段排程
base           日常輪播

寫入一層永遠不影響其他層;移除一層,底下的內容就會透出來。讓多方共用 螢幕變得安全的慣例是:整合程式擁有的是一個圖層,不是一個螢幕。 只寫你自己的那一層,操作人員永遠可以在你上面把螢幕接管回去,也永遠 看得到你蓋住了什麼。

限制

GET /limits

這個 API 用來衡量請求的上限值。用讀的,不要寫死 — 這些數值可以調高, 你這邊不必改版。

{
  "upload": { "maxBytes": 33554432 },
  "image": {
    "maxBytes": 33554432,
    "maxDimension": 4096,
    "maxPixels": 8847360,
    "unplayableMimes": ["image/webp"]
  },
  "video": { "maxBytes": 33554432 },
  "playlist": { "maxItems": 500 }
}

伺服器不做縮圖。 儲存下來的位元組就是去重雜湊的依據,任何轉換都會 破壞內容定址。縮圖是你的工作 — 在瀏覽器裡六行就夠:

const bmp = await createImageBitmap(file, { imageOrientation: "from-image" });
const scale = Math.min(
  1,
  4096 / Math.max(bmp.width, bmp.height),
  Math.sqrt(8847360 / (bmp.width * bmp.height)),
);
const canvas = Object.assign(document.createElement("canvas"), {
  width: Math.floor(bmp.width * scale),
  height: Math.floor(bmp.height * scale),
});
canvas.getContext("2d").drawImage(bmp, 0, 0, canvas.width, canvas.height);
const blob = await new Promise((r) => canvas.toBlob(r, "image/jpeg", 0.9));

imageOrientation: "from-image" 很重要:少了它,直式照片會在錯的軸上 被量測,然後又被渲染端再旋轉一次。

螢幕

GET /screens

{ "screens": [ {
  "id": "…", "name": "Lobby", "site": "HQ",
  "lastSeenAt": 1756400000000,
  "connectivity": "online",           // online | offline | never-seen
  "state": "converged",               // converged | syncing | no-content | unknown
  "converged": true,
  "content": { "layer": "takeover", "kind": "asset", "assetId": "…", "name": "menu.png" },
  "layers": [ /* 完整堆疊,由高至低 */ ]
} ] }

要下結論,就看 converged 這個欄位。 只有在螢幕正與我們通訊、有 內容可顯示、且回報的版本正是我們最後交代的那一版時,它才是 true — 伺服器送出去了不算。content 是堆疊的最上層。停止回報的螢幕會變成 unknown,不管它最後說了什麼。

GET /screens/:screenId 回傳單一螢幕,附上線狀態細節和裝置最後回報的 自身狀態。

PUT /screens/:screenId/layers/:layer

{ "playlistId": "…" }                        // 輪播一份播放清單
{ "assetId": "…", "durationMs": 15000 }      // 用單一項目固定螢幕

兩者只能擇一;:layer 是上面五層之一。播放清單必須存在;資產必須已 上傳完成 — 螢幕被指向還不存在的位元組,會一直認定檔案缺失、永遠停在 準備中。

DELETE /screens/:screenId/layers/:layer

移除一層;底下的內容透出來。清除一個本來就空的圖層也算成功:你要它 消失,它就是消失了。

DELETE /screens/:screenId/layers

清空整個堆疊 — 「讓這個螢幕變空白」的直接手段。

POST /screens/:screenId/commands

{ "type": "refetch" }      // 成員:立即重新同步
{ "type": "screenshot" }   // 成員:擷取面板上的畫面
{ "type": "reboot" }       // 管理員

指令搭螢幕下一次回報的便車送達,執行結果也會回報回來。refetch 是修復 工具,針對裝置持有錯誤位元組的情況,不是快速通道 — 平常發布的內容會在 螢幕自己的回報中送達。

上傳內容

POST /content?name=<filename>[&durationMs=<ms>]

請求本體就是檔案;型別由 Content-Type 標頭決定。

curl -X POST "$BASE/api/v1/content?name=menu.png" \
  -H "authorization: Bearer $TOKEN" \
  -H "content-type: image/png" \
  --data-binary @menu.png
{
  "assetId": "…", "sha256": "…", "kind": "image", "bytes": 20480,
  "width": 1920, "height": 1080, "deduped": false
}

widthheight 是伺服器量出來的,量不出來時就是 null — 檔案 照樣會儲存,但這裡的 null 是在提醒你:把螢幕指向它,畫面上不會出現 任何東西。指向之前先驗證這兩個欄位。

重複上傳相同的位元組是個 no-op,回應 200 和既有的那筆資產 (deduped: true),兩個推送互相競速時也一樣。這是刻意的,因為 連接器最自然的形狀就是「每次都把全部推一遍」。

回應中的 sha256 是我們對實際儲存的位元組算出來的;圖片防護(最長邊、 總像素)依據檔案自己的 PNG/JPEG/GIF 標頭執行 — 電視面板解不了的圖片 會在門口被拒絕,而不是拖垮一個螢幕。

限制:每個請求 32 MB、只收圖片和影片、不收 WebP — 它上傳沒問題、 同步沒問題,就是永遠畫不上面板。

超過 32 MB 請走分段上傳POST /assets 帶檔案的 sha256、mime、位元組數 和種類,回應一個 assetIdPUT /assets/:assetId/parts/:n 接收每段最多 10 MB 的分段,各回傳一個 etag;POST /assets/:assetId/complete 帶上所有 分段完成上傳,POST /assets/:assetId/abort 放棄它。主控台傳影片走的就是 這條路,因為瀏覽器需要在場地的 wifi 上續傳;同樣的理由,它也對你開放。 這裡的 sha256 由你申報 — 謊報只會弄髒你自己的媒體庫,因為裝置下載時會 驗證。

GET /assetsGET /assets/:assetId/content

組織裡就緒的資產;後者回傳位元組,支援 Range。

PATCH /assets/:assetId

{ "name": "lobby-final.png" }

只影響顯示名稱。身分是 sha256,所以改名不搬動任何位元組,也不會讓 螢幕持有的東西失效。

DELETE /assets/:assetId

給那些按排程產生內容、否則也會按排程堆出媒體庫項目的整合程式用的。 有東西還持有這筆資產時回應 409 而不刪除,並指名是誰:

reason 意思
asset_still_uploading 有一筆分段上傳擁有這列;請改為中止那筆上傳。
asset_in_playlists 先把它從指名的播放清單移除。
asset_on_screens 有螢幕正在顯示它。先清除或替換那個螢幕的圖層。

最後一條不是客套:刪掉螢幕正指向的位元組,那台裝置會永遠認定檔案 缺失,再也無法同步完成。

播放清單

GET /playlists 列出全部。GET /playlists/:playlistId 回傳一份與其 項目,讓你可以對照自己上次寫入的內容,不必自己保存一份我們的狀態。

{
  "playlist": { "id": "…", "name": "Photo wall" },
  "items": [ {
    "id": "…", "position": 0, "assetId": "…", "sha256": "…",
    "kind": "image", "name": "guest-0042.jpg", "durationMs": 5000
  } ]
}

POST /playlists{ "name": "…" } 回應 201{ "playlistId" }PATCH /playlists/:playlistId 改名。DELETE /playlists/:playlistId 刪除一份;若有螢幕被指定了它,回應 409 playlist_on_screens 並列出 那些螢幕 — 刪除牆上正在播的東西,必須是明確的兩步。

PUT /playlists/:playlistId/items

{ "items": [
  { "assetId": "…", "durationMs": 5000 },
  { "assetId": "…" }
] }

這會取代整份清單,而這正是特點:送出你想要的清單,永遠不必計算 差異。重送一份沒變的清單無害。順序就是陣列的順序。durationMs 可以 省略 — 影片用自己的長度,靜態圖有預設值。最多 playlist.maxItems 筆。

項目指向不存在或還沒上傳完成的資產時,會以 playlist_item_asset_not_found / playlist_item_asset_not_ready 拒絕, 指名該 assetId,且什麼都不會寫入。

速率限制

兩個額度,刻意計量不同的東西。

額度 依據 大小 429 的 reason
帶權杖並通過驗證的請求 你的權杖 120 / 分鐘 too_many_api_requests
驗證失敗的請求 你的位址 20 / 分鐘 too_many_attempts

要配速的是第一個。 它以權杖計量,所以某個程式失控的迴圈花不掉別人 的額度,而且它只對權杖計費 — 程式不會佔掉主控台操作人員的額度,反過來 也一樣。第二個只對拒絕計費 — 帶著有效權杖的請求永遠碰不到它; 實務上只有部署了被撤銷或打錯的權杖又拚命重試才會遇到。

兩者都以分鐘計,並按服務節點分別計數,所以把它們當成該退避的天花板, 不是要花好花滿的額度。429 帶著和其他拒絕相同的格式;退避後重試即可 — 兩個額度都不是封鎖。

目前還做不到的事

寫明白,免得有人以為是忘了:

  • 沒有依螢幕或依圖層的授權範圍。 權杖作用於整個組織,任何圖層都能 寫;「各自擁有一層」目前是大家守的慣例,不是伺服器強制的規則。
  • 沒有租約。 兩個程式寫同一層就是後寫的贏 — 所以這裡的每一筆寫入 都會被稽核。
  • 權杖沒有效期。 控制手段是撤銷,lastUsedAt 讓被遺忘的權杖在 主控台現形。
  • 沒有對外 webhook、沒有播放證明匯出、沒有 SSO。
  • 429 沒有 Retry-After 兩個額度都以分鐘計,等一分鐘一定夠。
  • 伺服器不做縮圖 — 見上面的限制。這是決定,不是缺漏。
  • 沒有推送:內容在螢幕的一次回報內送達(大約 20 秒),不是即時。
  • 沒有 SDK。 一半的路由一句 curl 就打完;其餘的收一個 JSON 本體。

稽核紀錄

上面的每一筆寫入 — 不論出自人或程式 — 都會以行為者當時的名字記錄, 所以撤銷或改名都改寫不了歷史。用 GET /audit 讀取,或在主控台的 團隊 → 活動 查看。當兩個寫入者共用一個螢幕,讓共用變得安全的 就是這份紀錄。

Navigation

輸入關鍵字開始搜尋…

↑↓ navigate↵ selectEsc close