文档

构建一个属于屏幕的应用

使用注入的浏览器 SDK 控制播放列表、应用事件、诊断和持久键值状态。

观看它的构建过程

影片 / 使用 SDK 构建自助服务终端应用

3:07

访客点击欢迎墙下的大堂签到自助终端。

Claude Code 编写了一个大厅签到自助机,针对 window.screenrig:waitUntilActive()、ready()、kv.get() 和 emitConfirmed()。访客点击即可联系智能体,智能体在欢迎墙上回应。章节和文字记录 →

运行时已在应用中

app pack 和 app upload 接收已构建的静态目录,根目录为 index.html。打包器会注入 _screenrig/runtime.js 并将其脚本标签添加到该文档。运行中的应用无需构建步骤和 npm 依赖即可接收 window.screenrig:无需安装、无需导入、无需配置。

每个不可变的应用发布都在apps.screenrig.ai下独立的隔离域名上运行。应用 id 比发布周期更长,因此某个发布写入的 key/value 数据,下一个发布仍可使用。

window.screenrig 表面

三个只读属性、十个方法、一个包含另外五个方法的 kv 命名空间,以及一个用于显示器、人员到场、硬件按键、光线传感器、网络状态和触觉反馈的 device 命名空间。请先阅读 readyState,并在调用某次激活可能没有的方法前检查 capabilities。

每种方法回答不同的问题,请从您的需求出发:

要执行此操作调用
等待播放器激活此页面screenrig.waitUntilActive()
表示应用程序正在运行并显示中screenrig.ready()
结束此应用在应用推进页面的轮次screenrig.nextPage()
告知播放器此页面暂时没有内容可显示screenrig.skip(reason)
告诉智能体发生了什么screenrig.emit(code)
确认后端已记录该事件screenrig.emitConfirmed(code)
为播放器和运行时留下诊断信息以收集screenrig.log(...)
读取实时应用状态screenrig.kv.get(), screenrig.kv.list()
写入实时应用状态screenrig.kv.set(), screenrig.kv.delete()
实时应用状态变更时更新screenrig.kv.onChange()
响应尺寸或功能变化screenrig.onResize(), screenrig.onCapabilities()
调暗、熄灭或唤醒显示器screenrig.device.display.set()
获知有人走近了屏幕screenrig.device.onPresence()
接管硬件音量键screenrig.device.onKey()
获知环境光线强度screenrig.device.onLight()
获知设备已上线或离线screenrig.device.onNetwork()
播放一次触觉轻震screenrig.device.vibrate()
应用完成运行时后拆除监听器screenrig.destroy()

screenrig.readyState#

注入的运行时是否附加。每个文档开始为 inert,当播放器握手和上下文到达时变为 active;尚未显示在屏幕上的页面保持 inert。等待 active 使用 screenrig.waitUntilActive();加载时不要将 inert 视为“屏幕外”。在 failed,或 window.screenrig 缺失时,渲染自己的内容并跳过下面的表面,因此相同构建也可在 screenRIG 外运行。处于 inert、ready()、log()、emit()、emitConfirmed()、nextPage()、skip() 以及 kv 读写时会抛出或拒绝,代码为 inert;kv.onChange 可随时注册。

类型
"inert" | "active" | "failed"

screenrig.context#

此文档运行位置。

字段类型备注
application_idstring来自 app upload 的 app_ ID。键值存储属于它。
release_idstring服务此来源的不可变版本。
primitive_idstring本页当前填充的原始数据。
viewport{ width, height }播放器渲染的表面。
primitive{ width, height }该文档在视口内占据的矩形区域。
capabilitiesobject与 screenrig.capabilities 相同的布尔值。
screen_id字符串,可选运行时已知哪个屏幕正在播放时出现。

context 是描述信息,不是请求内容。其中任何内容都不应签名、转发或返回给 screenRIG;下方方法会携带运行时所需内容。

启动参数不属于 context:播放列表可以为应用提供 params(参见iframe 地址和应用参数中的屏幕令牌),播放器会将每一项作为查询参数追加到页面地址中 ticket 之后。请从 location.search 读取,例如 new URLSearchParams(location.search).get("kiosk")。它们是提示而非凭据,任何能打开该页面的人都可以发送任意值,因此应视为不受信任的输入;要了解正在播放的是哪个屏幕,仍应使用 context.screen_id。Qt、Android、Apple 和 Windows 播放器从本地包加载应用,该包没有启动地址,也不向其传递参数。

