supervision 0.1.7 → 0.2.0-next.1

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.
Files changed (182) hide show
  1. package/dist/constants/media-renderer.d.ts +3 -0
  2. package/dist/constants/media-renderer.d.ts.map +1 -1
  3. package/dist/detections/chunked-detection-frame-source.d.ts.map +1 -1
  4. package/dist/index.d.ts +8 -4
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +9281 -5038
  7. package/dist/index.js.map +1 -1
  8. package/dist/mask-preparation.worker.js +409 -185
  9. package/dist/mask-preparation.worker.js.map +1 -1
  10. package/dist/media/display-pixel-ratio.d.ts +15 -0
  11. package/dist/media/display-pixel-ratio.d.ts.map +1 -0
  12. package/dist/media/engine-import-failure.d.ts +16 -0
  13. package/dist/media/engine-import-failure.d.ts.map +1 -0
  14. package/dist/media/media-errors.d.ts +6 -0
  15. package/dist/media/media-errors.d.ts.map +1 -1
  16. package/dist/media/media-normalization.d.ts.map +1 -1
  17. package/dist/media/media-probe.d.ts.map +1 -1
  18. package/dist/media/media-source-state.d.ts.map +1 -1
  19. package/dist/media/media-source.d.ts +17 -0
  20. package/dist/media/media-source.d.ts.map +1 -1
  21. package/dist/media/mediabunny-media-source.d.ts.map +1 -1
  22. package/dist/media/video-engine-media-source.d.ts +54 -0
  23. package/dist/media/video-engine-media-source.d.ts.map +1 -0
  24. package/dist/media/video-engine-media-source.js +3 -0
  25. package/dist/media/video-engine-media-source.js.map +1 -0
  26. package/dist/playback/media-playback-controller.d.ts +7 -1
  27. package/dist/playback/media-playback-controller.d.ts.map +1 -1
  28. package/dist/render-preparation/mask-frame-artifact.d.ts +48 -12
  29. package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
  30. package/dist/render-preparation/mask-frame-compositor.d.ts +12 -5
  31. package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
  32. package/dist/render-preparation/mask-frame-preparer.d.ts.map +1 -1
  33. package/dist/render-preparation/mask-preparation-worker-count.d.ts +4 -4
  34. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +13 -3
  35. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
  36. package/dist/render-preparation/prepared-render-window.d.ts +44 -2
  37. package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
  38. package/dist/render-preparation/prepared-window-timeline.d.ts +2 -6
  39. package/dist/render-preparation/prepared-window-timeline.d.ts.map +1 -1
  40. package/dist/renderers/injected-pixi.d.ts +38 -0
  41. package/dist/renderers/injected-pixi.d.ts.map +1 -0
  42. package/dist/renderers/mask-palette.d.ts +14 -0
  43. package/dist/renderers/mask-palette.d.ts.map +1 -0
  44. package/dist/renderers/mask-vertex.d.ts +14 -0
  45. package/dist/renderers/mask-vertex.d.ts.map +1 -0
  46. package/dist/renderers/media-renderer-core.d.ts +9 -0
  47. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  48. package/dist/renderers/media-renderer-scene.d.ts +50 -2
  49. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  50. package/dist/renderers/media-renderer-state.d.ts +20 -1
  51. package/dist/renderers/media-renderer-state.d.ts.map +1 -1
  52. package/dist/renderers/media-renderer-transport.d.ts +72 -0
  53. package/dist/renderers/media-renderer-transport.d.ts.map +1 -0
  54. package/dist/renderers/pixi-box-layer.d.ts +5 -0
  55. package/dist/renderers/pixi-box-layer.d.ts.map +1 -1
  56. package/dist/renderers/pixi-focus-layer.d.ts +19 -2
  57. package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
  58. package/dist/renderers/pixi-frame-present.d.ts +77 -0
  59. package/dist/renderers/pixi-frame-present.d.ts.map +1 -0
  60. package/dist/renderers/pixi-id-mask-shader.d.ts +6 -25
  61. package/dist/renderers/pixi-id-mask-shader.d.ts.map +1 -1
  62. package/dist/renderers/pixi-interaction-layer.d.ts.map +1 -1
  63. package/dist/renderers/pixi-interaction-presentation-layer.d.ts +12 -2
  64. package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
  65. package/dist/renderers/pixi-mask-halo.d.ts +18 -23
  66. package/dist/renderers/pixi-mask-halo.d.ts.map +1 -1
  67. package/dist/renderers/pixi-mask-layer.d.ts +94 -32
  68. package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
  69. package/dist/renderers/pixi-media-scene.d.ts +67 -0
  70. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  71. package/dist/renderers/pixi-polygon-layer.d.ts +9 -2
  72. package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
  73. package/dist/renderers/pixi-region-coverage-mask.d.ts +62 -0
  74. package/dist/renderers/pixi-region-coverage-mask.d.ts.map +1 -0
  75. package/dist/renderers/pixi-region-effect.d.ts +42 -0
  76. package/dist/renderers/pixi-region-effect.d.ts.map +1 -0
  77. package/dist/renderers/pixi-region-layer.d.ts +43 -2
  78. package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
  79. package/dist/renderers/pixi-shader-lifecycle.d.ts +14 -0
  80. package/dist/renderers/pixi-shader-lifecycle.d.ts.map +1 -0
  81. package/dist/renderers/pixi-vector-layer.d.ts.map +1 -1
  82. package/dist/renderers/prepared-annotation-window.d.ts +47 -0
  83. package/dist/renderers/prepared-annotation-window.d.ts.map +1 -0
  84. package/dist/renderers/presented-frame-channel.d.ts +138 -0
  85. package/dist/renderers/presented-frame-channel.d.ts.map +1 -0
  86. package/dist/renderers/scene-render-scheduler.d.ts +20 -0
  87. package/dist/renderers/scene-render-scheduler.d.ts.map +1 -0
  88. package/dist/sessions/media-session-defaults.d.ts +10 -6
  89. package/dist/sessions/media-session-defaults.d.ts.map +1 -1
  90. package/dist/sessions/media-session-media.d.ts.map +1 -1
  91. package/dist/sessions/media-session-state.d.ts.map +1 -1
  92. package/dist/sessions/media-session.d.ts.map +1 -1
  93. package/dist/tracking.worker.js +73 -4
  94. package/dist/tracking.worker.js.map +1 -1
  95. package/dist/types/media-normalization.d.ts +34 -0
  96. package/dist/types/media-normalization.d.ts.map +1 -1
  97. package/dist/types/media-renderer.d.ts +31 -1
  98. package/dist/types/media-renderer.d.ts.map +1 -1
  99. package/dist/types/media-session.d.ts +89 -15
  100. package/dist/types/media-session.d.ts.map +1 -1
  101. package/dist/types/render-preparation.d.ts +149 -5
  102. package/dist/types/render-preparation.d.ts.map +1 -1
  103. package/dist/video-engine-media-source-CUXnOJOV.js +450 -0
  104. package/dist/video-engine-media-source-CUXnOJOV.js.map +1 -0
  105. package/dist/web-video-engine/analysis-session.d.ts +75 -0
  106. package/dist/web-video-engine/analysis.d.ts +6 -0
  107. package/dist/web-video-engine/analysis.js +1850 -0
  108. package/dist/web-video-engine/cache-budget.d.ts +18 -0
  109. package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +89 -0
  110. package/dist/web-video-engine/clock.d.ts +82 -0
  111. package/dist/web-video-engine/constants.d.ts +261 -0
  112. package/dist/web-video-engine/create-scrub-cursor.d.ts +65 -0
  113. package/dist/web-video-engine/decode-resolution.d.ts +114 -0
  114. package/dist/web-video-engine/decode-scheduler.d.ts +371 -0
  115. package/dist/web-video-engine/decode-session.d.ts +283 -0
  116. package/dist/web-video-engine/decode-source.d.ts +258 -0
  117. package/dist/web-video-engine/diagnostics-store.d.ts +17 -0
  118. package/dist/web-video-engine/diagnostics.d.ts +316 -0
  119. package/dist/web-video-engine/embedded-engine-worker.d.ts +1 -0
  120. package/dist/web-video-engine/engine-core.d.ts +269 -0
  121. package/dist/web-video-engine/engine.d.ts +25 -0
  122. package/dist/web-video-engine/engine.js +912 -0
  123. package/dist/web-video-engine/engine.worker.d.ts +1 -0
  124. package/dist/web-video-engine/engine.worker.js +33624 -0
  125. package/dist/web-video-engine/frame-cache.d.ts +236 -0
  126. package/dist/web-video-engine/frame-extractor.d.ts +27 -0
  127. package/dist/web-video-engine/frame-timeline-DreEwsRY.js +616 -0
  128. package/dist/web-video-engine/frame-timeline.d.ts +80 -0
  129. package/dist/web-video-engine/frame-walker.d.ts +73 -0
  130. package/dist/web-video-engine/index.d.ts +13 -0
  131. package/dist/web-video-engine/index.d.ts.map +1 -0
  132. package/dist/web-video-engine/index.js +3 -0
  133. package/dist/web-video-engine/index.js.map +1 -0
  134. package/dist/web-video-engine/key-packet.d.ts +64 -0
  135. package/dist/web-video-engine/keyframe-index.d.ts +90 -0
  136. package/dist/web-video-engine/mirror-store.d.ts +55 -0
  137. package/dist/web-video-engine/renderer.d.ts +93 -0
  138. package/dist/web-video-engine/rotation.d.ts +34 -0
  139. package/dist/web-video-engine/scrub-controller.d.ts +375 -0
  140. package/dist/web-video-engine/scrub-cursor.d.ts +347 -0
  141. package/dist/web-video-engine/scrub-trajectory.d.ts +29 -0
  142. package/dist/web-video-engine/source-residency.d.ts +91 -0
  143. package/dist/web-video-engine/trace-recorder.d.ts +163 -0
  144. package/dist/web-video-engine/types.d.ts +200 -0
  145. package/dist/web-video-engine/video-engine.d.ts +437 -0
  146. package/dist/web-video-engine/webgpu-renderer.d.ts +61 -0
  147. package/dist/web-video-engine/worker-bridge.d.ts +11 -0
  148. package/dist/web-video-engine/worker-dispatch.d.ts +16 -0
  149. package/dist/web-video-engine/worker-protocol.d.ts +282 -0
  150. package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts.map +1 -1
  151. package/node_modules/supervision-js-core/dist/detections/composite-detection-frame-source.d.ts.map +1 -1
  152. package/node_modules/supervision-js-core/dist/index.d.ts +5 -4
  153. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  154. package/node_modules/supervision-js-core/dist/index.js +1167 -280
  155. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  156. package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts.map +1 -1
  157. package/node_modules/supervision-js-core/dist/styles/interaction-style.d.ts +4 -36
  158. package/node_modules/supervision-js-core/dist/styles/interaction-style.d.ts.map +1 -1
  159. package/node_modules/supervision-js-core/dist/styles/polyline-style.d.ts +5 -0
  160. package/node_modules/supervision-js-core/dist/styles/polyline-style.d.ts.map +1 -1
  161. package/node_modules/supervision-js-core/dist/types/annotation-renderer.d.ts +92 -7
  162. package/node_modules/supervision-js-core/dist/types/annotation-renderer.d.ts.map +1 -1
  163. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +191 -17
  164. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
  165. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +83 -5
  166. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  167. package/node_modules/supervision-js-core/dist/types/polyline-style.d.ts +2 -0
  168. package/node_modules/supervision-js-core/dist/types/polyline-style.d.ts.map +1 -1
  169. package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts +32 -0
  170. package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts.map +1 -1
  171. package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts.map +1 -1
  172. package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts +5 -0
  173. package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -1
  174. package/node_modules/supervision-js-core/dist/utils/detection-masks.d.ts +9 -0
  175. package/node_modules/supervision-js-core/dist/utils/detection-masks.d.ts.map +1 -1
  176. package/node_modules/supervision-js-core/dist/utils/detection-ranges.d.ts +18 -0
  177. package/node_modules/supervision-js-core/dist/utils/detection-ranges.d.ts.map +1 -0
  178. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +34 -2
  179. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -1
  180. package/node_modules/supervision-js-core/dist/utils/wait-bound.d.ts +7 -0
  181. package/node_modules/supervision-js-core/dist/utils/wait-bound.d.ts.map +1 -0
  182. package/package.json +24 -3
