数字标牌 —— 显示屏与广告
实体场所的屏幕不是网页。它在廉价的电视棒上无人值守运行数月,没人盯着控制台,故障模式是一帧画面冻结一周而无人察觉。下面的每一个决策都是为了应对这种情况而存在的。
此技能包含一个完整模块:媒体和播放列表的管理员CRUD、显示屏注册、PIN码和配置URL配对、全屏播放器,以及使其可管理的运维层(健康监控、预览、远程控制)。
何时使用
在实体空间中构建或扩展循环播放媒体的屏幕,基于Next.js App Router和Firestore或Supabase/Postgres。
何时不使用
- Web广告投放(展示量、竞价、跟踪像素、第三方标签)—— 这是不同的问题,需要不同的基础设施。
- 交互式信息亭,用户通过点击进行交易。标牌是单向的。
- 营销页面上的单个嵌入视频。使用
<video>标签即可。 - 通用的Next.js、CMS或身份验证设置 —— 假定这些已存在。
架构
管理员(浏览器) 服务器 设备(电视)
───────────────── ────── ───────────
媒体上传 ──────────────► 对象存储 ◄──── 公开/签名URL ─┐
广告CRUD ─────────┐ │
播放列表编辑 ───┼──► /api/admin/* ──► 广告 + 显示屏 ─┐ │
发送命令 ───────┘ (员工认证) (限定范围) │ │
▼ │
配对屏幕 ◄──── PIN码 ────────────► /api/display/pair │ │
为每个显示屏颁发 │ │
令牌 ▼ │
/api/display/playlist ── 轮询 ──┤
▲ 解析广告ID → 广告 │
└── 上传遥测数据, │
下发命令 播放器循环
整个运行时只依赖一种请求类型:设备轮询/api/display/playlist,上传遥测数据并接收内容和命令。所有的运维操作都通过这个通道进行。
关键事实 —— 在设计任何东西之前请先阅读
- 播放列表是显示屏上的有序数组,而不是一个独立的实体。
display.adIds: string[]就是 播放列表 —— 顺序是自由的,无需连接,一次读取。只有当相同内容必须在多个屏幕上运行时,才引入独立的播放列表实体;参见 data-model.md 了解权衡,以及 extensions.md 了解迁移方案。 - 使用轮询,而非流式传输。 30-60秒的轮询更便宜,能适应休眠的网络堆栈,并且会自动重新连接。实时通信是可选的升级,而不是基础。使用ETag缓存响应,否则你将每次轮询都为每个屏幕支付全量读取的成本。
- 媒体上传直接由浏览器到存储,绝不经过API路由。路由处理程序有主体大小限制,并且在代理传输字节时会消耗计算资源。
- 设备是不可信的且无人值守。 它在任何人可以走上去的硬件上的
localStorage中保存一个长期有效的凭证。该凭证必须是每个屏幕独立的,并且可以撤销。 - 播放器必须永不停止。 每个媒体元素都必须有超时、错误处理程序和跳过方式。一个有问题的素材会前进,而不是卡住。
硬性规则
绝不从渲染状态派生轮询间隔。 在稳定的间隔上进行轮询,并从ref读取幻灯片状态。一个效果依赖于幻灯片索引的计时器在每次幻灯片切换时都会被销毁并重新创建,因此比幻灯片持续时间长的刷新永远不会触发;行为契约将轮询的定时绑定到挂钟时间。
绝不将“暂停”和“删除”重载为同一个布尔值。 使用
active表示操作员意图,并使用单独的deletedAt表示生命周期,否则删除的项目一旦有人重新启用它就会重新出现。
绝不信任客户端提供的租户/位置范围。 从经过身份验证的员工会话或令牌解析到的显示屏行中在服务器端派生它,并在每个
[id]路由上强制执行 —— 返回404,而非403,这样ID就无法被探测。
绝不为每个屏幕签发一个共享令牌。 在配对时为每个显示屏签发自有的令牌,只存储其哈希值,并使撤销成为一个单行更新。
绝不在
onLoad之前开始图片的持续时间计时器。 在速度慢的电视上,否则幻灯片在可见之前就会过期。
绝不将持久的屏幕状态放入一次性命令中。 空白和接管必须能在设备每日重新加载和电源循环后幸存 —— 将它们作为每个轮询上的状态(
mode)传递,并将确认清除的命令通道保留给真正的一次性命令,如重新加载。并且,任何效果是重新加载的命令必须在重新加载之前持久化其确认,否则服务器将永远重新发送它。
快速开始
- 为你的应用填充接口契约并确认域名重命名 —— adaptation.md。
- 建模实体并选择数组vs连接表 —— data-model.md。
- 创建表/集合、索引、安全规则和媒体存储桶 —— firestore-backend.md 或 supabase-backend.md。
- 构建设备和管理员端点,包括配对和令牌验证 —— api-routes.md。
- 让一个屏幕配对 —— pairing.md —— 然后投入播放器循环 —— player-runtime.md。
- 构建后台管理:媒体库、显示屏列表、播放列表编辑器 —— admin-ui.md。
- 在投入生产前添加健康监控、预览和远程控制 —— operations.md。
- 对照 player-runtime.md 中的行为契约表进行验证 —— 拔掉网线,在循环中删除当前广告,发出重载命令 —— 并将播放器机器测试作为回归覆盖。
参考目录
根据触发关键词加载相应的参考文档。对于全新设计,请先阅读 data-model.md。
api-routes.md 和 operations.md 中的代码基于Firestore作为标准后端编写;supabase-backend.md 定义了所有替代方案 —— 替换 adIds 的连接表,以及服务函数的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 |
| 端点、配对、令牌、缓存 | API route, pairing, PIN, token, ETag, scope check, zod | api-routes.md |
| 让一个屏幕配对 | pairing, PIN, provisioning URL, hub, credential, unpair | pairing.md |
| TV播放器循环 | player, loop, crossfade, video stall, rotation, portrait, wake lock, offline | player-runtime.md |
| 后台管理UI | 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 |