screenrig.capabilities#

十个布尔值表示哪些受限方法会对该激活执行操作。键名带点,使用括号表示法读取。

标记大门
page.advancescreenrig.nextPage()
page.skipscreenrig.skip()
kv.readscreenrig.kv.get(),screenrig.kv.list() 和 screenrig.kv.onChange()
kv.writescreenrig.kv.set() 和 screenrig.kv.delete()
device.displayscreenrig.device.display.get(),.set() 和 screenrig.device.onDisplay()
device.presencescreenrig.device.onPresence()
device.keysscreenrig.device.onKey()
device.lightscreenrig.device.onLight()
device.networkscreenrig.device.onNetwork()
device.hapticsscreenrig.device.vibrate()
if (window.screenrig.capabilities["kv.write"]) {
  saveButton.hidden = false;
}

使用 onCapabilities 订阅,而不是启动时一次性读取标志。

类型
布尔值对象

screenrig.waitUntilActive(timeoutMs)#

首先调用,优先于其他方法,并在等待时渲染 UI。当播放器激活此页面时解析,或运行时已 active 时立即解析,且不声明应用已渲染。timeoutMs(默认 10000,最大 300000)后若未解析则以 activation_timeout 拒绝;等待轮次的页面可继续等待。请在其解析后再开始播放声音,因为播放器会在页面显示之前加载应用:浏览器允许自动播放的屏幕会立即开始播放,拒绝自动播放的屏幕会使 play() 以 NotAllowedError 拒绝,因此请捕获该错误并静音播放。

const sr = window.screenrig;
async function start() {
  render(); // the UI works with or without screenRIG
  if (!sr) return;
  for (;;) {
    try { await sr.waitUntilActive(60000); break; }
    catch (err) { if (err.code !== "activation_timeout") return; }
  }
  await sr.ready();
  // From here: emit, emitConfirmed, log, kv, nextPage.
}
start();

在事件处理程序、计时器或 catch 调用 SDK 之前检查 readyState === "active",并排队早于它到达的输入。在 inert 运行时的处理程序抛出 log(),这可能导致启动前未发送任何事件。

返回
Promise<void>

screenrig.ready()#

在 waitUntilActive() 解析后且依赖 context、kv 或 nextPage 之前调用,一旦应用具备上屏所需条件。调用时若 inert,则抛出异常。

await window.screenrig.waitUntilActive();
await window.screenrig.ready();
返回
Promise<void>

screenrig.nextPage()#

仅在此原语为控制器的应用前进页面调用,当应用完成其轮次。一次调用对应一次激活。

window.screenrig.nextPage();

该方法用于 advance.mode 为 application 的页面,此原语为控制器:

{
  "advance": { "mode": "application", "max_ms": 60000 },
  "primitives": [
    {
      "id": "app",
      "primitive": "application",
      "release_id": "rel_01EXAMPLERELEASE000000000",
      "rect": { "x": 0, "y": 0, "width": 1920, "height": 1080 },
      "layer": 0,
      "content_fit": "fill",
      "controller": true
    }
  ]
}

该页面上恰好有一个原语携带 controller: true,且必须是应用原语。页面上的 max_ms 推进对象边界。

需要
capabilities["page.advance"]
返回
Promise<void>

screenrig.skip(reason)#

当应用程序暂时没有内容可显示时,请调用它来代替 ready()。屏幕会立即以页面的常规过渡切换到下一页,此次出现不计为播放。这是启动时的决定:请在激活后十秒内、在 ready() 之前调用。

await window.screenrig.waitUntilActive();
const events = await loadToday();
if (events.length === 0) {
  await window.screenrig.skip("no events today");
} else {
  render(events);
  await window.screenrig.ready();
}

