Skip to content

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.

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 WebSocket snapshot.
  • rev is 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 a since other than the current rev: the current snapshot is returned at once.
  • With since equal to the current rev: the request waits and returns as soon as the snapshot changes. After at most 4 seconds it returns the current snapshot (same rev), which serves as a heartbeat.
  • When the server shuts down, waiting requests return at once.
  • 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”.
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 UI
func 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.