Long poll (/poll/v1)
For clients that cannot use WebSocket. Standard watchOS apps cannot use WebSocket or the Network framework on a real watch (Apple TN3135), and simple hardware can use this endpoint too. Read-only; no authentication.
Request and response
Section titled “Request and response”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"}- Apart from
rev, identical to the WebSocketsnapshot. revis an opaque string: send it back unchanged and only compare it for equality. It changes whenever the snapshot changes. Each server start uses a new random prefix, so it never matches a value from before a restart.- Without
since, or with asinceother than the currentrev: the current snapshot is returned at once. - With
sinceequal to the currentrev: the request waits and returns as soon as the snapshot changes. After at most 4 seconds it returns the current snapshot (samerev), which serves as a heartbeat. - When the server shuts down, waiting requests return at once.
Client rules
Section titled “Client rules”- As soon as a response arrives, send the next request with its
rev. - No response within 6 seconds: show Unknown, cancel the request and send a new one.
- On failure (cannot connect, not 200, bad JSON) show Unknown and retry every 2 seconds without
since. - If your camera ID is not in the snapshot, show “Camera deleted”.
JavaScript example
Section titled “JavaScript example”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 example (watchOS 9 / iOS 16 and later, URLSession)
Section titled “Swift example (watchOS 9 / iOS 16 and later, 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}
/// Run in a Task; cancel the Task to stop. show must hop to the main thread to update the UIfunc poll(base: URL, deviceId: String, show: (Tally) -> Void) async { let config = URLSessionConfiguration.ephemeral config.timeoutIntervalForRequest = 6 // at most 6 s between chunks of data config.timeoutIntervalForResource = 6 // at most 6 s per request; a timeout means Unknown and a retry 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)) } }}When the watch is away from its iPhone, it must join the same Wi-Fi network as the computer.