页面的控制器,或该页面上唯一的应用程序,在没有图片或视频的页面上拥有此能力。没有该能力时方法以 capability_denied 拒绝;已调用 ready() 之后以 skip_after_ready 拒绝;播放器不支持时以 not_supported 拒绝。当播放器保留该页面时(操作员正在保持它、显示器已熄灭、没有其他页面可播放,或其他所有页面也都已跳过)以 forbidden 拒绝。此时请渲染您的后备内容并调用 ready()。全部跳过的图块播放列表会完整显示最后一个,而不是循环切换。可选的 reason(最多 64 个字符)会作为 player.application_skipped 出现在项目的播放器日志中。skip() 随 app pack 和 app upload 注入的运行时提供,因此请重新打包版本以获得该方法。

需要
capabilities["page.skip"]
返回
Promise<void>

screenrig.emit(code)#

当智能体应知晓事件发生时使用:页面完成步骤、提交自助终端表单、应用确认运行。这是命名的应用事件,记录在项目事件日志中,智能体通过捆绑 CLI 的 events list 或 events follow 观看。仅代码,无消息体。

log 不是替代品。诊断不是应用事件,读取事件日志的智能体不会看到它。

window.screenrig.emit("menu.rendered");

SDK 的 emit() 和 emitConfirmed() 会添加以屏幕为资源的 application.event 记录。使用 --type application.event 读取,可选择添加 --screen-id。返回的 details.primitive_id 和 details.code 标识发出事件的图元和命名操作。--app-id 选择应用资源、发布版本和 K/V 事件,因此不包含这些屏幕事件。

screenrig events list --type application.event --screen-id scr_SCREEN
参数类型备注
codestring净化为 [A-Za-z0-9._-],最多 64 个字符。
返回
void

screenrig.emitConfirmed(code)#

当应用必须确认事件已到达时使用:自助终端订单已下、表单已发送、访问已结束。发送与 emit 相同的命名应用事件,且仅当播放器确认后端接受事件时,承诺才解决。

事件被拒绝时返回后端问题,10 秒无回复后返回 ack_timeout。超时后勿自动重试:后端可能已接受事件。收据证明事件已入项目事件日志,但不代表已执行。

收据适用于运行应用的每个播放器。

await window.screenrig.emitConfirmed("order.placed");
参数类型备注
codestring净化为 [A-Za-z0-9._-],最多 64 个字符。
返回
Promise<void>

screenrig.log({ level, code, message })#

用于诊断:debug、info、warn 和 error 是播放器和运行时在应用显示时收集的文本。注入的运行时日志通过此方法自身记录。日志限制:每客户端 32 行,且每条消息最多 2048 字节。记录调试时需要的信息,而非每帧一行。

当你想用 emit 时,不要使用 log。智能体不将日志行视为应用事件,智能体需要处理的内容应放在 emit 代码中。

window.screenrig.log({
  level: "warn",
  code: "menu.stale",
  message: "Falling back to the cached menu."
});
字段类型备注
levelstringdebug,info,warn,或 error。
codestring净化为 [A-Za-z0-9._-],最多 64 个字符。
messagestring最多 2048 字节。
返回
void

screenrig.onResize(handler)#

当布局可能变化时订阅,而非只读取一次 context。处理程序接收 { viewport, primitive },与 context 传递的两个对象相同,调用返回结束订阅的函数。

const stopResize = window.screenrig.onResize(function (size) {
  render(size.viewport, size.primitive);
});

stopResize();
返回
取消订阅功能

screenrig.onCapabilities(handler)#

当授予的功能可能变化时订阅,而非启动时一次决定。在处理程序内读取 screenrig.capabilities 当前布尔值,启用或隐藏相关控件;调用返回结束订阅的函数。

const stopCapabilities = window.screenrig.onCapabilities(function () {
  saveButton.hidden = !window.screenrig.capabilities["kv.write"];
});
返回
取消订阅功能

screenrig.destroy()#

应用完成运行时拆除监听器。 运行完整周期并允许播放器替换文档的应用无需调用。

返回
void

screenrig.kv#

应用的键值存储,用于存活超过一次激活的实时应用状态。五种自身方法:get、list、set、delete 和 onChange。详见下文 键值存储。

类型
命名空间

键值存储

使用存储获取实时应用状态:应用渲染的菜单、操作员今晨更改的文案、上次激活留下的计数器。用 kv.get 和 kv.list 读取,用 kv.set 和 kv.delete 写入,用 kv.onChange 监听所有更改。

