import type { LivestreamSpec, Segment } from "../transport/livestream-session.ts"; import type { Clock } from "../clock.ts"; import type { ProtectError } from "../errors.ts"; import type { ProtectLogging } from "../logging.ts"; import type { ProtectWebSocket } from "../transport/ws.ts"; /** * The coarse, stable lifecycle of a pooled stream as a subscriber observes it. `connecting` is bounded establishment (no MEDIA segment has ever flowed); `live` is a * healthy stream; `recovering` is an in-flight recovery episode (a recoverable stall shows here *without* ending iteration); `closed` is terminal (disposed, codec * change, or the policy gave up). * * @category Client */ export type LivestreamSubscriptionState = "closed" | "connecting" | "live" | "recovering"; /** * What the library can observe at a recovery decision point. Consumer-private state (buffer headroom, controller health) is deliberately *not* here - a consumer that * wants to correlate recovery with controller health reads it in its own policy closure. The fields are exactly what the library itself can compute. * * @category Client */ export interface RecoveryContext { /** Consecutive failed reconnect attempts this episode; resets to 0 once a media segment flows again. */ attempts: number; /** The camera this recovery episode belongs to, so a consumer policy can correlate the decision with per-camera state it alone observes. */ cameraId: string; /** Milliseconds since this episode began (the stall moment, or the first establish attempt). Lets a policy enforce a bounded establishment deadline. */ elapsedMs: number; /** Bounded establishment (fail fast if it never came up) vs. unbounded live recovery (it proved itself once). */ phase: "establishing" | "recovering"; /** Aggregated delay tolerance across subscribers - the MINIMUM reported, so the most-urgent governs; `Infinity` when none reported. */ toleranceMs: number; } /** * A recovery decision the {@link RecoveryPolicy} returns. Its arms are exhaustive: * * - `reconnect` tears down any socket and connects fresh; the attempt is treated as failed if no media segment arrives within `awaitMs`, then the policy is re-consulted. * `awaitMs` doubles as the inter-attempt spacing (a fast-failing attempt still occupies its full window), so the single policy governs both disposition and timing. * - `wait` does nothing for `forMs`, then re-consults - unifying defer-soak (a still-connecting session's late segment cancels it early) and inter-attempt backoff. * - `giveUp` is terminal: the give-up error is thrown into every subscriber and the stream is torn down. * * @category Client */ export type RecoveryDecision = { awaitMs: number; kind: "reconnect"; } | { forMs: number; kind: "wait"; } | { kind: "giveUp"; }; /** * The single decision authority for a stream's recovery, injected at the pool (the same dependency inversion as `resolveUrl`). Pure: given the library-observable * {@link RecoveryContext}, it returns a {@link RecoveryDecision}. The library ships {@link defaultLivestreamRecoveryPolicy}, a health-agnostic default; a consumer * injects its own to add the controller-health correlation only it can observe. * * @category Client */ export type RecoveryPolicy = (context: RecoveryContext) => RecoveryDecision; /** * The library's default, health-agnostic recovery policy. It is **phase-split**, because establishing and recovering have different timing authorities: * * - **Establishment (`phase === "establishing"`) is hardware-bound and urgency-INDEPENDENT.** A fresh stream is minted at the camera's own first-segment latency, which * no consumer can hurry - so the per-attempt window follows the fixed, patient {@link PROTECT_LIVESTREAM_ESTABLISH_BACKOFF_MS} curve and ignores `toleranceMs` entirely * (tearing a fresh stream down early just churns the negotiation). It gives up once the episode runs past {@link PROTECT_LIVESTREAM_ESTABLISH_DEADLINE_MS} - the * stream that connects but never produces media, its windows walking the patient establishment curve and holding at the last stage until the deadline. * - **Live recovery (`phase === "recovering"`) is headroom-clamped and unbounded.** A stream that proved itself once is retried forever (never `giveUp`); each window * grows along {@link PROTECT_LIVESTREAM_RECOVERY_BACKOFF_MS}, capped by the consumer's headroom (the aggregated tolerance less a margin; the patient default when none * reported) and clamped to {@link PROTECT_LIVESTREAM_AWAIT_MIN_MS}/{@link PROTECT_LIVESTREAM_AWAIT_MAX_MS}. An urgent subscriber recovers at the floor; a patient one * follows the curve. * * These are safe **ecosystem** defaults - conservative across the diverse hardware real users run, not tuned for a fast controller. A consumer that has measured its own * controllers' latency (e.g. HBUP) injects a tighter policy; a consumer composing one may delegate here for the timing and override only the disposition, or replace it. * * @param context - The library-observable recovery context. * * @returns The recovery decision for this step. * * @category Client */ export declare function defaultLivestreamRecoveryPolicy(context: RecoveryContext): RecoveryDecision; /** * The delivery counters a {@link LivestreamSubscription} exposes, so a consumer can monitor its own slowness (a growing `queueDepth` / `peakQueueDepth`) and respond - * the only backpressure mechanism, since the queue itself is unbounded. * * @category Client */ export interface LivestreamSubscriptionStats { /** Segments handed to the iterator over this subscription's life. */ delivered: number; /** Segments dropped undelivered when this subscription was disposed under `discardOnDispose`; `0` for a default drain-on-dispose subscription. */ discarded: number; /** The `clock.now()` timestamp of the most recent segment enqueued for this subscriber. */ lastSegmentAt: number; /** The historical high-water mark of `queueDepth`. Monotonic: it records the peak backlog this subscriber ever accrued, which a later discard does not lower. */ peakQueueDepth: number; /** Segments queued but not yet delivered right now - the live backlog. Reads `0` immediately after a `discardOnDispose` disposal clears the queue. */ queueDepth: number; } /** * Per-subscription options for `LivestreamPool.subscribe` (reached through the {@link Camera.livestream} consumer entry point). All optional: a bare `subscribe(spec)` * is a resilient, drain-on-dispose subscription with no abort wiring and no reported urgency. * * @category Client */ export interface LivestreamSubscribeOptions { /** * Discard any still-queued, undelivered segments the instant this subscription is disposed, so a consumer mid-iteration receives the terminal rather than draining * stale bytes first. Off by default: a disposed subscription normally drains its queue before ending. Scoped to the consumer's own disposal alone; a pool-forced * terminal (codec change, give-up, last-subscriber detach, pool disposal) still drains the queue before the terminal surfaces, exactly as it does without this option. */ discardOnDispose?: boolean; /** An abort signal that disposes this subscription (and only this one - a shared session lives as long as any subscriber remains). */ signal?: AbortSignal; /** * How many milliseconds this consumer can tolerate receiving no segment, pulled fresh at each decision and aggregated across a shared stream by the minimum. It serves * consumer-owned policies at once. First, the resilient recovery's await budget self-tunes from the consumer's real buffer headroom: a live view reports ~0 * (recover aggressively), a paced recording its cushion, a passive buffer a large value (wait patiently). Second, it is the consumer's MEDIA-stall detection deadline * for the pool's always-on (while live) media watchdog: declaring nothing leaves the default 10 s window (every stream is media-watched by default), a tighter value * tightens detection (clamped up to a small floor against jitter), and Infinity opts out of media-stall detection entirely (the session's any-byte heartbeat still * watches the socket). Omit it to leave the stream maximally patient on recovery and media-watched at the 10 s default. */ urgency?: () => number; } /** * Construction options for {@link LivestreamPool}. `resolveUrl` is the transport-backed negotiation hook (build it with {@link livestreamUrlResolver}, the shared {@link * wsEndpointResolver}'s livestream specialization); `webSocket` is the injected I/O dependency, shared with {@link EventStream}'s and threaded down to each session; * `recoveryPolicy` is the per-stream recovery decision authority, defaulting to {@link defaultLivestreamRecoveryPolicy}; `verifyTls` is the strict-TLS opt-in threaded * down to each session's owned agent, defaulting to `false` for the controller's self-signed certificate. * * @category Client */ export interface LivestreamPoolOptions { clock?: Clock; log?: ProtectLogging; recoveryPolicy?: RecoveryPolicy; resolveUrl: (params: URLSearchParams, opts: { signal?: AbortSignal; }) => Promise; verifyTls?: boolean; webSocket?: (url: string) => ProtectWebSocket; } interface SubscriptionHost { get initSegment(): { codec: string; data: Buffer; } | null; get state(): LivestreamSubscriptionState; reassess(): void; whenEstablished(): Promise; } /** * A consumer's handle into a pooled livestream. It is an `AsyncIterable` (the iterator *is* the surface - no callback rail) and `AsyncDisposable`. Each * subscription has its **own unbounded queue**: the pool pushes every segment to every subscriber, so one slow consumer cannot stall a fast one, and nothing is dropped. * * The stream is **resilient by default**: a recoverable stall or reconnect is invisible to the iterator - it pauses while the underlying session is re-established and * resumes uninterrupted when segments flow again (a same-codec reconnect suppresses the redundant init). The iterator terminates only on these conditions: disposal (a * clean `done`), a codec change across a reconnect (it throws {@link ProtectCodecChangeError}), and the recovery policy giving up (it throws {@link * ProtectLivestreamUnavailableError}). A recoverable stall is *not* one of them. * * @category Client */ export declare class LivestreamSubscription implements AsyncIterable, AsyncDisposable { #private; /** This subscription's unique id, correlated with the `subscription:created` / `:disposed` diagnostics. */ readonly id: string; constructor(opts: { clock: Clock; discardOnDispose: boolean; host: SubscriptionHost; onDispose: (subscription: LivestreamSubscription) => void; signal?: AbortSignal; }); /** The negotiated RFC 6381 codec descriptor once the init segment has arrived, otherwise `""`. Read through to the shared session's cached init, the single source. */ get codec(): string; /** * The cached initialization segment of the shared stream once it has arrived, otherwise `null`. Stable across same-codec reconnects (it is suppressed and the cache is * retained), and replaced only on a codec change - which is itself terminal, so a consumer never observes the cached init mutate beneath it mid-stream. */ get initSegment(): { codec: string; data: Buffer; } | null; /** The coarse, stable lifecycle state. `"closed"` once this subscription is disposed; else the shared stream's state (a recoverable stall reads `"recovering"`). */ get state(): LivestreamSubscriptionState; /** The live delivery counters for this subscription. `queueDepth` / `peakQueueDepth` let a consumer detect that it is falling behind. */ get stats(): LivestreamSubscriptionStats; /** * Iterate the segments of this subscription. The iterator yields every queued segment, then either ends (clean close / disposal) or throws the terminal error (codec * change or give-up). A recoverable stall is invisible - the iterator parks and resumes. Breaking out of the loop disposes the subscription. * * @returns An async iterator of segments. */ [Symbol.asyncIterator](): AsyncIterator; /** * Dispose the subscription: detach it from its shared session (decrementing the reference count, which tears the session down if it was the last), end any in-flight * iteration, and detach the consumer's abort listener. Safe to call more than once. */ [Symbol.asyncDispose](): Promise; /** * Ask the shared stream's recovery policy to re-decide now - for example when this consumer's urgency just changed. It only re-decides an *in-flight* recovery episode, * so on a healthy stream it is a no-op; it never forces a reconnect of a working connection. */ reassess(): void; /** * Resolve `true` once the shared stream has produced its first MEDIA segment, or `false` once this subscription's wait for it is over. Liveness is * media-keyed: a controller that connects and acks with an init but then produces no media is NOT established - it elapses its recovery window and reconnects, so this * resolves `true` only when media is actually flowing. It resolves `false` in every case where the wait is over without media: the shared stream gave up * establishing, or this subscription itself was disposed - its own signal aborting, an explicit `await using` exit, or breaking its iterator - so a departed * subscriber's own wait ends with the subscription rather than hanging on the shared establishment latch until the remaining subscribers happen to settle it. The * boolean deliberately does not distinguish these outcomes because every consumer's correct reaction is identical: stop waiting and clean up. * * @returns A promise resolving `true` once the first media segment flows, `false` if establishment was abandoned or this subscription was disposed first. */ whenEstablished(): Promise; push(segment: Segment): void; fail(error: ProtectError): void; close(): void; } /** * The ref-counted, multi-subscriber, resilient-by-default livestream pool. Construct it at the composition root with a transport-backed {@link livestreamUrlResolver} * (the shared {@link wsEndpointResolver}'s livestream specialization) and an optional {@link RecoveryPolicy}; consumers reach it through `camera.livestream(...)`. * * @category Client */ export declare class LivestreamPool implements AsyncDisposable { #private; constructor(options: LivestreamPoolOptions); /** * Subscribe to a camera livestream. Returns immediately with a {@link LivestreamSubscription}; segments begin flowing once the underlying session connects (or * immediately, replayed from the cached init, for a shared session already live). Subscribers asking for an identical stream share one underlying WebSocket, and the * shared session recovers under the pool's policy without failing any subscriber. * * @param spec - The stream to subscribe to. * @param opts - Per-subscription {@link LivestreamSubscribeOptions}: an optional abort signal that disposes this subscription (and only this one), an * optional `urgency` closure feeding the resilient recovery, and `discardOnDispose` dropping the undelivered queue on this consumer's own disposal. * * @returns A live subscription. */ subscribe(spec: LivestreamSpec, opts?: LivestreamSubscribeOptions): LivestreamSubscription; /** * Dispose the pool: close every managed session and clear the map. Called by the client's own disposal. */ [Symbol.asyncDispose](): Promise; } export {}; //# sourceMappingURL=livestream-pool.d.ts.map