@@ -0,0 +1,371 @@
1
+ import { type AnySourceHandle } from "./decode-source";
2
+ import { FrameCache } from "./frame-cache";
3
+ import type { FrameId } from "./frame-timeline";
4
+ import { type ScrubCursor, ScrubCursorState, type ScrubFrame, type ScrubFrameListener, type ScrubTrackInfo, type SchedulerStats, type SeekIntent } from "./scrub-cursor";
5
+ import { type Sec, WebVideoEngineError } from "./types";
6
+ export interface DecodeSchedulerOptions {
7
+ readonly source: AnySourceHandle;
8
+ readonly cache: FrameCache;
9
+ /** A cache hit within this of the target serves a crisp frame; default 50. */
10
+ readonly exactToleranceMs?: number;
11
+ /** A coarse hit within this of the target serves a preview; default 250. */
12
+ readonly previewToleranceMs?: number;
13
+ /** Millisecond time source for latency stats; defaults to performance.now. */
14
+ readonly now?: () => number;
15
+ /** Delay before scrub-neighbor prefetch; overridable for deterministic tests. */
16
+ readonly scrubPrefetchQuietMs?: number;
17
+ /**
18
+ * Opens a fresh source handle to replace a wedged decoder, or null when the
19
+ * source cannot be re-opened (a one-shot stream). mediabunny decodes take no
20
+ * AbortSignal, so a hung decode is uncancellable; the watchdog disposes the
21
+ * dead provider and rebuilds from this seam so later decodes work. Omitted in
22
+ * tests that do not exercise recovery, where the scheduler degrades instead.
23
+ */
24
+ readonly reopen?: (() => Promise<AnySourceHandle>) | null;
25
+ /**
26
+ * Hang ceiling for one random-access decode; defaults to the runtime
27
+ * constant. A decode that outruns it is treated as failed and triggers
28
+ * provider recovery. Overridable so a test can drive it with fake timers at a
29
+ * small value.
30
+ */
31
+ readonly decodeHangTimeoutMs?: number;
32
+ /**
33
+ * Hang ceiling for the first-frame seed; defaults to the runtime constant,
34
+ * which is tighter than the one above. Overridable so a test can drive it
35
+ * with fake timers at a small value.
36
+ */
37
+ readonly seedHangTimeoutMs?: number;
38
+ /**
39
+ * Called once, with the failure, when the decoder is judged unable to decode
40
+ * this source at all. Nothing else tells a consumer: a stalled decoder still
41
+ * accepts every command and answers none of them, so without this the
42
+ * transport reads as playing over a canvas that will never change again.
43
+ */
44
+ readonly onDecodeFailure?: (error: WebVideoEngineError) => void;
45
+ }
46
+ /** Bounds the clone payload while preserving the first and last discovered
47
+ * anchors and sampling the interior evenly. GOP statistics still use the full
48
+ * index; only the diagnostic lane is thinned. */
49
+ export declare function boundedKeyframeTimestamps(keyframesS: readonly number[]): number[];
50
+ /**
51
+ * Caching scrub cursor. Wraps the same decode primitives as CanvasSinkScrubCursor
52
+ * but routes every random-access seek through a two-tier FrameCache and prefetches
53
+ * a decode window around the playhead so the next move is already in hand.
54
+ *
55
+ * A scrub first reads the cache. An exact hit needs no decode. A preview hit
56
+ * paints a coarse frame at once, then a crisp decode follows. A miss decodes
57
+ * straight away. Once the foreground frame settles, a background sweep fills the
58
+ * cache around the playhead, shaped by how the consumer is moving: neighbor
59
+ * frames while scrubbing or stepping, a coarse keyframe pass while idle.
60
+ *
61
+ * A new gesture signals the sweep to bail and proceeds without waiting for it, so
62
+ * speculative work never sits on the critical path; mediabunny builds a decoder
63
+ * per iterator, so a sweep still unwinding alongside a foreground decode costs
64
+ * memory, not correctness. Every decoded frame is copied into the cache, never
65
+ * held as a raw VideoFrame, so the decoder is never pinned.
66
+ */
67
+ export declare class DecodeScheduler implements ScrubCursor {
68
+ private provider;
69
+ private readonly cache;
70
+ private keyframeIndex;
71
+ private readonly trackInfo;
72
+ private readonly exactTolMs;
73
+ private readonly previewTolMs;
74
+ private readonly now;
75
+ private readonly scrubPrefetchQuietMs;
76
+ /** Re-opens the source after a hung decode, or null when it cannot be re-opened. */
77
+ private readonly reopen;
78
+ private readonly decodeHangTimeoutMs;
79
+ private readonly seedHangTimeoutMs;
80
+ /** Single-flight latch: a watchdog recovery already running. A second hang
81
+ * during the rebuild awaits the same recovery rather than racing a second
82
+ * dispose/rebuild of the provider. */
83
+ private recovering;
84
+ /** True once a hang hit a non-re-openable source: the provider is dead and
85
+ * could not be rebuilt, so decodes no-op rather than hang. Surfaced via stats
86
+ * so a consumer can show a degraded state instead of a silent freeze. */
87
+ private decoderDead;
88
+ /** Consecutive failed rebuilds. One is a transient read; a run of them is a
89
+ * source that cannot be re-opened, which is what decoderDead describes. */
90
+ private failedRebuilds;
91
+ /** True once the decoder is judged unable to decode this source at all. See
92
+ * SchedulerStats.decoderStalled; latched, and it stops the rebuild loop. */
93
+ private decoderStalled;
94
+ /** The failure markStalled latched, so a caller still awaiting a decode is
95
+ * handed the same one the consumer was told about rather than a parallel
96
+ * account of it. */
97
+ private stallFailure;
98
+ /** Decode requests the live provider has left unanswered in a row; see
99
+ * MAX_UNANSWERED_DECODES. */
100
+ private unansweredDecodes;
101
+ private readonly onDecodeFailure;
102
+ private iterator;
103
+ private nextInFlight;
104
+ private nextPending;
105
+ private currentState;
106
+ private readonly listeners;
107
+ private idleResolvers;
108
+ private settleResolvers;
109
+ private pendingSeekTargetS;
110
+ private pendingSeekKeyOnly;
111
+ private seekDraining;
112
+ private closed;
113
+ private lastEmittedFrame;
114
+ private mode;
115
+ /** Recent scrub motion. Only its heading is read, which is what decides the
116
+ * side of the playhead a scrub window spends most of its budget on. */
117
+ private readonly trajectory;
118
+ /** Outstanding background prefetch chain, or null once it unwinds. Awaited
119
+ * only by close(), so teardown cannot outrun a sweep still holding a decoder. */
120
+ private prefetchTask;
121
+ /** True only while a sweep is decoding, not while scrub prefetch is parked. */
122
+ private prefetchDecoding;
123
+ /** Bumped when a running prefetch is cancelled; that prefetch bails when it
124
+ * sees the token move, which is how cancellation propagates. */
125
+ private prefetchGen;
126
+ /** Whether a sweep is still depending on the live token. Cleared both by the
127
+ * cancellation that supersedes the sweep and by the sweep finishing. */
128
+ private prefetchArmed;
129
+ /** Woken when the token moves, so a sweep parked on a decode unwinds at the
130
+ * gesture rather than at the end of the decode it is parked on. */
131
+ private abandonResolvers;
132
+ private scrubSamples;
133
+ private scrubSumMs;
134
+ private scrubMaxMs;
135
+ private scrubLastMs;
136
+ /** Ring of recent scrub-decode latencies for p50/p95. Overwrite-oldest so the
137
+ * percentile tracks the recent region, not an all-time tail. One push/seek. */
138
+ private readonly scrubLatencyRing;
139
+ private scrubLatencyCount;
140
+ private scrubLatencyHead;
141
+ private targetVsLandedLastMs;
142
+ private timeToCrispLastMs;
143
+ private foregroundDecodes;
144
+ private prefetchExactFrames;
145
+ private prefetchPreviewFrames;
146
+ private keyframeAnchoredEmits;
147
+ private exactSeeks;
148
+ private keySeeks;
149
+ private seekCoalesceOverwrites;
150
+ constructor(options: DecodeSchedulerOptions);
151
+ get state(): ScrubCursorState;
152
+ get track(): ScrubTrackInfo;
153
+ get playReadAhead(): number;
154
+ get isIdle(): boolean;
155
+ open(): Promise<void>;
156
+ /**
157
+ * The first frame, on the seed ceiling and its own attempt budget.
158
+ *
159
+ * A decoder that accepts this decode and never answers it is the one failure
160
+ * the rest of the machinery cannot see: nothing has been asked of it twice
161
+ * yet, so no request budget can run out, and open() is what the load path
162
+ * awaits, so the wait ends only when the caller above gives up on the whole
163
+ * worker. Bounding it here turns that into a decoder failure named at the
164
+ * point it happened.
165
+ *
166
+ * Attempts are spent against rebuilt providers wherever the source can be
167
+ * re-opened, so exhausting them means the source was re-opened that many
168
+ * times and the first frame never arrived.
169
+ */
170
+ private seedFirstFrame;
171
+ /**
172
+ * Synchronous best-effort lookup the render loop calls on scrub to paint a
173
+ * frame this same tick. Pure: it reads the cache and returns a paintable
174
+ * frame without touching decode or cursor state.
175
+ */
176
+ peekCached(timeMs: number): ScrubFrame | null;
177
+ /**
178
+ * Resolves once foreground work has settled and any background prefetch
179
+ * kicked by that settle has drained. The plain idle() contract excludes
180
+ * prefetch so commits resolve promptly; this awaits full quiescence.
181
+ */
182
+ whenSettled(): Promise<void>;
183
+ /**
184
+ * Walks the whole track's keyframes into the index, metadata only, so the
185
+ * diagnostics keyframe lane fills out across the entire file. Decoder-less
186
+ * (it rides the EncodedPacketSink probe, never the frame sink), so it does
187
+ * not contend with the foreground decode. Idempotent and best-effort: a
188
+ * failure just leaves the lane as sparse as the lazy probes made it.
189
+ */
190
+ ensureKeyframeIndex(): Promise<void>;
191
+ getStats(): SchedulerStats;
192
+ /** Percentile over the bounded recent-latency ring. Copies the live samples
193
+ * into a small scratch array and sorts; bounded by SCRUB_LATENCY_RING, and
194
+ * this runs only at snapshot time, never per seek. */
195
+ private scrubPercentile;
196
+ /** The targets the next prefetch sweep would decode for the current mode
197
+ * and position; a pure recompute from existing state. The Idle plan is
198
+ * built from the keyframes discovered so far, so it grows as the index
199
+ * learns, exactly like the sweep it predicts. */
200
+ private prefetchPlanMs;
201
+ seekTo(timestamp: Sec, intent?: SeekIntent): void;
202
+ seekToKey(timestamp: Sec): void;
203
+ /** Floors a seek target to the track's first timestamp. A seek below the
204
+ * origin (an offset/trimmed clip) has no sample to land on, so it would
205
+ * emit nothing; clamping lands it on the first frame instead. */
206
+ private clampToOrigin;
207
+ /** Motion sampled across a pause, a step, or a stretch of playback describes
208
+ * no hand still on the timeline, so leaving a scrub drops the gesture. */
209
+ private enterMode;
210
+ attachPlay(startS: number): void;
211
+ detachPlay(): void;
212
+ next(): void;
213
+ /**
214
+ * Decodes one named frame of the source and emits it.
215
+ *
216
+ * The retrieval is at-or-before a time the frame table produced, and the
217
+ * demuxer normalises that back to the same integer tick it came from, so it
218
+ * lands on that frame and no other. Nothing here walks, compares or skips,
219
+ * so a step across a frame boundary needs no epsilon, and a burst of steps
220
+ * reuses whatever decode position the session already holds, with no
221
+ * iterator opened per press.
222
+ */
223
+ seekToFrame(frame: FrameId): Promise<ScrubFrame | null>;
224
+ idle(): Promise<void>;
225
+ seekSettled(): Promise<void>;
226
+ subscribe(listener: ScrubFrameListener): () => void;
227
+ close(): Promise<void>;
228
+ private drainSeek;
229
+ private runSeek;
230
+ private runExactSeek;
231
+ /**
232
+ * Whether a decode that outlived its target still improves the picture,
233
+ * judged from where the gesture has since reached. A drag that outruns the
234
+ * decoder supersedes every decode it starts, and the screen holds something
235
+ * older than all of them, so refusing the lot pins the picture at whatever
236
+ * was up when the drag began.
237
+ *
238
+ * Only a gesture reads this way. A jump has one destination and no
239
+ * intermediate positions worth showing, so a superseded decode there is
240
+ * work for a place nobody is looking.
241
+ */
242
+ private gainsOnTarget;
243
+ /**
244
+ * Whether a cached frame earns the screen over what is already on it. A
245
+ * crisp frame always does. A coarse stand-in exists to bridge the wait for a
246
+ * decode, so it earns the screen only by landing closer to the target than
247
+ * what is showing: replacing a crisp frame with a blurry one at the same
248
+ * position is a visible drop in quality for nothing, and on a slow drag,
249
+ * where each step lands within a frame of the last, it repeats several times
250
+ * a second and reads as the picture flickering between sharp and soft.
251
+ */
252
+ private worthPainting;
253
+ private runKeySeek;
254
+ private pullForwardOne;
255
+ /**
256
+ * Bounds a decode so a hung mediabunny decode (which takes no AbortSignal and
257
+ * cannot be cancelled) never wedges the caller. On timeout it returns null and
258
+ * kicks provider recovery; the orphaned decode promise is abandoned, since
259
+ * there is no handle to cancel it. The healthy path resolves before the timer
260
+ * and behaves exactly as a bare await: the loser timer is cleared, so it adds
261
+ * no work to a decode that lands in time.
262
+ *
263
+ * A rejected decode takes the same route as a hung one. Every caller here is
264
+ * mid-gesture with no way to answer a decode failure other than to try again
265
+ * on a working decoder, and a throw would escape the fire-and-forget seek
266
+ * drain and the play pull chain as an unhandled rejection. Taking that route
267
+ * is also why an unanswered decode has to be counted here: the failure is
268
+ * absorbed at this one point, so this is the only place that can tell a
269
+ * decoder having a bad moment from one that will never answer again.
270
+ */
271
+ private withDecodeWatchdog;
272
+ /**
273
+ * The same watchdog over a container metadata read. Deliberately unbooked:
274
+ * the keyframe probe never touches the decoder, so its answer is no evidence
275
+ * that decoding works, and clearing the stall budget on one would keep a
276
+ * background sweep forgiving a decoder that answers nothing.
277
+ */
278
+ private withProbeWatchdog;
279
+ private watchdogged;
280
+ private raceWatchdog;
281
+ /**
282
+ * Books one decode the live provider failed to answer, and decides whether
283
+ * to rebuild or to give up. A decoder that refuses outright says so in the
284
+ * rejection and there is nothing to rebuild toward; otherwise the runtime
285
+ * rebuilds until the request budget runs out, which is what stops a source
286
+ * that re-opens cleanly and then decodes nothing from being rebuilt forever
287
+ * while every surface reads as healthy.
288
+ */
289
+ private noteUnanswered;
290
+ /** Latches the terminal decoder failure and hands it out exactly once. */
291
+ private markStalled;
292
+ /**
293
+ * Rebuilds the decoder after a hung decode. The old provider is wedged on an
294
+ * uncancellable decode, so it is disposed and a fresh source opened in its
295
+ * place; the keyframe index is repointed at the new probe since disposing the
296
+ * old input invalidated the old one. Single-flight: a second hang during the
297
+ * rebuild awaits the same recovery. When the source cannot be re-opened (a
298
+ * one-shot stream), the decoder is marked dead so later decodes no-op rather
299
+ * than hang, leaving the engine degraded but alive instead of frozen.
300
+ */
301
+ private recoverDecoder;
302
+ /**
303
+ * The only writer of the generation token, so no bump can move it without
304
+ * also waking the sweep it cancels. A running sweep bails at its next
305
+ * checkpoint and is never joined: joining it puts an unbounded background
306
+ * walk on the critical path of every seek and play pull. The task handle
307
+ * stays so close() can await teardown.
308
+ */
309
+ private abandonPrefetch;
310
+ /** Resolves the moment `gen` stops being the live prefetch generation. */
311
+ private whenAbandoned;
312
+ /**
313
+ * Kicks a background sweep around `aroundS` for the current access mode.
314
+ * Fire-and-forget: it waits for any prior sweep to tear down so the decoder
315
+ * is never used twice at once, then runs under a captured generation token.
316
+ */
317
+ private schedulePrefetch;
318
+ private runPrefetch;
319
+ private runExactWindow;
320
+ private runPreviewSweep;
321
+ private decodeInto;
322
+ /**
323
+ * One sweep pull, ended by abandonment rather than by the decode it is
324
+ * waiting on. A native async generator serializes its request queue, so the
325
+ * return() that cancellation would issue is queued behind the in-flight
326
+ * next() and lands only once that walk finishes; the pull itself cannot be
327
+ * called back. The sweep therefore stops awaiting it and leaves it orphaned,
328
+ * closing the sample it may still deliver, which the cache will never own.
329
+ */
330
+ private pullSwept;
331
+ private windowTimestamps;
332
+ /**
333
+ * How a window's slots divide between the way the gesture is heading and the
334
+ * way it came from. An unmoving gesture has no direction to favour, so it
335
+ * splits evenly, which is also what a step or an idle settle wants.
336
+ */
337
+ private splitByHeading;
338
+ /**
339
+ * Whether reaching `t` backwards is worth it, measured in frames the walk
340
+ * from its keyframe would decode. The index is what the runtime knows so far,
341
+ * so an empty one is no evidence and does not veto: guessing a source has no
342
+ * keyframes would throw away every backward slot on a source that has plenty.
343
+ */
344
+ private walkToTargetAffordable;
345
+ /**
346
+ * Per-side neighbor count for the current mode, clamped so the whole window
347
+ * (both sides plus the center already in hand) fits the exact tier. A window
348
+ * wider than the tier would evict its own freshly decoded frames, so a
349
+ * re-scrub into the swept region would still miss.
350
+ */
351
+ private perSideWindowFrames;
352
+ private frameIntervalS;
353
+ private currentPositionS;
354
+ /** The timeline's name for a decoded frame, which is what the exact cache
355
+ * tier stores it under. Two decodes of one frame land on one identity
356
+ * however their microsecond timestamps differ, and two frames never share
357
+ * one however close together the source puts them. */
358
+ private frameIdOf;
359
+ /** The timeline identity of the frame covering a presentation time. */
360
+ private frameAtTime;
361
+ private emitDecoded;
362
+ /** Puts a decoded frame in both tiers without painting it, and releases it.
363
+ * For a frame worth keeping whose moment to be seen has passed. */
364
+ private cacheDecoded;
365
+ private emitCached;
366
+ private emit;
367
+ private frameFromCache;
368
+ private recordScrubLatency;
369
+ private flushIdleResolvers;
370
+ private flushSettleResolvers;
371
+ }
@@ -0,0 +1,283 @@
1
+ import { type Rotation } from "./rotation";
2
+ import type { VideoSampleLike } from "./scrub-cursor";
3
+ /**
4
+ * One packet as the session submits it. Mirrors the fields of mediabunny's
5
+ * EncodedPacket the decoder needs, so the session never imports mediabunny.
6
+ */
7
+ export interface EncodedPacketLike {
8
+ readonly data: Uint8Array;
9
+ readonly type: "key" | "delta";
10
+ readonly timestamp: number;
11
+ readonly duration: number;
12
+ }
13
+ export interface PacketRetrievalOptionsLike {
14
+ readonly metadataOnly?: boolean;
15
+ readonly verifyKeyPackets?: boolean;
16
+ }
17
+ /** The slice of mediabunny's EncodedPacketSink the session decodes through. */
18
+ export interface PacketSource {
19
+ getKeyPacket(timestamp: number, options?: PacketRetrievalOptionsLike): Promise<EncodedPacketLike | null>;
20
+ getNextKeyPacket(packet: EncodedPacketLike, options?: PacketRetrievalOptionsLike): Promise<EncodedPacketLike | null>;
21
+ packets(startPacket?: EncodedPacketLike, endPacket?: EncodedPacketLike, options?: PacketRetrievalOptionsLike): AsyncGenerator<EncodedPacketLike, void, unknown>;
22
+ }
23
+ /** The slice of WebCodecs' VideoDecoder the session drives. Chunks cross as
24
+ * inits so the session never reaches for a platform constructor. */
25
+ export interface VideoDecoderLike {
26
+ configure(config: VideoDecoderConfig): void;
27
+ decode(chunk: EncodedVideoChunkInit): void;
28
+ flush(): Promise<void>;
29
+ reset(): void;
30
+ close(): void;
31
+ }
32
+ export interface DecodeSessionOptions {
33
+ readonly packets: PacketSource;
34
+ readonly config: VideoDecoderConfig;
35
+ readonly createDecoder: (init: VideoDecoderInit) => VideoDecoderLike;
36
+ /** Dimensions used to take ownership of decoder output pixels when set. */
37
+ readonly outputWidth?: number;
38
+ readonly outputHeight?: number;
39
+ /** Test seam for taking ownership of decoder output pixels. */
40
+ readonly snapshotFrame?: (frame: VideoFrame, width: number, height: number) => VideoFrame;
41
+ /**
42
+ * The track's quarter turn, which the session's frames carry and its draw
43
+ * applies. A raw decoded picture holds the stored pixels and nothing about
44
+ * the display matrix, so this is the only route the turn has here, and a
45
+ * caller that forgets it paints every portrait recording sideways.
46
+ */
47
+ readonly rotation: Rotation;
48
+ /**
49
+ * Ceiling on waiting for one decoder output while chunks are in flight;
50
+ * defaults to OUTPUT_TIMEOUT_MS. Overridable so a test can drive the
51
+ * silent-decoder path without waiting out the production ceiling.
52
+ */
53
+ readonly outputTimeoutMs?: number;
54
+ }
55
+ /**
56
+ * Whether the session can drive a track with this decoder config. The gate that
57
+ * routes a source here and the constructor that refuses everything else read the
58
+ * same predicate.
59
+ */
60
+ export declare function sessionDrivable(config: VideoDecoderConfig): boolean;
61
+ /**
62
+ * The three retrievals the session answers, mirror for mirror with the sink
63
+ * slices the other decode paths ride. Held as an interface so the provider that
64
+ * wraps it stays fake-injectable under test.
65
+ */
66
+ export interface SessionFrameSource {
67
+ frameAt(targetS: number): Promise<VideoSampleLike | null>;
68
+ framesFrom(startS: number): AsyncGenerator<VideoSampleLike, void, unknown>;
69
+ framesCovering(startS: number, endS: number): AsyncGenerator<VideoSampleLike, void, unknown>;
70
+ /** Earliest position still reachable without re-anchoring, or -Infinity when
71
+ * the session has not been positioned yet. A decoder only moves forward, so
72
+ * asking for anything below this costs a walk from the enclosing keyframe. */
73
+ readonly reachableFromS: number;
74
+ /** Every frame the decoder has output since open, discarded pre-roll
75
+ * included; monotonic. */
76
+ readonly framesDecoded: number;
77
+ }
78
+ /**
79
+ * One long-lived VideoDecoder, held across seeks.
80
+ *
81
+ * A decoder can only ever move forward, and every flush re-arms its demand for a
82
+ * key frame, so a runtime that flushes per retrieval pays a walk back to the
83
+ * nearest true IDR on every seek. This session flushes only at end of stream:
84
+ * a seek at-or-ahead of the read head, inside the current anchor span, decodes
85
+ * only the packets between the two, and a backward jump or a jump past the span
86
+ * re-anchors. Positioning is the only thing that costs a walk.
87
+ *
88
+ * One decoder serving several readers is the whole point, and it is also the
89
+ * hazard: a scrub's frameAt and playback's framesFrom drive the same decoder,
90
+ * the same packet iterator, and the same queue of decoded frames. So every
91
+ * retrieval takes the decoder exclusively for the length of one frame, and a
92
+ * reader displaced by someone else's re-anchor is told, rather than left to read
93
+ * the emptied queue as end of stream and stop for good.
94
+ *
95
+ * Frames are handed out as VideoSampleLike and their close is the caller's, the
96
+ * same obligation the zero-copy sample path carries.
97
+ */
98
+ export declare class DecodeSession implements SessionFrameSource {
99
+ private readonly options;
100
+ private readonly keyPacket;
101
+ private readonly prefixWidth;
102
+ private decoder;
103
+ private iterator;
104
+ private peeked;
105
+ /** Timestamp of the key packet after the anchor: the end of the span a
106
+ * forward seek can reach without re-anchoring. */
107
+ private spanEndS;
108
+ private exhausted;
109
+ /** Timings the decoder still owes pictures for, in presentation order. */
110
+ private readonly pending;
111
+ private readonly decoded;
112
+ private wake;
113
+ /**
114
+ * Latched once the decoder is judged unable to decode this source at all.
115
+ * Every later retrieval refuses with it instead of re-anchoring onto the
116
+ * same failure, which is what turns a silent forever-retry into one honest
117
+ * error the caller can show.
118
+ */
119
+ private stalledError;
120
+ /**
121
+ * The current entry point's failure, held only until the walk that is owed a
122
+ * picture decides what it means. It condemns the anchor, never the session:
123
+ * a source whose sync table names one bad entry point still decodes from
124
+ * every other one, and latching the first failure is what turned a seek into
125
+ * a poisoned GOP into a player that never painted again.
126
+ */
127
+ private anchorError;
128
+ /** The entry currently feeding the decoder, or null before the first one. */
129
+ private entry;
130
+ /** Entry points a decoder error has ruled out, by whole-microsecond
131
+ * timestamp. Only ever grows: a bitstream does not change. */
132
+ private readonly rejectedAnchors;
133
+ /** Where the live retrieval wants to be, so a re-anchor after a failed entry
134
+ * aims at the position the caller asked for rather than the anchor's. */
135
+ private entryTargetS;
136
+ /** The last picture handed out since the caller last chose a position, so a
137
+ * re-anchor mid-walk resumes rather than replays. */
138
+ private servedS;
139
+ private closed;
140
+ private anchors;
141
+ private framesDecodedCount;
142
+ /** Where the decoder sits, kept in step with what it hands out. Read
143
+ * synchronously by callers deciding what to ask for, so it cannot await. */
144
+ private reachable;
145
+ /** Serializes retrievals; see the class note on shared-decoder exclusivity. */
146
+ private tail;
147
+ /** Bumped by every re-anchor, so a reader can tell whether the decoder is
148
+ * still where it left it or has been moved under it by another reader. */
149
+ private epoch;
150
+ private readonly outputTimeoutMs;
151
+ private readonly rotation;
152
+ private readonly outputWidth;
153
+ private readonly outputHeight;
154
+ private readonly snapshotFrame;
155
+ constructor(options: DecodeSessionOptions);
156
+ /** Times the session has configured the decoder onto an anchor. */
157
+ get anchorCount(): number;
158
+ /** Every frame the decoder has ever output, including walk pre-roll that is
159
+ * discarded before the target: the honest denominator for what a paint
160
+ * actually cost. Monotonic for the session's lifetime. */
161
+ get framesDecoded(): number;
162
+ /** See SessionFrameSource. The oldest frame decoded but not yet handed out
163
+ * when there is one, since those are still servable, else the last position
164
+ * handed out. */
165
+ get reachableFromS(): number;
166
+ /** The frame at or before `targetS`, or null when nothing precedes it. */
167
+ frameAt(targetS: number): Promise<VideoSampleLike | null>;
168
+ /**
169
+ * Frames from `startS` onward, beginning at the frame at or before it. The
170
+ * walk from the anchor up to `startS` is decoded but not yielded, since the
171
+ * caller asked to start there.
172
+ *
173
+ * Playback rides this for the length of a session, so it outlives any number
174
+ * of scrubs. When one of those moved the decoder, the walk resumes from the
175
+ * last frame it handed out rather than ending.
176
+ */
177
+ framesFrom(startS: number): AsyncGenerator<VideoSampleLike, void, unknown>;
178
+ /**
179
+ * Every frame decoded while covering `[startS, endS]`, including the walk
180
+ * from the anchor. Those prefix frames cost the same decode either way, so
181
+ * yielding them hands the caller a span of frames for the price of the one
182
+ * it asked for.
183
+ *
184
+ * This serves speculative sweeps, so a re-anchor by anyone else ends it:
185
+ * chasing the span back would spend a foreground decode's worth of decoder
186
+ * time on frames nobody is waiting for.
187
+ */
188
+ framesCovering(startS: number, endS: number): AsyncGenerator<VideoSampleLike, void, unknown>;
189
+ /**
190
+ * Runs `op` with the decoder to itself. Retrievals interleave at frame
191
+ * granularity, which bounds how long a foreground seek waits behind a
192
+ * background sweep at one frame, while keeping any one walk's view of the
193
+ * packet iterator, the in-flight count, and the decoded queue consistent.
194
+ */
195
+ private exclusive;
196
+ /**
197
+ * The first frame after `afterS` once another reader has moved the decoder.
198
+ * Re-positions and discards the frames already handed out, so a walk picks
199
+ * up exactly where it left off however far away the decoder was taken.
200
+ */
201
+ private resumeAfter;
202
+ close(): void;
203
+ /** Decodes through `targetS` and returns the last frame at or before it,
204
+ * closing the frames walked past. */
205
+ private land;
206
+ private positionFor;
207
+ /** The earliest position still reachable without re-anchoring: the oldest
208
+ * frame decoded but not yet handed out, or the next packet in line when
209
+ * there is none. Decode runs ahead of the read, so the packet alone would
210
+ * put the head past frames the session can still serve. */
211
+ private readHeadS;
212
+ private anchorAt;
213
+ /**
214
+ * The packet to open a decode of `targetS` from: the container's own sync
215
+ * sample, unless a decoder has already proved that one is not a legal entry
216
+ * point, in which case the last verified IDR at or before the target.
217
+ *
218
+ * A sync sample that is itself an IDR is already the furthest-back entry
219
+ * worth reaching for, so a failure there is a failure of the source.
220
+ */
221
+ private resolveEntry;
222
+ /**
223
+ * Where the span a forward seek can reach without re-anchoring ends: the
224
+ * first sync sample past the target that is still worth anchoring at.
225
+ *
226
+ * Skipping the rejected ones is what keeps the pre-roll paid for once. Ending
227
+ * the span at an entry point already known not to decode would send the very
228
+ * next seek into that GOP back to the same rejected anchor and back through
229
+ * the same walk to recover from it.
230
+ */
231
+ private spanEndAfter;
232
+ /** Drops the work in flight and the frames it produced, in one turn, so no
233
+ * output from the old anchor can land against the new one. */
234
+ private quiesce;
235
+ private configureDecoder;
236
+ /** One frame at or before `boundS`, or null once the bound is passed. A frame
237
+ * decoded past the bound stays queued for the next read rather than being
238
+ * handed out or thrown away. */
239
+ private pull;
240
+ /**
241
+ * Re-opens the decode from the last entry point ahead of the failed one,
242
+ * which for an open GOP means the previous IDR plus a walk to the target.
243
+ *
244
+ * The failed anchor is struck off for the life of the session, so the cost is
245
+ * paid once per bad entry point rather than once per seek into it. When there
246
+ * is nothing further back to enter from, the failure is the source's and it
247
+ * latches here.
248
+ */
249
+ private enterFurtherBack;
250
+ /**
251
+ * Keeps the decoder's pipeline fed, regardless of which frame is being read.
252
+ * A decoder emits nothing until it holds enough pictures, so submitting only
253
+ * as far as the requested frame leaves the session waiting on output it is
254
+ * refusing to make possible. Feeding stops once the frames already decoded
255
+ * pile up, which is what bounds the memory the session holds.
256
+ */
257
+ private fill;
258
+ private peek;
259
+ private drain;
260
+ private awaitOutput;
261
+ /**
262
+ * Files a submitted chunk's timing in presentation order, which is the order
263
+ * pictures come back in and is not the order chunks go in on a B-frame
264
+ * source.
265
+ */
266
+ private awaitTiming;
267
+ /**
268
+ * A decoder that echoes the timestamp it was handed names its own pending
269
+ * entry, which survives an output being dropped. One that counts from an
270
+ * origin of its own names nothing, and position is all that is left. Taking
271
+ * position alone would let a single dropped output shift every picture after
272
+ * it onto the wrong detections, permanently and without a symptom.
273
+ */
274
+ private claimTiming;
275
+ private receive;
276
+ private fail;
277
+ /** Latches the terminal failure and returns it. First writer wins, so the
278
+ * cause a caller is handed is the one that started the failure rather than
279
+ * whichever consequence surfaced last. */
280
+ private stall;
281
+ private latch;
282
+ private discardDecoded;
283
+ }