商店属于 context.application_id 中的应用 ID,而非发布,因此条目可在下一次 app upload 后存活。值为字节,文本、JSON 和小型二进制均可往返。读取需 kv.read,写入需 kv.write;调用前请检查标志。

商店及其变更推送运行于所有运行应用的播放器:播放器 Web 应用(PWA)、Android、Amazon Signage Stick、macOS、Windows、Linux、Raspberry Pi、LG webOS Signage、BrightSign 和 Samsung Smart Signage(Tizen)。Apple TV、iPhone 和 iPad 不运行应用;参见 平台的原语支持。

screenrig.kv.get(key)#

读取实时应用状态。 等待 Promise。

const entry = await window.screenrig.kv.get("menu");
需要
capabilities["kv.read"]
返回
promise

screenrig.kv.list()#

在尚未知晓密钥时读取实时应用状态: 列出应用已存储的密钥。等待 Promise。

const keys = await window.screenrig.kv.list();
需要
capabilities["kv.read"]
返回
promise

screenrig.kv.set(key, value, options)#

写入实时应用状态。 value 是字符串或 Uint8Array;options 可选。

await window.screenrig.kv.set("menu", JSON.stringify(menu), {
  contentType: "application/json"
});

await window.screenrig.kv.set("badge.png", pngBytes, {
  contentType: "image/png"
});
参数类型必填备注
keystring是参见 键。
value字符串或 Uint8Array是待存储的字节。
options.contentTypestring否默认字符串为 text/plain,Uint8Array 的默认值为 application/octet-stream。
options.expectedRevision非负整数否仅当条目仍处于此版本时写入。
options.idempotencyKeystring否重试相同写入操作,避免重复应用。
需要
capabilities["kv.write"]
返回
promise

screenrig.kv.delete(key, expectedRevision, options)#

写入实时应用状态 通过删除条目。expectedRevision 可选:省略以删除当前条目,或提供以拒绝并发更改。

await window.screenrig.kv.delete("menu");
参数类型必填备注
keystring是参见 键。
expectedRevision非负整数否你读取时条目的修订版本。
options.idempotencyKeystring否重试相同删除操作,避免重复应用。
需要
capabilities["kv.write"]
返回
promise

screenrig.kv.onChange(handler)#

在实时应用状态变化时更新,而非轮询。处理程序在此应用的任何键变化时运行,无论是谁写的:带有 kv set 的智能体、通过 键值 webhook 的外部系统,或任何屏幕上的此应用。它接收哪个键改变,而非值,因此用 kv.get 读取新值。调用返回结束订阅的函数。

const sr = window.screenrig;

async function showMenu() {
  render(await sr.kv.get("menu"));
}

const stopChanges = sr.kv.onChange(function (change) {
  if (change.key === "menu" && !change.deleted) showMenu();
});

await sr.waitUntilActive();
await sr.ready();
await showMenu();

在首次读取前订阅,以免错过读取时发生的更改;激活前注册的处理程序在页面激活后开始运行。调用 stopChanges() 结束订阅。

更改在几秒内传达到所有播放该应用的屏幕。对同一键的多次快速写入可能合并为携带最新修订的单次更改,因此将更改视为“重新读取此键”,而非写入计数。更改是信号,不是历史:应用启动时读取所需内容,然后跟踪更改。

字段类型备注
keystring已更改的键。
revision整数,可选条目的新修订版。当 deleted 是 true 时缺失。
deletedbooleantrue 在密钥被删除时。
需要
capabilities["kv.read"];没有它处理程序永远不会运行
返回
取消订阅功能

键列表

密钥为 1 到 256 个 UTF-8 字节。限制为字节数,不是字符数,因此含多字节字符的密钥比长度显示的更快达到限制。密钥不得以 _screenrig/ 开头;该前缀属于注入的运行时。

修订和幂等性

每个条目都有一个修订版本。可选地传递expectedRevision到screenrig.kv.set或screenrig.kv.delete,仅当条目仍处于你读取的修订版本时才写入。过期的修订版本意味着其他人先写入:请重新读取条目,重新应用更改,然后重试。

