长轮询(/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 时,显示「机位已删除」。
JavaScript 示例
Section titled “JavaScript 示例”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。