@synapse-sdk/media 0.3.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/camera.d.ts CHANGED
@@ -1,12 +1,11 @@
1
- import type { H264Encoder } from "./encoder.ts";
2
1
  /** `MediaStreamTrackProcessor` is available (Chrome 94+). */
3
2
  export declare function trackProcessorSupported(): boolean;
4
3
  /** The `VideoFrame`s of a video track as a `ReadableStream` (`maxBufferSize` 1: the newest frame wins, older ones are dropped by the browser). */
5
4
  export declare function trackFrames(track: MediaStreamTrack): ReadableStream<VideoFrame>;
6
5
  /**
7
- * Pumps a camera track into the encoder until the track ends or `signal` aborts. Resolves with
8
- * the number of frames offered to the encoder (dropped ones included; see `encoder.stats()`).
6
+ * Pumps a camera track into `publish` (a video publisher's) until the track ends or `signal`
7
+ * aborts; each frame is closed after the call. Resolves with the number of frames handed over.
9
8
  */
10
- export declare function pumpTrack(track: MediaStreamTrack, encoder: H264Encoder, opts?: {
9
+ export declare function pumpTrack(track: MediaStreamTrack, publish: (frame: VideoFrame, tUs: number) => void, opts?: {
11
10
  signal?: AbortSignal;
12
11
  }): Promise<number>;
package/dist/camera.js CHANGED
@@ -1,7 +1,7 @@
1
- // Camera frames into the encoder: `MediaStreamTrackProcessor` (Chrome-only, not in TypeScript's
2
- // DOM lib, so it is reached through `globalThis`) turns a `MediaStreamTrack` into a stream of
3
- // `VideoFrame`s. Each frame is encoded with `tUs = nowUs()` at dequeue (stamp at capture, as
4
- // dylithium did) and closed right after.
1
+ // Camera frames into a video publisher: `MediaStreamTrackProcessor` (Chrome-only, not in
2
+ // TypeScript's DOM lib, so it is reached through `globalThis`) turns a `MediaStreamTrack` into a
3
+ // stream of `VideoFrame`s. Each frame is published with `tUs = nowUs()` at dequeue (stamp at
4
+ // capture, as dylithium did) and closed right after.
5
5
  import { SynapseError, nowUs } from "@synapse-sdk/core";
6
6
  /** `MediaStreamTrackProcessor` is available (Chrome 94+). */
7
7
  export function trackProcessorSupported() { return typeof globalThis.MediaStreamTrackProcessor === "function"; }
@@ -13,12 +13,14 @@ export function trackFrames(track) {
13
13
  return new Ctor({ track, maxBufferSize: 1 }).readable;
14
14
  }
15
15
  /**
16
- * Pumps a camera track into the encoder until the track ends or `signal` aborts. Resolves with
17
- * the number of frames offered to the encoder (dropped ones included; see `encoder.stats()`).
16
+ * Pumps a camera track into `publish` (a video publisher's) until the track ends or `signal`
17
+ * aborts; each frame is closed after the call. Resolves with the number of frames handed over.
18
18
  */
19
- export async function pumpTrack(track, encoder, opts = {}) {
19
+ export async function pumpTrack(track, publish, opts = {}) {
20
20
  const reader = trackFrames(track).getReader();
21
21
  const onAbort = () => { void reader.cancel(); };
22
+ if (opts.signal?.aborted)
23
+ onAbort();
22
24
  opts.signal?.addEventListener("abort", onAbort, { once: true });
23
25
  let n = 0;
24
26
  try {
@@ -27,7 +29,7 @@ export async function pumpTrack(track, encoder, opts = {}) {
27
29
  if (done || !value)
28
30
  break;
29
31
  try {
30
- encoder.encode(value, { tUs: nowUs() });
32
+ publish(value, nowUs());
31
33
  n++;
32
34
  }
33
35
  finally {
package/dist/canvas.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { VideoSink } from "./decoder.ts";
2
1
  export interface CanvasSinkOptions {
3
2
  /** Coalesce to the display refresh (drops superseded pictures). Default false: draw immediately. */
4
3
  vsync?: boolean;
@@ -12,8 +11,8 @@ export interface CanvasSink {
12
11
  readonly dropped: number;
13
12
  detach(): void;
14
13
  }
15
- /** Anything with a decoder-style `attachSink` (an `H264Decoder`, or a `VideoSubscriber` through its decoder). */
14
+ /** Anything with a decoder-style `attachSink` that hands over pictures the sink then owns (an `H264Decoder`). */
16
15
  export interface VideoSource {
17
- attachSink(sink: VideoSink): () => void;
16
+ attachSink(sink: (vf: VideoFrame) => void): () => void;
18
17
  }
19
18
  export declare function attachCanvas(source: VideoSource, canvas: HTMLCanvasElement, opts?: CanvasSinkOptions): CanvasSink;
package/dist/decoder.d.ts CHANGED
@@ -16,7 +16,7 @@ export interface H264DecoderOptions {
16
16
  hardwareAcceleration?: HardwareAcceleration;
17
17
  /** Sync policy overrides; the defaults are the shared 10/150/60 (`SYNC_DEFAULT`) with `startAtRecovery: false` (see the constructor). */
18
18
  sync?: Partial<SyncConfig>;
19
- /** The decoder has produced nothing for `stuckFrames` chunks: ask the publisher for an IDR (`VideoSubscriber` wires this to the `<topic>/idr` service). */
19
+ /** The decoder has produced nothing for `stuckFrames` chunks: ask the publisher for an IDR (`videoSubscriber` wires this to the `<topic>/idr` service). */
20
20
  onRequestIdr?: () => void;
21
21
  /** Diagnostics (configure, sync, reset, error lines). */
22
22
  onLog?: (msg: string) => void;
@@ -41,7 +41,7 @@ export declare class H264Decoder {
41
41
  private readonly opts;
42
42
  /** The shared sync policy (read `sync.state`, `sync.stats`). */
43
43
  readonly sync: DecoderSync;
44
- /** Stuck signal; set by the options or later by the owner (`VideoSubscriber` does). */
44
+ /** Stuck signal; set by the options or later by the owner (`videoSubscriber` does). */
45
45
  onRequestIdr: (() => void) | undefined;
46
46
  /** Observer of every decoded picture (stats, latency), called before the sink. */
47
47
  onOutput: ((meta: DecodedFrameMeta) => void) | undefined;
package/dist/decoder.js CHANGED
@@ -9,14 +9,14 @@
9
9
  // → drop the decoder and wait for the next start point. Frames are handed to one sink as they
10
10
  // decode; the sink owns them and must `close()` them (`attachCanvas` does).
11
11
  import { nowUs } from "@synapse-sdk/core";
12
- import { codecStringFromSps } from "./annexb.js";
12
+ import { codecStringFromSps } from "@synapse-sdk/core";
13
13
  import { DecoderSync } from "./sync.js";
14
14
  import { DEFAULT_H264_CODEC, requireWebCodecs } from "./webcodecs.js";
15
15
  export class H264Decoder {
16
16
  opts;
17
17
  /** The shared sync policy (read `sync.state`, `sync.stats`). */
18
18
  sync;
19
- /** Stuck signal; set by the options or later by the owner (`VideoSubscriber` does). */
19
+ /** Stuck signal; set by the options or later by the owner (`videoSubscriber` does). */
20
20
  onRequestIdr;
21
21
  /** Observer of every decoded picture (stats, latency), called before the sink. */
22
22
  onOutput;
@@ -36,7 +36,7 @@ export class H264Decoder {
36
36
  requireWebCodecs("H264Decoder");
37
37
  // Chromium's VideoDecoder checks that a `key` chunk carries an IDR (Annex-B without `description`):
38
38
  // a recovery-point start throws "A key frame is required after configure()", so WebCodecs waits for
39
- // an IDR (the owner asks for one through `onRequestIdr`; `VideoSubscriber` does so while waiting).
39
+ // an IDR (the owner asks for one through `onRequestIdr`; the sync policy raises it while waiting too).
40
40
  this.sync = new DecoderSync({ startAtRecovery: false, ...opts.sync });
41
41
  this.onRequestIdr = opts.onRequestIdr;
42
42
  }
package/dist/encoder.js CHANGED
@@ -1,15 +1,14 @@
1
1
  // WebCodecs H.264 encoder for a live stream (R27 §4; dylithium had no browser encoder).
2
2
  //
3
3
  // `avc: { format: "annexb" }` puts SPS/PPS in-band on every key chunk, so the bytes are exactly
4
- // what `MediaFrame.data` carries (no avcC, no repackaging) and the flags are read off the
4
+ // what a `video/h264` topic carries (no avcC, no repackaging) and the flags are read off the
5
5
  // bitstream like every other Synapse publisher. `latencyMode: "realtime"`, `bitrateMode:
6
6
  // "constant"`, `hardwareAcceleration: "no-preference"` (Chrome picks; software OpenH264 in
7
7
  // headless Chromium). WebCodecs has no GOP and no intra-refresh: keyframes happen on the first
8
- // frame, on `forceIdr()` (the `<topic>/idr` service) and optionally every `keyframeInterval`
8
+ // frame, on `forceIdr()` (the `<topic>/idr` service `videoPublisher` serves) and optionally every `keyframeInterval`
9
9
  // frames; a receiver's policy for a browser publisher is therefore IDR-only, which is fine.
10
10
  // Captured frames are dropped when the encoder queue is behind, encoded ones never (lessons register).
11
- import { nowUs } from "@synapse-sdk/core";
12
- import { mediaFrameFromAnnexb } from "./annexb.js";
11
+ import { auParamSets, auSyncPoints, nowUs } from "@synapse-sdk/core";
13
12
  import { requireWebCodecs } from "./webcodecs.js";
14
13
  /** Lowest H.264 level (level_idc) whose Table A-1 limits cover the picture rate, frame size and Baseline bitrate. */
15
14
  export function h264LevelIdc(width, height, fps, bitrateBps) {
@@ -136,10 +135,9 @@ export class H264Encoder {
136
135
  #onChunk(chunk, _meta) {
137
136
  const data = new Uint8Array(chunk.byteLength);
138
137
  chunk.copyTo(data);
139
- const f = mediaFrameFromAnnexb(data, chunk.timestamp);
140
- if (chunk.type === "key")
141
- f.key = true; // R27 §4: a key chunk is an IDR (the bitstream says so too)
142
- f.stages = { encodedUs: nowUs(), sentUs: 0 };
138
+ // Flags from the bitstream like every Synapse publisher; R27 §4: a key chunk is an IDR (the bitstream says so too).
139
+ const { idr, recovery } = auSyncPoints(data);
140
+ const f = { data, tUs: chunk.timestamp, key: idr || chunk.type === "key", config: auParamSets(data) !== undefined, recovery, stages: { encodedUs: nowUs(), sentUs: 0 } };
143
141
  const started = this.#inflight.get(chunk.timestamp);
144
142
  if (started !== undefined) {
145
143
  this.#inflight.delete(chunk.timestamp);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from "./annexb.ts";
1
+ export { AuSplitter, NAL_TYPE, SEI_RECOVERY_POINT, auParamSets, auSyncPoints, codecStringFromSps, concatBytes, findSps, findStartCode, hasVcl, isKeyframe, isVclType, nalIsVcl, nalPayload, nalType, nalUnits, nalWithStartCode, rbspUnescape, seiHasRecoveryPoint, seiPayloadTypes, splitAccessUnits, type Nal, } from "@synapse-sdk/core";
2
2
  export * from "./reinsert.ts";
3
3
  export * from "./sync.ts";
4
4
  export * from "./webcodecs.ts";
@@ -6,6 +6,6 @@ export * from "./decoder.ts";
6
6
  export * from "./canvas.ts";
7
7
  export * from "./encoder.ts";
8
8
  export * from "./video.ts";
9
- export * from "./camera.ts";
10
- export type { MediaFrame, MediaLayout, Stages } from "@synapse-sdk/core";
9
+ export { trackFrames, trackProcessorSupported } from "./camera.ts";
10
+ export type { MediaFrame, Stages } from "@synapse-sdk/core";
11
11
  export { isStartPoint, mediaFlags, mediaFrameFromHeader, mediaHeader, stagesOf } from "@synapse-sdk/core";
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- // @synapse-sdk/media: H.264 over Synapse in the browser (stage plan J1/J2).
2
- // - `annexb`: the Annex-B walkers shared with `crates/synapse-media` (flags from the bitstream).
1
+ // @synapse-sdk/media: H.264 video over Synapse in the browser.
2
+ // - `videoPublisher(node, topic)` / `videoSubscriber(node, topic)`: pictures in, pictures out.
3
+ // - `H264Encoder` / `H264Decoder`: WebCodecs, for a two-step path of your own (constructors throw `not_supported` without WebCodecs).
3
4
  // - `ParamSetCache` / `DecoderSync`: the publisher re-insertion and decoder sync policies, identical in every SDK.
4
- // - `H264Decoder` / `H264Encoder`: WebCodecs (browser only; constructors throw `not_supported` elsewhere).
5
- // - `attachCanvas`, `VideoPublisher`, `VideoSubscriber`, `h264Topic`, camera helpers.
5
+ // - The Annex-B walkers (flags from the bitstream), `attachCanvas`, camera helpers.
6
6
  // The React hook lives in `@synapse-sdk/media/react`.
7
- export * from "./annexb.js";
7
+ export { AuSplitter, NAL_TYPE, SEI_RECOVERY_POINT, auParamSets, auSyncPoints, codecStringFromSps, concatBytes, findSps, findStartCode, hasVcl, isKeyframe, isVclType, nalIsVcl, nalPayload, nalType, nalUnits, nalWithStartCode, rbspUnescape, seiHasRecoveryPoint, seiPayloadTypes, splitAccessUnits, } from "@synapse-sdk/core";
8
8
  export * from "./reinsert.js";
9
9
  export * from "./sync.js";
10
10
  export * from "./webcodecs.js";
@@ -12,5 +12,5 @@ export * from "./decoder.js";
12
12
  export * from "./canvas.js";
13
13
  export * from "./encoder.js";
14
14
  export * from "./video.js";
15
- export * from "./camera.js";
15
+ export { trackFrames, trackProcessorSupported } from "./camera.js";
16
16
  export { isStartPoint, mediaFlags, mediaFrameFromHeader, mediaHeader, stagesOf } from "@synapse-sdk/core";
package/dist/react.d.ts CHANGED
@@ -1,24 +1,15 @@
1
1
  import type { Synapse, TopicRef } from "@synapse-sdk/client";
2
2
  import { type SynapseError } from "@synapse-sdk/core";
3
3
  import { type CanvasSinkOptions } from "./canvas.ts";
4
- import { type H264DecoderOptions } from "./decoder.ts";
5
- import { VideoSubscriber, type VideoStats, type VideoSubscriberOptions } from "./video.ts";
6
- export interface UseVideoOptions extends VideoSubscriberOptions, CanvasSinkOptions {
4
+ export interface UseVideoOptions extends CanvasSinkOptions {
7
5
  /** The node to use instead of the one from `<SynapseProvider>`. */
8
6
  node?: Synapse;
9
- decoder?: H264DecoderOptions;
10
- /** Stats re-render rate; default 2 Hz; 0 disables. */
11
- statsHz?: number;
12
7
  }
13
8
  export interface UseVideoResult {
14
9
  /** Put this on the `<canvas>`: `<canvas ref={ref} />`. */
15
10
  ref: (el: HTMLCanvasElement | null) => void;
16
- /** Last stats snapshot, undefined until the first tick. */
17
- stats: VideoStats | undefined;
18
11
  /** `not_supported` when WebCodecs is missing, or whatever failed while setting up. */
19
12
  error: SynapseError | undefined;
20
- /** The live subscriber (undefined while there is no canvas or the topic is `"skip"`). */
21
- subscriber: VideoSubscriber | undefined;
22
13
  }
23
- /** Decodes an H.264 topic into a canvas. `topic` may be `"skip"` to render nothing without subscribing. */
14
+ /** Decodes a video topic into a canvas. `topic` may be `"skip"` to render nothing without subscribing. */
24
15
  export declare function useVideo(topic: TopicRef<Uint8Array, Uint8Array> | "skip", opts?: UseVideoOptions): UseVideoResult;
package/dist/react.js CHANGED
@@ -1,48 +1,52 @@
1
- // `@synapse-sdk/media/react`: `useVideo(topic)` — a canvas ref callback plus live stats. The
2
- // per-frame path (decoder → canvas) never touches React; only the stats snapshot re-renders, at
3
- // `statsHz` (default 2). Needs a `<SynapseProvider>` (from `@synapse-sdk/react`) or `opts.node`.
1
+ // `@synapse-sdk/media/react`: `useVideo(topic)` — a canvas ref callback for a video topic, on
2
+ // `videoSubscriber`. The per-frame path (decoder → canvas) never touches React. Needs a
3
+ // `<SynapseProvider>` (from `@synapse-sdk/react`) or `opts.node`.
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
5
  import { toSynapseError } from "@synapse-sdk/core";
6
6
  import { useSynapseOptional } from "@synapse-sdk/react";
7
7
  import { attachCanvas } from "./canvas.js";
8
- import { H264Decoder } from "./decoder.js";
9
- import { VideoSubscriber } from "./video.js";
10
- /** Decodes an H.264 topic into a canvas. `topic` may be `"skip"` to render nothing without subscribing. */
8
+ import { videoSubscriber } from "./video.js";
9
+ /** Decodes a video topic into a canvas. `topic` may be `"skip"` to render nothing without subscribing. */
11
10
  export function useVideo(topic, opts = {}) {
12
11
  const ctxNode = useSynapseOptional();
13
12
  const node = opts.node ?? ctxNode;
14
13
  const [canvas, setCanvas] = useState(null);
15
- const [stats, setStats] = useState(undefined);
16
14
  const [error, setError] = useState(undefined);
17
- const [subscriber, setSubscriber] = useState(undefined);
18
15
  const optsRef = useRef(opts);
19
16
  optsRef.current = opts;
20
17
  const ref = useCallback((el) => setCanvas(el), []);
21
18
  const topicName = topic === "skip" ? undefined : typeof topic === "string" ? topic : topic.name;
22
- const statsHz = opts.statsHz ?? 2;
23
19
  useEffect(() => {
24
- if (!canvas || !node || topic === "skip") {
25
- setSubscriber(undefined);
26
- setStats(undefined);
20
+ if (!canvas || !node || topic === "skip")
27
21
  return;
28
- }
29
22
  const o = optsRef.current;
30
- let decoder, sub;
31
23
  try {
32
- decoder = new H264Decoder(o.decoder);
33
- const sink = attachCanvas(decoder, canvas, { vsync: o.vsync, resize: o.resize });
34
- sub = new VideoSubscriber(node, topic, decoder, o);
35
- setSubscriber(sub);
24
+ // The canvas sink owns each picture handed to it (draws, then closes it).
25
+ let sink;
26
+ const drawn = attachCanvas({ attachSink: s => { sink = s; return () => { if (sink === s)
27
+ sink = undefined; }; } }, canvas, { vsync: o.vsync, resize: o.resize });
28
+ let video;
29
+ try {
30
+ video = videoSubscriber(node, topic);
31
+ }
32
+ catch (e) {
33
+ drawn.detach();
34
+ throw e;
35
+ }
36
+ void (async () => { for await (const vf of video) {
37
+ if (sink)
38
+ sink(vf);
39
+ else
40
+ vf.close();
41
+ } })();
36
42
  setError(undefined);
37
- const timer = statsHz > 0 ? setInterval(() => setStats(sub.stats()), 1000 / statsHz) : undefined;
38
- return () => { if (timer)
39
- clearInterval(timer); sub.close(); sink.detach(); decoder.close(); };
43
+ return () => { video.close(); drawn.detach(); };
40
44
  }
41
45
  catch (e) {
42
46
  setError(toSynapseError(e, "not_supported", "sdk"));
43
47
  return;
44
48
  }
45
49
  // eslint-disable-next-line react-hooks/exhaustive-deps
46
- }, [node, canvas, topicName, statsHz]);
47
- return { ref, stats, error, subscriber };
50
+ }, [node, canvas, topicName]);
51
+ return { ref, error };
48
52
  }
package/dist/reinsert.js CHANGED
@@ -1,4 +1,4 @@
1
- import { auParamSets, concatBytes } from "./annexb.js";
1
+ import { auParamSets, concatBytes } from "@synapse-sdk/core";
2
2
  /** Remembers the most recent SPS+PPS (with start codes) and re-inserts them where a decoder needs them. */
3
3
  export class ParamSetCache {
4
4
  #paramSets;
package/dist/sync.js CHANGED
@@ -10,6 +10,8 @@ export class DecoderSync {
10
10
  #startedAtRecovery = false;
11
11
  #syncChunk = false;
12
12
  #fedSinceOutput = 0;
13
+ /** Frames since the last IDR request (or since the output or start): the stuck rule. */
14
+ #fedSinceRequest = 0;
13
15
  constructor(cfg = {}) { this.cfg = { ...SYNC_DEFAULT, ...cfg }; }
14
16
  get state() { return this.#state; }
15
17
  get paramSets() { return this.cache.paramSets; }
@@ -36,18 +38,29 @@ export class DecoderSync {
36
38
  this.stats.startsAtRecovery++;
37
39
  }
38
40
  else {
41
+ // still waiting: after a stuck window of dropped frames, ask for an IDR
39
42
  this.stats.dropped++;
40
- return { feed: undefined, keyChunk: false, started: false, signal: undefined };
43
+ this.#fedSinceOutput++;
44
+ this.#fedSinceRequest++;
45
+ let signal;
46
+ if (this.#fedSinceRequest >= this.cfg.stuckFrames) {
47
+ this.#fedSinceRequest = 0;
48
+ this.stats.idrRequests++;
49
+ signal = "request_idr";
50
+ }
51
+ return { feed: undefined, keyChunk: false, started: false, signal };
41
52
  }
42
53
  this.#state = "primed";
43
54
  this.#syncChunk = true;
44
55
  this.#fedSinceOutput = 0;
56
+ this.#fedSinceRequest = 0;
45
57
  this.stats.starts++;
46
58
  started = true;
47
59
  }
48
60
  const keyChunk = frame.key || this.#syncChunk;
49
61
  this.#syncChunk = false;
50
62
  this.#fedSinceOutput++;
63
+ this.#fedSinceRequest++;
51
64
  this.stats.fed++;
52
65
  return { feed: data, keyChunk, started, signal: this.#checkStuck() };
53
66
  }
@@ -57,9 +70,9 @@ export class DecoderSync {
57
70
  this.#reset();
58
71
  return "reset";
59
72
  }
60
- if (this.#fedSinceOutput >= tolerated) {
61
- // Give the IDR a full window before asking again.
62
- this.#fedSinceOutput = 0;
73
+ if (this.#fedSinceRequest >= tolerated) {
74
+ // Give the IDR a full window before asking again (the wedged count runs on).
75
+ this.#fedSinceRequest = 0;
63
76
  this.#startedAtRecovery = false;
64
77
  this.stats.idrRequests++;
65
78
  return "request_idr";
@@ -69,6 +82,7 @@ export class DecoderSync {
69
82
  /** The decoder produced a frame. */
70
83
  onOutput() {
71
84
  this.#fedSinceOutput = 0;
85
+ this.#fedSinceRequest = 0;
72
86
  this.#startedAtRecovery = false;
73
87
  this.stats.output++;
74
88
  if (this.#state === "primed")
@@ -81,6 +95,7 @@ export class DecoderSync {
81
95
  this.#syncChunk = false;
82
96
  this.#startedAtRecovery = false;
83
97
  this.#fedSinceOutput = 0;
98
+ this.#fedSinceRequest = 0;
84
99
  this.stats.resets++;
85
100
  }
86
101
  }
package/dist/video.d.ts CHANGED
@@ -1,124 +1,47 @@
1
- import { type MediaFrame, type MediaLayout, type TopicContract, type TopicRef } from "@synapse-sdk/core";
2
- import type { Envelope, Publisher, Registration, Subscription, Synapse } from "@synapse-sdk/client";
3
- import type { H264Decoder } from "./decoder.ts";
4
- import type { H264Encoder } from "./encoder.ts";
5
- import type { SyncState } from "./sync.ts";
6
- export declare const H264 = "video/h264";
7
- export interface H264TopicOptions {
8
- /** RaptorQ repair percentage; default 30 (`repairPct: 0` = source symbols only). */
9
- repairPct?: number;
10
- /** FEC give-up budget, ms; default 50 (video). */
11
- deadlineMs?: number;
12
- /** One MoQ group per frame with the relay caching the latest, instead of datagrams (a poster-frame topic, not live video). */
13
- retain?: boolean;
14
- }
15
- /** A raw topic for H.264 access units: `contentType: "video/h264"`, `layout: {codec: "h264", stream: "annexb", ...}`, datagrams with FEC. */
16
- export declare function h264Topic(name: string, layout?: Omit<MediaLayout, "codec" | "stream">, opts?: H264TopicOptions): TopicContract<Uint8Array, Uint8Array>;
17
- /** The IDR-on-demand service a video publisher serves (decision 4): `<topic>/idr`, no arguments, `{ ok: true }`. */
18
- export declare function idrServiceName(topic: string): string;
19
- export interface VideoPublisherOptions {
20
- /** Serve `<topic>/idr` → `encoder.forceIdr()`; default true. */
21
- idrService?: boolean;
22
- }
23
- export interface VideoPublisherStats {
24
- /** Access units handed to `publishMedia`. */
25
- sent: number;
26
- /** `<topic>/idr` calls served. */
27
- idrCalls: number;
28
- /** Frames the SPS/PPS re-insertion policy touched (WebCodecs streams: none, key chunks carry them). */
29
- reinserted: number;
1
+ import { type TopicRef } from "@synapse-sdk/core";
2
+ import type { Envelope, Synapse } from "@synapse-sdk/client";
3
+ import { H264Encoder, type H264EncoderOptions } from "./encoder.ts";
4
+ /** `videoPublisher` settings: the `H264Encoder` knobs a caller may want, all with defaults. */
5
+ export type VideoPublisherOptions = Pick<H264EncoderOptions, "fps" | "bitrate" | "hardwareAcceleration">;
6
+ /** Pictures in, `video/h264` out (`videoPublisher`). */
7
+ export interface VideoPublisher {
8
+ /**
9
+ * Encodes and publishes `frame`, stamped `tUs` (the capture time, if you have one; default now).
10
+ * Synchronous; the encoder drops a picture rather than queue behind (see `encoder.stats()`). The
11
+ * first picture opens the encoder at its size. The frame stays yours to close.
12
+ */
13
+ publish(frame: VideoFrame, tUs?: number): void;
14
+ /** Publishes a camera track's frames until it ends or `signal` aborts (Chrome's `MediaStreamTrackProcessor`); resolves with the frames handed over. */
15
+ pumpTrack(track: MediaStreamTrack, opts?: {
16
+ signal?: AbortSignal;
17
+ }): Promise<number>;
18
+ /** Ends the topic, the `<topic>/idr` service and the encoder. */
19
+ close(): void;
20
+ /** The encoder, once the first picture made it. */
21
+ readonly encoder: H264Encoder | undefined;
30
22
  }
31
23
  /**
32
- * Encoder output → `publisher.publishMedia()`: `t_us` = capture (the `VideoFrame.timestamp`),
33
- * `STAGES` = encoder stamp + `sentUs` 0 (the publisher stamps it at the write, WP §4.1), SPS/PPS
34
- * re-insertion at recovery points, plus the `<topic>/idr` service that forces the next frame to be
35
- * an IDR. `VideoPublisher.open()` resolves once the runtime admitted the topic (and the service).
24
+ * A video publisher on `topic` (a name, or a `video/h264` topic: datagrams with FEC unless the
25
+ * contract says otherwise). Resolves once the topic and its `<topic>/idr` service are admitted.
36
26
  */
37
- export declare class VideoPublisher {
38
- #private;
39
- readonly node: Synapse;
40
- readonly topic: TopicContract<Uint8Array, Uint8Array>;
41
- readonly publisher: Publisher<Uint8Array>;
42
- readonly registration: Registration | undefined;
43
- readonly encoder: H264Encoder;
44
- private constructor();
45
- static open(node: Synapse, topic: TopicRef<Uint8Array, Uint8Array>, encoder: H264Encoder, opts?: VideoPublisherOptions): Promise<VideoPublisher>;
46
- stats(): VideoPublisherStats;
47
- /** Stops publishing (the encoder stays yours to close). */
27
+ export declare function videoPublisher(node: Synapse, topic: TopicRef<Uint8Array, Uint8Array>, opts?: VideoPublisherOptions): Promise<VideoPublisher>;
28
+ /** Decoded pictures of a `video/h264` topic (`videoSubscriber`): `for await` them (you close each), or get them through `onFrame`. */
29
+ export interface VideoSubscriber extends AsyncIterable<VideoFrame> {
30
+ /** Stops the subscription and the decoder; pictures not yet read are closed. */
48
31
  close(): void;
49
32
  }
50
33
  export interface VideoSubscriberOptions {
51
- /** Call `<topic>/idr` when the decoder is stuck and while it waits for a start point; default true. */
52
- requestIdr?: boolean;
53
- /** Minimum spacing between IDR requests, ms; default 1000. */
54
- idrMinIntervalMs?: number;
55
- /** Subscription priority 0..7; default 2. */
56
- priority?: number;
57
- }
58
- /**
59
- * Latency split over the last stats window (averages, ms). Undefined when the stamps needed are
60
- * not there. `e2eMs` and `netMs` cross the publisher's clock and ours: exact when it shares our time
61
- * domain, otherwise through the broker's clock (`node.toLocalUs`, SDK §4.4) and reported with
62
- * `epsMs`, the error bound; undefined while the publisher's domain has no offset yet (no judgment).
63
- */
64
- export interface VideoLatency {
65
- /** Capture → decoded picture, `± epsMs`. */
66
- e2eMs: number | undefined;
67
- /** Capture → encoder output (`STAGES.encoded_us`; one clock). */
68
- encMs: number | undefined;
69
- /** Encoder output → transport write (`STAGES.sent_us`; one clock). */
70
- sendMs: number | undefined;
71
- /** Transport write → our receive, `± epsMs`. */
72
- netMs: number | undefined;
73
- /** `decode()` → output. */
74
- decodeMs: number | undefined;
75
- /** Error bound on `e2eMs`/`netMs` from the clock conversion: 0 in our own domain, the two estimates' bounds added across domains, undefined when the clocks are not comparable (then both are undefined too). */
76
- epsMs: number | undefined;
77
- }
78
- export interface VideoStats {
79
- /** Decoded pictures per second over the window. */
80
- fps: number;
81
- /** Received bit rate over the window. */
82
- kbps: number;
83
- /** Totals since the subscriber was created. */
84
- received: number;
85
- decoded: number;
86
- /** Frames not decoded: dropped while waiting for a start point, plus frames superseded before they were read. */
87
- dropped: number;
88
- superseded: number;
89
- resets: number;
90
- idrRequests: number;
91
- /** Decoder queue depth right now. */
92
- queue: number;
93
- width: number;
94
- height: number;
95
- state: SyncState;
96
- latency: VideoLatency;
97
- }
98
- /** A received envelope on a raw media topic as a `MediaFrame` (the header's flags and `STAGES`). */
99
- export declare function mediaFrameOf(env: Envelope<Uint8Array>): MediaFrame;
100
- /**
101
- * A raw subscription feeding an `H264Decoder`. Every envelope becomes a `MediaFrame` and goes to
102
- * `decoder.push()`; the decoder's stuck signal and the waiting state (a late joiner on an IDR-only
103
- * stream sees nothing until an IDR comes) both call the publisher's `<topic>/idr` service, at most
104
- * once per `idrMinIntervalMs`. Stats: fps, totals, and the latency split from `t_us` and `STAGES`.
105
- */
106
- export declare class VideoSubscriber {
107
- #private;
108
- readonly node: Synapse;
109
- readonly decoder: H264Decoder;
110
- private readonly opts;
111
- readonly topic: TopicContract<Uint8Array, Uint8Array>;
112
- readonly sub: Subscription<Uint8Array>;
113
- constructor(node: Synapse, topic: TopicRef<Uint8Array, Uint8Array>, decoder: H264Decoder, opts?: VideoSubscriberOptions);
114
- /** Window averages since the previous call plus totals; resets the window. */
115
- stats(): VideoStats;
116
- /** IDR calls that failed (no publisher serving `<topic>/idr`, or timed out). */
117
- get idrFailed(): number;
118
34
  /**
119
- * Stops the subscription and gives the decoder back its previous callbacks (the decoder stays
120
- * yours to close). Close subscribers sharing one decoder newest first: a subscriber whose
121
- * callbacks another one wrapped since cannot take them back, so the newer one still calls them.
35
+ * Each decoded picture with the envelope of the access unit it came from (`tUs` = capture time,
36
+ * `key`, `dom`, publisher). The picture is closed when the callback returns; `clone()` it to keep
37
+ * it. With `onFrame` the subscriber yields nothing.
122
38
  */
123
- close(): void;
39
+ onFrame?: (frame: VideoFrame, meta: Envelope<Uint8Array>) => void;
124
40
  }
41
+ /**
42
+ * Decoded pictures of the video topic `topic` (by name; the established descriptor says what it
43
+ * carries). Every frame is fed to the decoder in order and every picture it gives is delivered.
44
+ * Frames before a start point are dropped by the decoder's policy, which asks the publisher for an
45
+ * IDR when stuck or waiting. Throws `not_supported` where WebCodecs is missing.
46
+ */
47
+ export declare function videoSubscriber(node: Synapse, topic: TopicRef<Uint8Array, Uint8Array>, opts?: VideoSubscriberOptions): VideoSubscriber;