idempotencyKey 负责另一半。对于在响应丢失后可能重试的写入操作,每次尝试都可以携带相同的密钥,商店只会应用一次。

来自 CLI 的相同商店

代理可以通过捆绑 CLI 的 kv 命令,针对 app upload 中的 app_ ID,从浏览器外部访问这些条目。这是运营者对商店的视图。它不能替代上述五种方法:应用程序通过 screenrig.kv 在浏览器中、屏幕上运行时更改自己的数据。代理写入的更改通过 kv.onChange 传达给运行中的应用程序。

从其他系统推送数据

键值 webhook 是一个 URL,写入此应用的一个键。交给任何能发送 HTTP POST 的工具,如 销售点、电子表格脚本、定时任务或 curl,每次 POST 都替换该键的值。运行该应用的每个屏幕通过 kv.onChange 在几秒内获知,无需新版本发布或智能体介入。

代理使用捆绑 CLI 为密钥创建一个,并提供其 URL 和密钥:

screenrig kv webhook create menu --app-id app_01EXAMPLEAPP00000000000 --description "Point of sale menu feed"

响应携带 webhook 的 url 和 secret(srkw_…)。密钥仅在创建 webhook 和密钥轮换时显示,请立即在发送系统中输入。URL 非密钥,密钥才是。

发送方将新值作为原始请求主体发布,秘密放在 Authorization 头中:

curl https://api.screenrig.ai/api/applications/app_01EXAMPLEAPP00000000000/kv-webhooks/kvw_01EXAMPLEWEBHOOK00000/write \
  -H "Authorization: Bearer $KV_WEBHOOK_SECRET" \
  -H "Content-Type: application/json" \
  --data '{"soup":"lentil","price":"6.50"}'

仅使用 HTTP Basic 认证的发送方使用 webhook id 作为用户名,secret 作为密码:

curl https://api.screenrig.ai/api/applications/app_01EXAMPLEAPP00000000000/kv-webhooks/kvw_01EXAMPLEWEBHOOK00000/write \
  --user "kvw_01EXAMPLEWEBHOOK00000:$KV_WEBHOOK_SECRET" \
  -H "Content-Type: application/json" \
  --data '{"soup":"lentil","price":"6.50"}'
  • 主体最大 1 MiB,成为值,Content-Type 为其内容类型(缺头时为 application/octet-stream)。值传递至浏览器应用,切勿作为凭证发布。
  • 每次发布都是一次类似 kv set 的键值写入:会替换值、提升版本号,并像其他写入一样计入存储用量。
  • 响应为密钥的新元数据(key、revision、bytes、sha256、content_type 和 updated_at),不包含其值。
  • 添加一个 Idempotency-Key 头(8 到 200 个字符)以安全重试 POST:24 小时内相同密钥只应用一次,且相同密钥但不同主体或内容类型的请求以 idempotency_mismatch 拒绝。无此头时,每个 POST 都是新写入。
  • 一个 webhook 每分钟最多接收 120 次 POST;超过则返回 429 和 Retry-After。
  • 缺失或错误的密钥,或未知的 webhook,响应 401。URL 或查询字符串中的密钥永不被接受。
  • 应用最多有 20 个 webhook。kv webhook rotate-secret 颁发新密钥,旧密钥立即停止;kv webhook delete 立即停止 URL,但保留密钥值。

卡片同样支持 webhook:每次发送都会生成其数据文档,可原样使用,也可通过映射生成(卡片 webhook)。

键值 webhook 命令 →

设备

运行壁挂面板或自助终端的应用程序可以控制自己所在的屏幕。device 标志会交给控制其页面的框架(页面的控制器,或页面上唯一的应用程序),前提是播放器能在该设备上完成这项工作:Android 播放器授予 device.display、device.keys 和 device.network,在设备有摄像头、且设置设备的人允许播放器使用摄像头时授予 device.presence(Android 设置、应用、screenRIG Player、权限、相机),在设备有光线传感器时授予 device.light,在设备有振动马达时授予 device.haptics。无法控制设备的播放器会让每个 device 标志都为 false。display.get()、display.set() 和 vibrate() 在不提供它们的播放器上以 not_supported 拒绝,没有该标志时以 capability_denied 拒绝。onDisplay、onPresence、onKey、onLight 和 onNetwork 可随时注册,并且与 kv.onChange 一样,没有该标志时永远不会运行。

