跳转到内容

长轮询(/poll/v1)

适用于无法使用 WebSocket 的客户端:watchOS 普通 App 在真机上无法使用 WebSocket 和 Network framework(Apple TN3135);简单硬件也可使用此接口。只读,无需鉴权。

GET <base>/poll/v1?since=<rev> → 200,JSON,Cache-Control: no-store:

{
"type": "snapshot",
"source": { "type": "vmix", "state": "ready", "error": null },
"devices": [{ "id": "k3m9x2qa", "name": "Camera 1", "program": true, "preview": false }],
"rev": "0f8c2d1e-7b3a-4c59-9e21-5a6d8f4b3c70-12"
}
  • 除 rev 外,与 WebSocket 的 snapshot 完全相同。
  • rev 是不透明字符串:客户端只需原样带回,并只比较是否相等。快照内容变化时 rev 随之变化;服务每次重启都使用新的随机前缀,不会与重启前的值重复。
  • 未带 since,或 since 不等于当前 rev:立即返回当前快照。
  • since 等于当前 rev:请求挂起,快照变化时立即返回新快照;最多等待 4 秒,超时返回当前快照(rev 不变),相当于心跳。
  • 服务关闭时,挂起的请求会立即返回。
  • 收到响应后,立即用新的 rev 发起下一次请求。
  • 单次请求 6 秒内未返回:显示 Unknown,取消该请求并重新发起。
  • 请求失败(无法连接、非 200、JSON 无效)时显示 Unknown,每 2 秒重试一次,且不带 since。
  • 快照中找不到本机位 ID 时,显示「机位已删除」。
async function poll() {
let rev = null
for (;;) {
try {
const query = rev === null ? '' : `?since=${encodeURIComponent(rev)}`
const res = await fetch(`${base}/poll/v1${query}`, { signal: AbortSignal.timeout(6000) })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const snapshot = await res.json()
rev = snapshot.rev
show(tallyOf(snapshot, deviceId))
} catch {
rev = null
show('unknown')
await new Promise((resolve) => setTimeout(resolve, 2000))
}
}
}
poll()

Swift 示例(watchOS 9 / iOS 16 及以上,URLSession)

Section titled “Swift 示例(watchOS 9 / iOS 16 及以上,URLSession)”
import Foundation
struct Snapshot: Decodable {
struct Device: Decodable {
let id: String
let name: String
let program: Bool?
let preview: Bool?
}
let devices: [Device]
let rev: String
}
enum Tally { case program, preview, off, unknown, deleted }
func tally(of snapshot: Snapshot, deviceId: String) -> Tally {
guard let device = snapshot.devices.first(where: { $0.id == deviceId }) else { return .deleted }
guard let program = device.program, let preview = device.preview else { return .unknown }
if program { return .program }
if preview { return .preview }
return .off
}
/// 在一个 Task 里运行,取消 Task 即停止;show 负责切到主线程更新界面
func poll(base: URL, deviceId: String, show: (Tally) -> Void) async {
let config = URLSessionConfiguration.ephemeral
config.timeoutIntervalForRequest = 6 // 两批数据之间最多等 6 秒
config.timeoutIntervalForResource = 6 // 整次请求最多 6 秒,超时走 Unknown 和重试
let session = URLSession(configuration: config)
var rev: String?
while !Task.isCancelled {
var url = URLComponents(url: base.appending(path: "poll/v1"), resolvingAgainstBaseURL: false)!
if let rev { url.queryItems = [URLQueryItem(name: "since", value: rev)] }
do {
let (data, response) = try await session.data(from: url.url!)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) }
let snapshot = try JSONDecoder().decode(Snapshot.self, from: data)
rev = snapshot.rev
show(tally(of: snapshot, deviceId: deviceId))
} catch {
rev = nil
show(.unknown)
try? await Task.sleep(for: .seconds(2))
}
}
}

手表脱离 iPhone 使用时,需自行连接与电脑相同的 Wi-Fi。