screenrig.device.display.set(state)#

调暗、熄灭或唤醒显示器。brightness 取值 0 到 1(0 是背光能达到的最低亮度,并非关闭);on: false 会让屏幕变暗,on: true 会让它重新亮起。省略某个成员则保持其原状。屏幕变暗时,触摸仍会传到应用程序,因此一次触摸即可将其唤醒。让屏幕保持熄灭的显示计划优先于应用程序,页面离开屏幕时播放器会恢复显示。

await window.screenrig.device.display.set({ brightness: 0.3 });
await window.screenrig.device.display.set({ on: false });
解析为
{ on, brightness },即调用之后的显示器状态(播放器无法读取时 brightness 为 null);set({}) 仅读取,屏幕变暗期间 brightness 保持其值
需要
capabilities["device.display"]

screenrig.device.display.get()#

当前显示器状态,格式为 { on, brightness }。

需要
capabilities["device.display"]

screenrig.device.onDisplay(handler)#

每当显示器发生变化时,以 { on, brightness } 调用,无论是此应用程序、显示计划还是操作员所做的更改。

返回
取消订阅功能
需要
capabilities["device.display"]

screenrig.device.onPresence(handler)#

当设备检测到有人在屏幕前移动时调用,每秒最多一次。播放器读取设备上的摄像头,不保留任何图像:不存储、不记录、也不发送任何图像。只有在注册了处理程序时,摄像头才会运行。

window.screenrig.device.onPresence(function () {
  window.screenrig.device.display.set({ on: true, brightness: 1 });
});
返回
取消订阅功能
需要
capabilities["device.presence"]

screenrig.device.onKey(handler)#

每次按下硬件音量键(包括重复按键)时,以 "volume_up" 或 "volume_down" 调用。注册处理程序期间,按键归应用程序所有,设备音量保持不变。

返回
取消订阅功能
需要
capabilities["device.keys"]

screenrig.device.onLight(handler)#

以环境光照度(单位 lux)调用:先是处理程序注册后的第一个读数,之后每当光线变化至少 10% 或跨越 1 lux 时再调用,每秒最多一次。设备需要具备光线传感器。

window.screenrig.device.onLight(function (lux) {
  document.body.classList.toggle("dim-room", lux < 10);
});
返回
取消订阅功能
需要
capabilities["device.light"]

screenrig.device.onNetwork(handler)#

每当设备获得或失去系统已验证的互联网连接时,以 true 或 false 调用;注册第一个处理程序时,会立即以当前状态调用。

window.screenrig.device.onNetwork(function (online) {
  offlineBadge.hidden = online;
});
返回
取消订阅功能
需要
capabilities["device.network"]

screenrig.device.vibrate(ms)#

播放一次触觉轻震。 ms 是 1 到 200 之间的整数毫秒数,默认为 12。播放器开始振动后,Promise 以无值兑现;对于任何其他 ms,则以 invalid_request 拒绝,且不发送任何内容。

await window.screenrig.device.vibrate();
await window.screenrig.device.vibrate(40);
解析为
无值
需要
capabilities["device.haptics"]

访问屏幕所在网络中的设备

应用程序除了 https:// 地址外,还可以从 http:// 和 ws:// 地址获取数据,因此可以与屏幕所在局域网中的设备或服务器通信,例如位于 http://192.168.1.20 的控制器。Android、Linux 和 Raspberry Pi、Windows 以及 macOS 的播放器均允许这样做。在运行播放器 Web 应用的自助终端浏览器上,由浏览器的所有者通过企业策略允许:参见本地网络上的应用程序。您调用的服务器需针对应用程序的源响应 CORS;在 macOS 上,应用程序的请求携带 Origin: null,因此服务器的 CORS 需要在那里允许该源。

浏览器引擎会在安全页面上阻止 <img src="http://…">,因此请使用 fetch() 和 URL.createObjectURL() 加载来自 http:// 地址的图片。

const response = await fetch("http://192.168.1.20/snapshot.jpg");
image.src = URL.createObjectURL(await response.blob());

构建并放置应用