@blockcast/mmt-render 0.1.0 → 0.3.0-main.1cd3cdea89da

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 (35) hide show
  1. package/README.md +8 -19
  2. package/dist/.build-stamp +0 -0
  3. package/dist/audio/audio-render-pipeline.js +217 -248
  4. package/dist/audio/audio-render-pipeline.js.map +2 -2
  5. package/dist/audio/audio-ring-buffer.js +93 -127
  6. package/dist/audio/audio-ring-buffer.js.map +1 -1
  7. package/dist/audio/index.js +313 -361
  8. package/dist/audio/index.js.map +3 -3
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/pacing/frame-pacer.d.ts +22 -1
  12. package/dist/pacing/frame-pacer.d.ts.map +1 -1
  13. package/dist/pacing/frame-pacer.js +43 -39
  14. package/dist/pacing/frame-pacer.js.map +2 -2
  15. package/dist/pacing/index.d.ts +0 -2
  16. package/dist/pacing/index.d.ts.map +1 -1
  17. package/dist/pacing/index.js +43 -95
  18. package/dist/pacing/index.js.map +3 -3
  19. package/dist/video/index.js +129 -139
  20. package/dist/video/index.js.map +2 -2
  21. package/dist/video/keyframe-gate.js +5 -17
  22. package/dist/video/keyframe-gate.js.map +1 -1
  23. package/dist/video/video-render-pipeline.js +129 -139
  24. package/dist/video/video-render-pipeline.js.map +2 -2
  25. package/dist/worklet/render-worklet.js +171 -219
  26. package/dist/worklet/render-worklet.js.map +2 -2
  27. package/package.json +4 -8
  28. package/src/index.ts +1 -1
  29. package/src/pacing/frame-pacer.ts +28 -1
  30. package/src/pacing/index.ts +0 -2
  31. package/dist/pacing/interleave-timing.d.ts +0 -64
  32. package/dist/pacing/interleave-timing.d.ts.map +0 -1
  33. package/dist/pacing/interleave-timing.js +0 -58
  34. package/dist/pacing/interleave-timing.js.map +0 -7
  35. package/src/pacing/interleave-timing.ts +0 -95
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/video/keyframe-gate.ts", "../../src/video/video-render-pipeline.ts", "../../src/pacing/frame-pacer.ts"],
4
- "sourcesContent": ["/**\n * KeyframeGate \u2014 drops delta frames until a keyframe arrives.\n *\n * Required when subscribing mid-stream: the relay may deliver P/B frames\n * from the middle of a GOP before the first IDR. Feeding those into\n * VideoDecoder before an init yields \"key frame required\" errors.\n *\n * Call reset() to re-arm the gate after a decoder error (requestKeyframe).\n */\n\nexport class KeyframeGate {\n #open = false\n\n get isOpen(): boolean {\n return this.#open\n }\n\n /**\n * Decide whether to pass a frame through.\n * Opens permanently once the first keyframe is seen.\n */\n allow(keyframe: boolean): boolean {\n if (this.#open) return true\n if (keyframe) {\n this.#open = true\n return true\n }\n return false\n }\n\n /** Re-close the gate. Next keyframe reopens it. */\n reset(): void {\n this.#open = false\n }\n}\n", "/**\n * VideoRenderPipeline \u2014 end-to-end video render for MoQ/MMT players.\n *\n * Pipeline: MoQ object \u2192 MmtpDecodePipeline \u2192 keyframe gate \u2192 WebCodecs\n * VideoDecoder \u2192 consumer's onFrame(VideoFrame).\n *\n * Replaces:\n * - moqtail's `decodeQueueSize > 10 drop-deltas` hack in webcodecs-backend.ts\n * - hang-mmt-fec's \"reorderWindow = 0 // TODO\" in video/decoder.ts\n *\n * Pacing uses FrameCountPacer (anchor on first decoded picture, frame count \u00D7\n * 1/fps) with threshold = mapper.pacingThresholdMs. Reorder window comes from\n * mapper.d (groups per FEC block), which is the natural bound for out-of-order\n * delivery within a block after converting catalog groups to frame count.\n */\n\nimport { MmtpDecodePipeline, type PipelineFecClient } from '@blockcast/mmt-container/mmtp-decode-pipeline'\nimport type { MfuReassemblyPolicy } from '@blockcast/mmt-container/mfu-reassembly-policy'\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\nimport { FrameCountPacer } from '../pacing/frame-pacer.js'\nimport { KeyframeGate } from './keyframe-gate.js'\n\nexport interface VideoFrameMeta {\n timestamp: number\n keyframe: boolean\n packetId: number\n}\n\nexport type VideoFrameCallback = (frame: VideoFrame, meta: VideoFrameMeta) => void\n\nexport function deriveVideoReorderWindow(\n framerate: number,\n mapper: Pick<FecBlockMapper, 'd' | 'gopDurationMs'>,\n): number {\n const framesPerGroup = Math.ceil(framerate * mapper.gopDurationMs / 1000)\n return mapper.d * framesPerGroup\n}\n\nexport interface VideoRenderPipelineOpts {\n codec: string\n /** MMTP packet_id for this video track \u2014 from catalog. */\n packetId?: number\n description?: Uint8Array\n width: number\n height: number\n /** Frames per second (used for pacing + default reorder window). */\n framerate: number\n mapper: FecBlockMapper\n fecClient?: PipelineFecClient\n /** Reliable MoQ is clock-free; datagram deadlines come from catalog/in-band timing. */\n reassemblyPolicy: MfuReassemblyPolicy\n /** Receives VideoFrames ready to render. */\n onFrame: VideoFrameCallback\n /** Called on decoder errors. */\n onError?: (err: unknown) => void\n /** Reorder window in frames. Defaults to one catalog-derived FEC block. */\n reorderWindow?: number\n /** Warmup frames before pacing engages. Default 3 (absorbs decoder init jitter). */\n warmupFrames?: number\n /** Optional decoder hardware acceleration hint. */\n hardwareAcceleration?: HardwareAcceleration\n}\n\nexport class VideoRenderPipeline {\n readonly #pacer: FrameCountPacer\n readonly #gate = new KeyframeGate()\n readonly #opts: VideoRenderPipelineOpts\n\n #decoder: VideoDecoder | null = null\n #pipeline: MmtpDecodePipeline | null = null\n #closed = false\n\n constructor(opts: VideoRenderPipelineOpts) {\n if (!(opts.framerate > 0)) throw new Error('framerate must be > 0')\n this.#opts = opts\n this.#pacer = new FrameCountPacer({\n frameDurationMs: 1000 / opts.framerate,\n mapper: opts.mapper,\n warmupFrames: opts.warmupFrames ?? 3,\n })\n this.#init()\n }\n\n feedMoqObject(packet: Uint8Array): void {\n this.#pipeline?.feedMoqObject(packet)\n }\n\n paceSleepMs(): number {\n return this.#pacer.computeSleepMs()\n }\n\n get pacerSnapshot() {\n return this.#pacer.snapshot\n }\n\n /** Request a fresh keyframe after a decoder error (or source switch). */\n reset(): void {\n this.#gate.reset()\n this.#pacer.reset()\n try { this.#decoder?.reset() } catch { /* may be closed */ }\n // Re-configure so the decoder is ready for the next keyframe.\n this.#configureDecoder()\n }\n\n close(): void {\n if (this.#closed) return\n this.#closed = true\n try { this.#decoder?.close() } catch { /* noop */ }\n try { this.#pipeline?.dispose() } catch { /* noop */ }\n this.#decoder = null\n this.#pipeline = null\n }\n\n // \u2500\u2500\u2500 Private \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n #init(): void {\n this.#configureDecoder()\n\n const pipeline = new MmtpDecodePipeline()\n const reorderWindow = this.#opts.reorderWindow\n ?? deriveVideoReorderWindow(this.#opts.framerate, this.#opts.mapper)\n pipeline.configure({\n codec: this.#opts.codec,\n packetId: this.#opts.packetId,\n description: this.#opts.description,\n reorderWindow,\n framerate: this.#opts.framerate,\n groupDurationMs: this.#opts.mapper.gopDurationMs,\n reassemblyPolicy: this.#opts.reassemblyPolicy,\n })\n if (this.#opts.fecClient) pipeline.setFecClient(this.#opts.fecClient)\n\n pipeline.onFrame((codecData, meta) => {\n if (this.#closed || !this.#decoder) return\n if (codecData.byteLength === 0) return\n if (!this.#gate.allow(meta.keyframe)) return\n\n this.#pacer.notifyFrame()\n try {\n this.#decoder.decode(new EncodedVideoChunk({\n type: meta.keyframe ? 'key' : 'delta',\n data: codecData,\n timestamp: meta.timestamp,\n }))\n } catch (err) {\n this.#opts.onError?.(err)\n this.reset()\n }\n })\n this.#pipeline = pipeline\n }\n\n #configureDecoder(): void {\n try { this.#decoder?.close() } catch { /* noop */ }\n\n const userMeta: VideoFrameMeta = { timestamp: 0, keyframe: false, packetId: 0 }\n const decoder = new VideoDecoder({\n output: (frame) => {\n // WebCodecs VideoFrame doesn't carry our frame meta; the consumer\n // rematches via frame.timestamp if needed.\n userMeta.timestamp = frame.timestamp\n this.#opts.onFrame(frame, userMeta)\n },\n error: (err) => {\n this.#opts.onError?.(err)\n this.#gate.reset()\n },\n })\n const config: VideoDecoderConfig = {\n codec: this.#opts.codec,\n codedWidth: this.#opts.width,\n codedHeight: this.#opts.height,\n ...(this.#opts.description ? { description: this.#opts.description } : {}),\n ...(this.#opts.hardwareAcceleration ? { hardwareAcceleration: this.#opts.hardwareAcceleration } : {}),\n }\n decoder.configure(config)\n this.#decoder = decoder\n }\n}\n", "/**\n * FrameCountPacer \u2014 media-agnostic wall-clock pacer for MMT A/V decode loops.\n *\n * Why not use MPU timestamps? Under FEC interleaving, encoders pack multiple\n * Access Units (AUs) per MMTP MPU with a single mpu_timestamp. The observed\n * timestamp delta between MPUs is a fraction of the real per-AU duration\n * (e.g. 10.22ms between MPUs for 21.33ms AAC frames at D=2 interleave).\n * Pacing on that delta sees \"media \u2248 wall\" and never sleeps, letting burst\n * relay delivery saturate the decoder / ring buffer.\n *\n * The Sony libatsc3 pattern (atsc3_mmt_context_mfu_depacketizer) solves this\n * by anchoring wall-clock time to the first received AU and counting AUs:\n * media_elapsed_ms = au_count \u00D7 nominal_au_duration_ms\n * The MPU timestamp is used elsewhere as an absolute-time anchor, never as\n * an incremental clock.\n *\n * The sleep threshold comes from the catalog's FEC interleaveDepthMs (via\n * FecBlockMapper.pacingThresholdMs). When no FEC is configured, a\n * NullBlockMapper yields threshold=0 and this pacer never sleeps.\n *\n * One pacer instance = one track (e.g. audio, video) on one player.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\nexport interface FrameCountPacerOpts {\n /** Required: AU duration (audio) or picture interval (video), in ms. */\n frameDurationMs: number\n /** FEC mapper from @blockcast/mmt-fec. NullBlockMapper disables pacing. */\n mapper: FecBlockMapper\n /**\n * Number of frames to let through before pacing starts. Lets startup\n * burst drain populate the ring buffer / decoder queue. Default 0.\n */\n warmupFrames?: number\n}\n\nexport interface PacerSnapshot {\n /** Frames observed (from notifyFrame). */\n frameCount: number\n /** Computed media time: frameCount \u00D7 frameDurationMs. */\n mediaMs: number\n /** Wall-clock time since anchor. */\n wallMs: number\n /** mediaMs - wallMs. Positive \u2192 decoder ahead of realtime. */\n aheadMs: number\n /** Pacing threshold (ms) from the FEC mapper. */\n thresholdMs: number\n /** Has the first frame been observed (wall anchor set)? */\n anchored: boolean\n}\n\nexport class FrameCountPacer {\n readonly frameDurationMs: number\n readonly thresholdMs: number\n readonly warmupFrames: number\n\n #frameCount = 0\n #anchorWallMs = 0\n #anchored = false\n\n constructor(opts: FrameCountPacerOpts) {\n if (!(opts.frameDurationMs > 0)) {\n throw new Error('frameDurationMs must be > 0')\n }\n this.frameDurationMs = opts.frameDurationMs\n this.thresholdMs = opts.mapper.pacingThresholdMs\n this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0)\n }\n\n /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */\n notifyFrame(): void {\n if (!this.#anchored) {\n this.#anchorWallMs = now()\n this.#anchored = true\n }\n this.#frameCount++\n }\n\n /**\n * Compute sleep ms before reading the next MoQ object.\n * Returns 0 when:\n * - pacing is disabled (NullBlockMapper \u2192 threshold=0)\n * - the anchor is not yet set\n * - still in warmup\n * - decoder is not ahead of wall clock by more than threshold\n * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).\n */\n computeSleepMs(): number {\n if (this.thresholdMs <= 0) return 0\n if (!this.#anchored) return 0\n if (this.#frameCount <= this.warmupFrames) return 0\n\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = now() - this.#anchorWallMs\n const aheadMs = mediaMs - wallMs\n if (aheadMs <= this.thresholdMs) return 0\n return aheadMs - this.thresholdMs / 2\n }\n\n /** Reset anchor + counter. Call on source switch (MoQ \u2194 multicast). */\n reset(): void {\n this.#frameCount = 0\n this.#anchorWallMs = 0\n this.#anchored = false\n }\n\n get snapshot(): PacerSnapshot {\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0\n return {\n frameCount: this.#frameCount,\n mediaMs,\n wallMs,\n aheadMs: mediaMs - wallMs,\n thresholdMs: this.thresholdMs,\n anchored: this.#anchored,\n }\n }\n}\n\nfunction now(): number {\n // performance.now() is available in main thread, workers, and AudioWorklet.\n // Fall back to Date.now() for exotic runtimes (shouldn't happen in browsers).\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return performance.now()\n }\n return Date.now()\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * KeyframeGate \u2014 drops delta frames until a keyframe arrives.\n *\n * Required when subscribing mid-stream: the relay may deliver P/B frames\n * from the middle of a GOP before the first IDR. Feeding those into\n * VideoDecoder before an init yields \"key frame required\" errors.\n *\n * Call reset() to re-arm the gate after a decoder error (requestKeyframe).\n */\n\nexport class KeyframeGate {\n #open = false\n\n get isOpen(): boolean {\n return this.#open\n }\n\n /**\n * Decide whether to pass a frame through.\n * Opens permanently once the first keyframe is seen.\n */\n allow(keyframe: boolean): boolean {\n if (this.#open) return true\n if (keyframe) {\n this.#open = true\n return true\n }\n return false\n }\n\n /** Re-close the gate. Next keyframe reopens it. */\n reset(): void {\n this.#open = false\n }\n}\n", "/**\n * VideoRenderPipeline \u2014 end-to-end video render for MoQ/MMT players.\n *\n * Pipeline: MoQ object \u2192 MmtpDecodePipeline \u2192 keyframe gate \u2192 WebCodecs\n * VideoDecoder \u2192 consumer's onFrame(VideoFrame).\n *\n * Replaces:\n * - moqtail's `decodeQueueSize > 10 drop-deltas` hack in webcodecs-backend.ts\n * - hang-mmt-fec's \"reorderWindow = 0 // TODO\" in video/decoder.ts\n *\n * Pacing uses FrameCountPacer (anchor on first decoded picture, frame count \u00D7\n * 1/fps) with threshold = mapper.pacingThresholdMs. Reorder window comes from\n * mapper.d (groups per FEC block), which is the natural bound for out-of-order\n * delivery within a block after converting catalog groups to frame count.\n */\n\nimport { MmtpDecodePipeline, type PipelineFecClient } from '@blockcast/mmt-container/mmtp-decode-pipeline'\nimport type { MfuReassemblyPolicy } from '@blockcast/mmt-container/mfu-reassembly-policy'\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\nimport { FrameCountPacer } from '../pacing/frame-pacer.js'\nimport { KeyframeGate } from './keyframe-gate.js'\n\nexport interface VideoFrameMeta {\n timestamp: number\n keyframe: boolean\n packetId: number\n}\n\nexport type VideoFrameCallback = (frame: VideoFrame, meta: VideoFrameMeta) => void\n\nexport function deriveVideoReorderWindow(\n framerate: number,\n mapper: Pick<FecBlockMapper, 'd' | 'gopDurationMs'>,\n): number {\n const framesPerGroup = Math.ceil(framerate * mapper.gopDurationMs / 1000)\n return mapper.d * framesPerGroup\n}\n\nexport interface VideoRenderPipelineOpts {\n codec: string\n /** MMTP packet_id for this video track \u2014 from catalog. */\n packetId?: number\n description?: Uint8Array\n width: number\n height: number\n /** Frames per second (used for pacing + default reorder window). */\n framerate: number\n mapper: FecBlockMapper\n fecClient?: PipelineFecClient\n /** Reliable MoQ is clock-free; datagram deadlines come from catalog/in-band timing. */\n reassemblyPolicy: MfuReassemblyPolicy\n /** Receives VideoFrames ready to render. */\n onFrame: VideoFrameCallback\n /** Called on decoder errors. */\n onError?: (err: unknown) => void\n /** Reorder window in frames. Defaults to one catalog-derived FEC block. */\n reorderWindow?: number\n /** Warmup frames before pacing engages. Default 3 (absorbs decoder init jitter). */\n warmupFrames?: number\n /** Optional decoder hardware acceleration hint. */\n hardwareAcceleration?: HardwareAcceleration\n}\n\nexport class VideoRenderPipeline {\n readonly #pacer: FrameCountPacer\n readonly #gate = new KeyframeGate()\n readonly #opts: VideoRenderPipelineOpts\n\n #decoder: VideoDecoder | null = null\n #pipeline: MmtpDecodePipeline | null = null\n #closed = false\n\n constructor(opts: VideoRenderPipelineOpts) {\n if (!(opts.framerate > 0)) throw new Error('framerate must be > 0')\n this.#opts = opts\n this.#pacer = new FrameCountPacer({\n frameDurationMs: 1000 / opts.framerate,\n mapper: opts.mapper,\n warmupFrames: opts.warmupFrames ?? 3,\n })\n this.#init()\n }\n\n feedMoqObject(packet: Uint8Array): void {\n this.#pipeline?.feedMoqObject(packet)\n }\n\n paceSleepMs(): number {\n return this.#pacer.computeSleepMs()\n }\n\n get pacerSnapshot() {\n return this.#pacer.snapshot\n }\n\n /** Request a fresh keyframe after a decoder error (or source switch). */\n reset(): void {\n this.#gate.reset()\n this.#pacer.reset()\n try { this.#decoder?.reset() } catch { /* may be closed */ }\n // Re-configure so the decoder is ready for the next keyframe.\n this.#configureDecoder()\n }\n\n close(): void {\n if (this.#closed) return\n this.#closed = true\n try { this.#decoder?.close() } catch { /* noop */ }\n try { this.#pipeline?.dispose() } catch { /* noop */ }\n this.#decoder = null\n this.#pipeline = null\n }\n\n // \u2500\u2500\u2500 Private \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n #init(): void {\n this.#configureDecoder()\n\n const pipeline = new MmtpDecodePipeline()\n const reorderWindow = this.#opts.reorderWindow\n ?? deriveVideoReorderWindow(this.#opts.framerate, this.#opts.mapper)\n pipeline.configure({\n codec: this.#opts.codec,\n packetId: this.#opts.packetId,\n description: this.#opts.description,\n reorderWindow,\n framerate: this.#opts.framerate,\n groupDurationMs: this.#opts.mapper.gopDurationMs,\n reassemblyPolicy: this.#opts.reassemblyPolicy,\n })\n if (this.#opts.fecClient) pipeline.setFecClient(this.#opts.fecClient)\n\n pipeline.onFrame((codecData, meta) => {\n if (this.#closed || !this.#decoder) return\n if (codecData.byteLength === 0) return\n if (!this.#gate.allow(meta.keyframe)) return\n\n this.#pacer.notifyFrame()\n try {\n this.#decoder.decode(new EncodedVideoChunk({\n type: meta.keyframe ? 'key' : 'delta',\n data: codecData,\n timestamp: meta.timestamp,\n }))\n } catch (err) {\n this.#opts.onError?.(err)\n this.reset()\n }\n })\n this.#pipeline = pipeline\n }\n\n #configureDecoder(): void {\n try { this.#decoder?.close() } catch { /* noop */ }\n\n const userMeta: VideoFrameMeta = { timestamp: 0, keyframe: false, packetId: 0 }\n const decoder = new VideoDecoder({\n output: (frame) => {\n // WebCodecs VideoFrame doesn't carry our frame meta; the consumer\n // rematches via frame.timestamp if needed.\n userMeta.timestamp = frame.timestamp\n this.#opts.onFrame(frame, userMeta)\n },\n error: (err) => {\n this.#opts.onError?.(err)\n this.#gate.reset()\n },\n })\n const config: VideoDecoderConfig = {\n codec: this.#opts.codec,\n codedWidth: this.#opts.width,\n codedHeight: this.#opts.height,\n ...(this.#opts.description ? { description: this.#opts.description } : {}),\n ...(this.#opts.hardwareAcceleration ? { hardwareAcceleration: this.#opts.hardwareAcceleration } : {}),\n }\n decoder.configure(config)\n this.#decoder = decoder\n }\n}\n", "/**\n * FrameCountPacer \u2014 media-agnostic wall-clock pacer for MMT A/V decode loops.\n *\n * Why not use MPU timestamps? Under FEC interleaving, encoders pack multiple\n * Access Units (AUs) per MMTP MPU with a single mpu_timestamp. The observed\n * timestamp delta between MPUs is a fraction of the real per-AU duration\n * (e.g. 10.22ms between MPUs for 21.33ms AAC frames at D=2 interleave).\n * Pacing on that delta sees \"media \u2248 wall\" and never sleeps, letting burst\n * relay delivery saturate the decoder / ring buffer.\n *\n * The Sony libatsc3 pattern (atsc3_mmt_context_mfu_depacketizer) solves this\n * by anchoring wall-clock time to the first received AU and counting AUs:\n * media_elapsed_ms = au_count \u00D7 nominal_au_duration_ms\n * The MPU timestamp is used elsewhere as an absolute-time anchor, never as\n * an incremental clock.\n *\n * The sleep threshold comes from the catalog's FEC interleaveDepthMs (via\n * FecBlockMapper.pacingThresholdMs). When no FEC is configured, a\n * NullBlockMapper yields threshold=0 and this pacer never sleeps.\n *\n * One pacer instance = one track (e.g. audio, video) on one player.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\nexport interface FrameCountPacerOpts {\n /** Required: AU duration (audio) or picture interval (video), in ms. */\n frameDurationMs: number\n /** FEC mapper from @blockcast/mmt-fec. NullBlockMapper disables pacing. */\n mapper: FecBlockMapper\n /**\n * Number of frames to let through before pacing starts. Lets startup\n * burst drain populate the ring buffer / decoder queue. Default 0.\n */\n warmupFrames?: number\n}\n\nexport interface PacerSnapshot {\n /** Frames observed (from notifyFrame). */\n frameCount: number\n /** Computed media time: frameCount \u00D7 frameDurationMs. */\n mediaMs: number\n /** Wall-clock time since anchor. */\n wallMs: number\n /** mediaMs - wallMs. Positive \u2192 decoder ahead of realtime. */\n aheadMs: number\n /** Pacing threshold (ms) from the FEC mapper. */\n thresholdMs: number\n /** Has the first frame been observed (wall anchor set)? */\n anchored: boolean\n}\n\nexport class FrameCountPacer {\n readonly frameDurationMs: number\n readonly thresholdMs: number\n readonly warmupFrames: number\n\n #frameCount = 0\n #anchorWallMs = 0\n #anchored = false\n\n constructor(opts: FrameCountPacerOpts) {\n if (!(opts.frameDurationMs > 0)) {\n throw new Error('frameDurationMs must be > 0')\n }\n this.frameDurationMs = opts.frameDurationMs\n this.thresholdMs = opts.mapper.pacingThresholdMs\n this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0)\n }\n\n /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */\n notifyFrame(): void {\n if (!this.#anchored) {\n this.#anchorWallMs = now()\n this.#anchored = true\n }\n this.#frameCount++\n }\n\n /**\n * Compute sleep ms before reading the next MoQ object.\n * Returns 0 when:\n * - pacing is disabled (NullBlockMapper \u2192 threshold=0)\n * - the anchor is not yet set\n * - still in warmup\n * - the caller reports a standing backlog (see `observedDelayMs`)\n * - decoder is not ahead of wall clock by more than threshold\n * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).\n *\n * `observedDelayMs` is how far this frame is behind the producer's own clock,\n * measured by the caller against a producer-supplied sending instant (for\n * MMTP, the header send-wall timestamp). It is optional because the anchored\n * rate below is blind to it: `#anchorWallMs` is the wall time of the FIRST\n * frame read, so `aheadMs` measures consumption against the join instant and\n * not against the producer. Once `aheadMs` reaches the threshold the loop\n * consumes at exactly the source rate, and recovering latency present at join\n * requires consuming FASTER than the source rate. So without this argument a\n * consumer that joined a live stream mid-block keeps that join latency for the\n * life of the subscription.\n *\n * A caller that can measure the real distance from the producer passes it and\n * gets a drain: while the backlog exceeds the threshold there is nothing to\n * pace against, so return 0 and let the transport set the rate. At the live\n * edge the anchored-rate behavior below applies unchanged.\n *\n * Callers with no producer clock omit it and keep the pure ATSC3-style\n * anchored pacing (see the class doc), where a first-frame anchor is correct\n * because a broadcast push has no live edge to catch up to.\n */\n computeSleepMs(observedDelayMs?: number): number {\n if (this.thresholdMs <= 0) return 0\n if (!this.#anchored) return 0\n if (this.#frameCount <= this.warmupFrames) return 0\n // Only a finite measurement can license the drain. NaN/Infinity mean the\n // caller could not measure the distance, which is not the same claim as\n // \"the consumer is at the edge\", so fall through to anchored pacing.\n if (observedDelayMs !== undefined && Number.isFinite(observedDelayMs) && observedDelayMs > this.thresholdMs) {\n return 0\n }\n\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = now() - this.#anchorWallMs\n const aheadMs = mediaMs - wallMs\n if (aheadMs <= this.thresholdMs) return 0\n return aheadMs - this.thresholdMs / 2\n }\n\n /** Reset anchor + counter. Call on source switch (MoQ \u2194 multicast). */\n reset(): void {\n this.#frameCount = 0\n this.#anchorWallMs = 0\n this.#anchored = false\n }\n\n get snapshot(): PacerSnapshot {\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0\n return {\n frameCount: this.#frameCount,\n mediaMs,\n wallMs,\n aheadMs: mediaMs - wallMs,\n thresholdMs: this.thresholdMs,\n anchored: this.#anchored,\n }\n }\n}\n\nfunction now(): number {\n // performance.now() is available in main thread, workers, and AudioWorklet.\n // Fall back to Date.now() for exotic runtimes (shouldn't happen in browsers).\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return performance.now()\n }\n return Date.now()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,38 +1,26 @@
1
- var __typeError = (msg) => {
2
- throw TypeError(msg);
3
- };
4
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
- var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
- var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
-
9
1
  // src/video/keyframe-gate.ts
10
- var _open;
11
2
  var KeyframeGate = class {
12
- constructor() {
13
- __privateAdd(this, _open, false);
14
- }
3
+ #open = false;
15
4
  get isOpen() {
16
- return __privateGet(this, _open);
5
+ return this.#open;
17
6
  }
18
7
  /**
19
8
  * Decide whether to pass a frame through.
20
9
  * Opens permanently once the first keyframe is seen.
21
10
  */
22
11
  allow(keyframe) {
23
- if (__privateGet(this, _open)) return true;
12
+ if (this.#open) return true;
24
13
  if (keyframe) {
25
- __privateSet(this, _open, true);
14
+ this.#open = true;
26
15
  return true;
27
16
  }
28
17
  return false;
29
18
  }
30
19
  /** Re-close the gate. Next keyframe reopens it. */
31
20
  reset() {
32
- __privateSet(this, _open, false);
21
+ this.#open = false;
33
22
  }
34
23
  };
35
- _open = new WeakMap();
36
24
  export {
37
25
  KeyframeGate
38
26
  };
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/video/keyframe-gate.ts"],
4
4
  "sourcesContent": ["/**\n * KeyframeGate \u2014 drops delta frames until a keyframe arrives.\n *\n * Required when subscribing mid-stream: the relay may deliver P/B frames\n * from the middle of a GOP before the first IDR. Feeding those into\n * VideoDecoder before an init yields \"key frame required\" errors.\n *\n * Call reset() to re-arm the gate after a decoder error (requestKeyframe).\n */\n\nexport class KeyframeGate {\n #open = false\n\n get isOpen(): boolean {\n return this.#open\n }\n\n /**\n * Decide whether to pass a frame through.\n * Opens permanently once the first keyframe is seen.\n */\n allow(keyframe: boolean): boolean {\n if (this.#open) return true\n if (keyframe) {\n this.#open = true\n return true\n }\n return false\n }\n\n /** Re-close the gate. Next keyframe reopens it. */\n reset(): void {\n this.#open = false\n }\n}\n"],
5
- "mappings": ";;;;;;;;;AAAA;AAUO,IAAM,eAAN,MAAmB;AAAA,EAAnB;AACL,8BAAQ;AAAA;AAAA,EAER,IAAI,SAAkB;AACpB,WAAO,mBAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAA4B;AAChC,QAAI,mBAAK,OAAO,QAAO;AACvB,QAAI,UAAU;AACZ,yBAAK,OAAQ;AACb,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,QAAc;AACZ,uBAAK,OAAQ;AAAA,EACf;AACF;AAvBE;",
5
+ "mappings": ";AAUO,IAAM,eAAN,MAAmB;AAAA,EACxB,QAAQ;AAAA,EAER,IAAI,SAAkB;AACpB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAA4B;AAChC,QAAI,KAAK,MAAO,QAAO;AACvB,QAAI,UAAU;AACZ,WAAK,QAAQ;AACb,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,30 +1,12 @@
1
- var __typeError = (msg) => {
2
- throw TypeError(msg);
3
- };
4
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
- var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
- var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
- var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
9
- var __privateWrapper = (obj, member, setter, getter) => ({
10
- set _(value) {
11
- __privateSet(obj, member, value, setter);
12
- },
13
- get _() {
14
- return __privateGet(obj, member, getter);
15
- }
16
- });
17
-
18
1
  // src/video/video-render-pipeline.ts
19
2
  import { MmtpDecodePipeline } from "@blockcast/mmt-container/mmtp-decode-pipeline";
20
3
 
21
4
  // src/pacing/frame-pacer.ts
22
- var _frameCount, _anchorWallMs, _anchored;
23
5
  var FrameCountPacer = class {
6
+ #frameCount = 0;
7
+ #anchorWallMs = 0;
8
+ #anchored = false;
24
9
  constructor(opts) {
25
- __privateAdd(this, _frameCount, 0);
26
- __privateAdd(this, _anchorWallMs, 0);
27
- __privateAdd(this, _anchored, false);
28
10
  if (!(opts.frameDurationMs > 0)) {
29
11
  throw new Error("frameDurationMs must be > 0");
30
12
  }
@@ -34,11 +16,11 @@ var FrameCountPacer = class {
34
16
  }
35
17
  /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */
36
18
  notifyFrame() {
37
- if (!__privateGet(this, _anchored)) {
38
- __privateSet(this, _anchorWallMs, now());
39
- __privateSet(this, _anchored, true);
19
+ if (!this.#anchored) {
20
+ this.#anchorWallMs = now();
21
+ this.#anchored = true;
40
22
  }
41
- __privateWrapper(this, _frameCount)._++;
23
+ this.#frameCount++;
42
24
  }
43
25
  /**
44
26
  * Compute sleep ms before reading the next MoQ object.
@@ -46,41 +28,62 @@ var FrameCountPacer = class {
46
28
  * - pacing is disabled (NullBlockMapper → threshold=0)
47
29
  * - the anchor is not yet set
48
30
  * - still in warmup
31
+ * - the caller reports a standing backlog (see `observedDelayMs`)
49
32
  * - decoder is not ahead of wall clock by more than threshold
50
33
  * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).
34
+ *
35
+ * `observedDelayMs` is how far this frame is behind the producer's own clock,
36
+ * measured by the caller against a producer-supplied sending instant (for
37
+ * MMTP, the header send-wall timestamp). It is optional because the anchored
38
+ * rate below is blind to it: `#anchorWallMs` is the wall time of the FIRST
39
+ * frame read, so `aheadMs` measures consumption against the join instant and
40
+ * not against the producer. Once `aheadMs` reaches the threshold the loop
41
+ * consumes at exactly the source rate, and recovering latency present at join
42
+ * requires consuming FASTER than the source rate. So without this argument a
43
+ * consumer that joined a live stream mid-block keeps that join latency for the
44
+ * life of the subscription.
45
+ *
46
+ * A caller that can measure the real distance from the producer passes it and
47
+ * gets a drain: while the backlog exceeds the threshold there is nothing to
48
+ * pace against, so return 0 and let the transport set the rate. At the live
49
+ * edge the anchored-rate behavior below applies unchanged.
50
+ *
51
+ * Callers with no producer clock omit it and keep the pure ATSC3-style
52
+ * anchored pacing (see the class doc), where a first-frame anchor is correct
53
+ * because a broadcast push has no live edge to catch up to.
51
54
  */
52
- computeSleepMs() {
55
+ computeSleepMs(observedDelayMs) {
53
56
  if (this.thresholdMs <= 0) return 0;
54
- if (!__privateGet(this, _anchored)) return 0;
55
- if (__privateGet(this, _frameCount) <= this.warmupFrames) return 0;
56
- const mediaMs = __privateGet(this, _frameCount) * this.frameDurationMs;
57
- const wallMs = now() - __privateGet(this, _anchorWallMs);
57
+ if (!this.#anchored) return 0;
58
+ if (this.#frameCount <= this.warmupFrames) return 0;
59
+ if (observedDelayMs !== void 0 && Number.isFinite(observedDelayMs) && observedDelayMs > this.thresholdMs) {
60
+ return 0;
61
+ }
62
+ const mediaMs = this.#frameCount * this.frameDurationMs;
63
+ const wallMs = now() - this.#anchorWallMs;
58
64
  const aheadMs = mediaMs - wallMs;
59
65
  if (aheadMs <= this.thresholdMs) return 0;
60
66
  return aheadMs - this.thresholdMs / 2;
61
67
  }
62
68
  /** Reset anchor + counter. Call on source switch (MoQ ↔ multicast). */
63
69
  reset() {
64
- __privateSet(this, _frameCount, 0);
65
- __privateSet(this, _anchorWallMs, 0);
66
- __privateSet(this, _anchored, false);
70
+ this.#frameCount = 0;
71
+ this.#anchorWallMs = 0;
72
+ this.#anchored = false;
67
73
  }
68
74
  get snapshot() {
69
- const mediaMs = __privateGet(this, _frameCount) * this.frameDurationMs;
70
- const wallMs = __privateGet(this, _anchored) ? now() - __privateGet(this, _anchorWallMs) : 0;
75
+ const mediaMs = this.#frameCount * this.frameDurationMs;
76
+ const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0;
71
77
  return {
72
- frameCount: __privateGet(this, _frameCount),
78
+ frameCount: this.#frameCount,
73
79
  mediaMs,
74
80
  wallMs,
75
81
  aheadMs: mediaMs - wallMs,
76
82
  thresholdMs: this.thresholdMs,
77
- anchored: __privateGet(this, _anchored)
83
+ anchored: this.#anchored
78
84
  };
79
85
  }
80
86
  };
81
- _frameCount = new WeakMap();
82
- _anchorWallMs = new WeakMap();
83
- _anchored = new WeakMap();
84
87
  function now() {
85
88
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
86
89
  return performance.now();
@@ -89,156 +92,143 @@ function now() {
89
92
  }
90
93
 
91
94
  // src/video/keyframe-gate.ts
92
- var _open;
93
95
  var KeyframeGate = class {
94
- constructor() {
95
- __privateAdd(this, _open, false);
96
- }
96
+ #open = false;
97
97
  get isOpen() {
98
- return __privateGet(this, _open);
98
+ return this.#open;
99
99
  }
100
100
  /**
101
101
  * Decide whether to pass a frame through.
102
102
  * Opens permanently once the first keyframe is seen.
103
103
  */
104
104
  allow(keyframe) {
105
- if (__privateGet(this, _open)) return true;
105
+ if (this.#open) return true;
106
106
  if (keyframe) {
107
- __privateSet(this, _open, true);
107
+ this.#open = true;
108
108
  return true;
109
109
  }
110
110
  return false;
111
111
  }
112
112
  /** Re-close the gate. Next keyframe reopens it. */
113
113
  reset() {
114
- __privateSet(this, _open, false);
114
+ this.#open = false;
115
115
  }
116
116
  };
117
- _open = new WeakMap();
118
117
 
119
118
  // src/video/video-render-pipeline.ts
120
119
  function deriveVideoReorderWindow(framerate, mapper) {
121
120
  const framesPerGroup = Math.ceil(framerate * mapper.gopDurationMs / 1e3);
122
121
  return mapper.d * framesPerGroup;
123
122
  }
124
- var _pacer, _gate, _opts, _decoder, _pipeline, _closed, _VideoRenderPipeline_instances, init_fn, configureDecoder_fn;
125
123
  var VideoRenderPipeline = class {
124
+ #pacer;
125
+ #gate = new KeyframeGate();
126
+ #opts;
127
+ #decoder = null;
128
+ #pipeline = null;
129
+ #closed = false;
126
130
  constructor(opts) {
127
- __privateAdd(this, _VideoRenderPipeline_instances);
128
- __privateAdd(this, _pacer);
129
- __privateAdd(this, _gate, new KeyframeGate());
130
- __privateAdd(this, _opts);
131
- __privateAdd(this, _decoder, null);
132
- __privateAdd(this, _pipeline, null);
133
- __privateAdd(this, _closed, false);
134
131
  if (!(opts.framerate > 0)) throw new Error("framerate must be > 0");
135
- __privateSet(this, _opts, opts);
136
- __privateSet(this, _pacer, new FrameCountPacer({
132
+ this.#opts = opts;
133
+ this.#pacer = new FrameCountPacer({
137
134
  frameDurationMs: 1e3 / opts.framerate,
138
135
  mapper: opts.mapper,
139
136
  warmupFrames: opts.warmupFrames ?? 3
140
- }));
141
- __privateMethod(this, _VideoRenderPipeline_instances, init_fn).call(this);
137
+ });
138
+ this.#init();
142
139
  }
143
140
  feedMoqObject(packet) {
144
- __privateGet(this, _pipeline)?.feedMoqObject(packet);
141
+ this.#pipeline?.feedMoqObject(packet);
145
142
  }
146
143
  paceSleepMs() {
147
- return __privateGet(this, _pacer).computeSleepMs();
144
+ return this.#pacer.computeSleepMs();
148
145
  }
149
146
  get pacerSnapshot() {
150
- return __privateGet(this, _pacer).snapshot;
147
+ return this.#pacer.snapshot;
151
148
  }
152
149
  /** Request a fresh keyframe after a decoder error (or source switch). */
153
150
  reset() {
154
- __privateGet(this, _gate).reset();
155
- __privateGet(this, _pacer).reset();
151
+ this.#gate.reset();
152
+ this.#pacer.reset();
156
153
  try {
157
- __privateGet(this, _decoder)?.reset();
154
+ this.#decoder?.reset();
158
155
  } catch {
159
156
  }
160
- __privateMethod(this, _VideoRenderPipeline_instances, configureDecoder_fn).call(this);
157
+ this.#configureDecoder();
161
158
  }
162
159
  close() {
163
- if (__privateGet(this, _closed)) return;
164
- __privateSet(this, _closed, true);
160
+ if (this.#closed) return;
161
+ this.#closed = true;
165
162
  try {
166
- __privateGet(this, _decoder)?.close();
163
+ this.#decoder?.close();
167
164
  } catch {
168
165
  }
169
166
  try {
170
- __privateGet(this, _pipeline)?.dispose();
167
+ this.#pipeline?.dispose();
171
168
  } catch {
172
169
  }
173
- __privateSet(this, _decoder, null);
174
- __privateSet(this, _pipeline, null);
175
- }
176
- };
177
- _pacer = new WeakMap();
178
- _gate = new WeakMap();
179
- _opts = new WeakMap();
180
- _decoder = new WeakMap();
181
- _pipeline = new WeakMap();
182
- _closed = new WeakMap();
183
- _VideoRenderPipeline_instances = new WeakSet();
184
- // ─── Private ─────────────────────────────────────────────────────
185
- init_fn = function() {
186
- __privateMethod(this, _VideoRenderPipeline_instances, configureDecoder_fn).call(this);
187
- const pipeline = new MmtpDecodePipeline();
188
- const reorderWindow = __privateGet(this, _opts).reorderWindow ?? deriveVideoReorderWindow(__privateGet(this, _opts).framerate, __privateGet(this, _opts).mapper);
189
- pipeline.configure({
190
- codec: __privateGet(this, _opts).codec,
191
- packetId: __privateGet(this, _opts).packetId,
192
- description: __privateGet(this, _opts).description,
193
- reorderWindow,
194
- framerate: __privateGet(this, _opts).framerate,
195
- groupDurationMs: __privateGet(this, _opts).mapper.gopDurationMs,
196
- reassemblyPolicy: __privateGet(this, _opts).reassemblyPolicy
197
- });
198
- if (__privateGet(this, _opts).fecClient) pipeline.setFecClient(__privateGet(this, _opts).fecClient);
199
- pipeline.onFrame((codecData, meta) => {
200
- if (__privateGet(this, _closed) || !__privateGet(this, _decoder)) return;
201
- if (codecData.byteLength === 0) return;
202
- if (!__privateGet(this, _gate).allow(meta.keyframe)) return;
203
- __privateGet(this, _pacer).notifyFrame();
170
+ this.#decoder = null;
171
+ this.#pipeline = null;
172
+ }
173
+ // ─── Private ─────────────────────────────────────────────────────
174
+ #init() {
175
+ this.#configureDecoder();
176
+ const pipeline = new MmtpDecodePipeline();
177
+ const reorderWindow = this.#opts.reorderWindow ?? deriveVideoReorderWindow(this.#opts.framerate, this.#opts.mapper);
178
+ pipeline.configure({
179
+ codec: this.#opts.codec,
180
+ packetId: this.#opts.packetId,
181
+ description: this.#opts.description,
182
+ reorderWindow,
183
+ framerate: this.#opts.framerate,
184
+ groupDurationMs: this.#opts.mapper.gopDurationMs,
185
+ reassemblyPolicy: this.#opts.reassemblyPolicy
186
+ });
187
+ if (this.#opts.fecClient) pipeline.setFecClient(this.#opts.fecClient);
188
+ pipeline.onFrame((codecData, meta) => {
189
+ if (this.#closed || !this.#decoder) return;
190
+ if (codecData.byteLength === 0) return;
191
+ if (!this.#gate.allow(meta.keyframe)) return;
192
+ this.#pacer.notifyFrame();
193
+ try {
194
+ this.#decoder.decode(new EncodedVideoChunk({
195
+ type: meta.keyframe ? "key" : "delta",
196
+ data: codecData,
197
+ timestamp: meta.timestamp
198
+ }));
199
+ } catch (err) {
200
+ this.#opts.onError?.(err);
201
+ this.reset();
202
+ }
203
+ });
204
+ this.#pipeline = pipeline;
205
+ }
206
+ #configureDecoder() {
204
207
  try {
205
- __privateGet(this, _decoder).decode(new EncodedVideoChunk({
206
- type: meta.keyframe ? "key" : "delta",
207
- data: codecData,
208
- timestamp: meta.timestamp
209
- }));
210
- } catch (err) {
211
- __privateGet(this, _opts).onError?.(err);
212
- this.reset();
213
- }
214
- });
215
- __privateSet(this, _pipeline, pipeline);
216
- };
217
- configureDecoder_fn = function() {
218
- try {
219
- __privateGet(this, _decoder)?.close();
220
- } catch {
221
- }
222
- const userMeta = { timestamp: 0, keyframe: false, packetId: 0 };
223
- const decoder = new VideoDecoder({
224
- output: (frame) => {
225
- userMeta.timestamp = frame.timestamp;
226
- __privateGet(this, _opts).onFrame(frame, userMeta);
227
- },
228
- error: (err) => {
229
- __privateGet(this, _opts).onError?.(err);
230
- __privateGet(this, _gate).reset();
208
+ this.#decoder?.close();
209
+ } catch {
231
210
  }
232
- });
233
- const config = {
234
- codec: __privateGet(this, _opts).codec,
235
- codedWidth: __privateGet(this, _opts).width,
236
- codedHeight: __privateGet(this, _opts).height,
237
- ...__privateGet(this, _opts).description ? { description: __privateGet(this, _opts).description } : {},
238
- ...__privateGet(this, _opts).hardwareAcceleration ? { hardwareAcceleration: __privateGet(this, _opts).hardwareAcceleration } : {}
239
- };
240
- decoder.configure(config);
241
- __privateSet(this, _decoder, decoder);
211
+ const userMeta = { timestamp: 0, keyframe: false, packetId: 0 };
212
+ const decoder = new VideoDecoder({
213
+ output: (frame) => {
214
+ userMeta.timestamp = frame.timestamp;
215
+ this.#opts.onFrame(frame, userMeta);
216
+ },
217
+ error: (err) => {
218
+ this.#opts.onError?.(err);
219
+ this.#gate.reset();
220
+ }
221
+ });
222
+ const config = {
223
+ codec: this.#opts.codec,
224
+ codedWidth: this.#opts.width,
225
+ codedHeight: this.#opts.height,
226
+ ...this.#opts.description ? { description: this.#opts.description } : {},
227
+ ...this.#opts.hardwareAcceleration ? { hardwareAcceleration: this.#opts.hardwareAcceleration } : {}
228
+ };
229
+ decoder.configure(config);
230
+ this.#decoder = decoder;
231
+ }
242
232
  };
243
233
  export {
244
234
  VideoRenderPipeline,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/video/video-render-pipeline.ts", "../../src/pacing/frame-pacer.ts", "../../src/video/keyframe-gate.ts"],
4
- "sourcesContent": ["/**\n * VideoRenderPipeline \u2014 end-to-end video render for MoQ/MMT players.\n *\n * Pipeline: MoQ object \u2192 MmtpDecodePipeline \u2192 keyframe gate \u2192 WebCodecs\n * VideoDecoder \u2192 consumer's onFrame(VideoFrame).\n *\n * Replaces:\n * - moqtail's `decodeQueueSize > 10 drop-deltas` hack in webcodecs-backend.ts\n * - hang-mmt-fec's \"reorderWindow = 0 // TODO\" in video/decoder.ts\n *\n * Pacing uses FrameCountPacer (anchor on first decoded picture, frame count \u00D7\n * 1/fps) with threshold = mapper.pacingThresholdMs. Reorder window comes from\n * mapper.d (groups per FEC block), which is the natural bound for out-of-order\n * delivery within a block after converting catalog groups to frame count.\n */\n\nimport { MmtpDecodePipeline, type PipelineFecClient } from '@blockcast/mmt-container/mmtp-decode-pipeline'\nimport type { MfuReassemblyPolicy } from '@blockcast/mmt-container/mfu-reassembly-policy'\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\nimport { FrameCountPacer } from '../pacing/frame-pacer.js'\nimport { KeyframeGate } from './keyframe-gate.js'\n\nexport interface VideoFrameMeta {\n timestamp: number\n keyframe: boolean\n packetId: number\n}\n\nexport type VideoFrameCallback = (frame: VideoFrame, meta: VideoFrameMeta) => void\n\nexport function deriveVideoReorderWindow(\n framerate: number,\n mapper: Pick<FecBlockMapper, 'd' | 'gopDurationMs'>,\n): number {\n const framesPerGroup = Math.ceil(framerate * mapper.gopDurationMs / 1000)\n return mapper.d * framesPerGroup\n}\n\nexport interface VideoRenderPipelineOpts {\n codec: string\n /** MMTP packet_id for this video track \u2014 from catalog. */\n packetId?: number\n description?: Uint8Array\n width: number\n height: number\n /** Frames per second (used for pacing + default reorder window). */\n framerate: number\n mapper: FecBlockMapper\n fecClient?: PipelineFecClient\n /** Reliable MoQ is clock-free; datagram deadlines come from catalog/in-band timing. */\n reassemblyPolicy: MfuReassemblyPolicy\n /** Receives VideoFrames ready to render. */\n onFrame: VideoFrameCallback\n /** Called on decoder errors. */\n onError?: (err: unknown) => void\n /** Reorder window in frames. Defaults to one catalog-derived FEC block. */\n reorderWindow?: number\n /** Warmup frames before pacing engages. Default 3 (absorbs decoder init jitter). */\n warmupFrames?: number\n /** Optional decoder hardware acceleration hint. */\n hardwareAcceleration?: HardwareAcceleration\n}\n\nexport class VideoRenderPipeline {\n readonly #pacer: FrameCountPacer\n readonly #gate = new KeyframeGate()\n readonly #opts: VideoRenderPipelineOpts\n\n #decoder: VideoDecoder | null = null\n #pipeline: MmtpDecodePipeline | null = null\n #closed = false\n\n constructor(opts: VideoRenderPipelineOpts) {\n if (!(opts.framerate > 0)) throw new Error('framerate must be > 0')\n this.#opts = opts\n this.#pacer = new FrameCountPacer({\n frameDurationMs: 1000 / opts.framerate,\n mapper: opts.mapper,\n warmupFrames: opts.warmupFrames ?? 3,\n })\n this.#init()\n }\n\n feedMoqObject(packet: Uint8Array): void {\n this.#pipeline?.feedMoqObject(packet)\n }\n\n paceSleepMs(): number {\n return this.#pacer.computeSleepMs()\n }\n\n get pacerSnapshot() {\n return this.#pacer.snapshot\n }\n\n /** Request a fresh keyframe after a decoder error (or source switch). */\n reset(): void {\n this.#gate.reset()\n this.#pacer.reset()\n try { this.#decoder?.reset() } catch { /* may be closed */ }\n // Re-configure so the decoder is ready for the next keyframe.\n this.#configureDecoder()\n }\n\n close(): void {\n if (this.#closed) return\n this.#closed = true\n try { this.#decoder?.close() } catch { /* noop */ }\n try { this.#pipeline?.dispose() } catch { /* noop */ }\n this.#decoder = null\n this.#pipeline = null\n }\n\n // \u2500\u2500\u2500 Private \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n #init(): void {\n this.#configureDecoder()\n\n const pipeline = new MmtpDecodePipeline()\n const reorderWindow = this.#opts.reorderWindow\n ?? deriveVideoReorderWindow(this.#opts.framerate, this.#opts.mapper)\n pipeline.configure({\n codec: this.#opts.codec,\n packetId: this.#opts.packetId,\n description: this.#opts.description,\n reorderWindow,\n framerate: this.#opts.framerate,\n groupDurationMs: this.#opts.mapper.gopDurationMs,\n reassemblyPolicy: this.#opts.reassemblyPolicy,\n })\n if (this.#opts.fecClient) pipeline.setFecClient(this.#opts.fecClient)\n\n pipeline.onFrame((codecData, meta) => {\n if (this.#closed || !this.#decoder) return\n if (codecData.byteLength === 0) return\n if (!this.#gate.allow(meta.keyframe)) return\n\n this.#pacer.notifyFrame()\n try {\n this.#decoder.decode(new EncodedVideoChunk({\n type: meta.keyframe ? 'key' : 'delta',\n data: codecData,\n timestamp: meta.timestamp,\n }))\n } catch (err) {\n this.#opts.onError?.(err)\n this.reset()\n }\n })\n this.#pipeline = pipeline\n }\n\n #configureDecoder(): void {\n try { this.#decoder?.close() } catch { /* noop */ }\n\n const userMeta: VideoFrameMeta = { timestamp: 0, keyframe: false, packetId: 0 }\n const decoder = new VideoDecoder({\n output: (frame) => {\n // WebCodecs VideoFrame doesn't carry our frame meta; the consumer\n // rematches via frame.timestamp if needed.\n userMeta.timestamp = frame.timestamp\n this.#opts.onFrame(frame, userMeta)\n },\n error: (err) => {\n this.#opts.onError?.(err)\n this.#gate.reset()\n },\n })\n const config: VideoDecoderConfig = {\n codec: this.#opts.codec,\n codedWidth: this.#opts.width,\n codedHeight: this.#opts.height,\n ...(this.#opts.description ? { description: this.#opts.description } : {}),\n ...(this.#opts.hardwareAcceleration ? { hardwareAcceleration: this.#opts.hardwareAcceleration } : {}),\n }\n decoder.configure(config)\n this.#decoder = decoder\n }\n}\n", "/**\n * FrameCountPacer \u2014 media-agnostic wall-clock pacer for MMT A/V decode loops.\n *\n * Why not use MPU timestamps? Under FEC interleaving, encoders pack multiple\n * Access Units (AUs) per MMTP MPU with a single mpu_timestamp. The observed\n * timestamp delta between MPUs is a fraction of the real per-AU duration\n * (e.g. 10.22ms between MPUs for 21.33ms AAC frames at D=2 interleave).\n * Pacing on that delta sees \"media \u2248 wall\" and never sleeps, letting burst\n * relay delivery saturate the decoder / ring buffer.\n *\n * The Sony libatsc3 pattern (atsc3_mmt_context_mfu_depacketizer) solves this\n * by anchoring wall-clock time to the first received AU and counting AUs:\n * media_elapsed_ms = au_count \u00D7 nominal_au_duration_ms\n * The MPU timestamp is used elsewhere as an absolute-time anchor, never as\n * an incremental clock.\n *\n * The sleep threshold comes from the catalog's FEC interleaveDepthMs (via\n * FecBlockMapper.pacingThresholdMs). When no FEC is configured, a\n * NullBlockMapper yields threshold=0 and this pacer never sleeps.\n *\n * One pacer instance = one track (e.g. audio, video) on one player.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\nexport interface FrameCountPacerOpts {\n /** Required: AU duration (audio) or picture interval (video), in ms. */\n frameDurationMs: number\n /** FEC mapper from @blockcast/mmt-fec. NullBlockMapper disables pacing. */\n mapper: FecBlockMapper\n /**\n * Number of frames to let through before pacing starts. Lets startup\n * burst drain populate the ring buffer / decoder queue. Default 0.\n */\n warmupFrames?: number\n}\n\nexport interface PacerSnapshot {\n /** Frames observed (from notifyFrame). */\n frameCount: number\n /** Computed media time: frameCount \u00D7 frameDurationMs. */\n mediaMs: number\n /** Wall-clock time since anchor. */\n wallMs: number\n /** mediaMs - wallMs. Positive \u2192 decoder ahead of realtime. */\n aheadMs: number\n /** Pacing threshold (ms) from the FEC mapper. */\n thresholdMs: number\n /** Has the first frame been observed (wall anchor set)? */\n anchored: boolean\n}\n\nexport class FrameCountPacer {\n readonly frameDurationMs: number\n readonly thresholdMs: number\n readonly warmupFrames: number\n\n #frameCount = 0\n #anchorWallMs = 0\n #anchored = false\n\n constructor(opts: FrameCountPacerOpts) {\n if (!(opts.frameDurationMs > 0)) {\n throw new Error('frameDurationMs must be > 0')\n }\n this.frameDurationMs = opts.frameDurationMs\n this.thresholdMs = opts.mapper.pacingThresholdMs\n this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0)\n }\n\n /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */\n notifyFrame(): void {\n if (!this.#anchored) {\n this.#anchorWallMs = now()\n this.#anchored = true\n }\n this.#frameCount++\n }\n\n /**\n * Compute sleep ms before reading the next MoQ object.\n * Returns 0 when:\n * - pacing is disabled (NullBlockMapper \u2192 threshold=0)\n * - the anchor is not yet set\n * - still in warmup\n * - decoder is not ahead of wall clock by more than threshold\n * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).\n */\n computeSleepMs(): number {\n if (this.thresholdMs <= 0) return 0\n if (!this.#anchored) return 0\n if (this.#frameCount <= this.warmupFrames) return 0\n\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = now() - this.#anchorWallMs\n const aheadMs = mediaMs - wallMs\n if (aheadMs <= this.thresholdMs) return 0\n return aheadMs - this.thresholdMs / 2\n }\n\n /** Reset anchor + counter. Call on source switch (MoQ \u2194 multicast). */\n reset(): void {\n this.#frameCount = 0\n this.#anchorWallMs = 0\n this.#anchored = false\n }\n\n get snapshot(): PacerSnapshot {\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0\n return {\n frameCount: this.#frameCount,\n mediaMs,\n wallMs,\n aheadMs: mediaMs - wallMs,\n thresholdMs: this.thresholdMs,\n anchored: this.#anchored,\n }\n }\n}\n\nfunction now(): number {\n // performance.now() is available in main thread, workers, and AudioWorklet.\n // Fall back to Date.now() for exotic runtimes (shouldn't happen in browsers).\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return performance.now()\n }\n return Date.now()\n}\n", "/**\n * KeyframeGate \u2014 drops delta frames until a keyframe arrives.\n *\n * Required when subscribing mid-stream: the relay may deliver P/B frames\n * from the middle of a GOP before the first IDR. Feeding those into\n * VideoDecoder before an init yields \"key frame required\" errors.\n *\n * Call reset() to re-arm the gate after a decoder error (requestKeyframe).\n */\n\nexport class KeyframeGate {\n #open = false\n\n get isOpen(): boolean {\n return this.#open\n }\n\n /**\n * Decide whether to pass a frame through.\n * Opens permanently once the first keyframe is seen.\n */\n allow(keyframe: boolean): boolean {\n if (this.#open) return true\n if (keyframe) {\n this.#open = true\n return true\n }\n return false\n }\n\n /** Re-close the gate. Next keyframe reopens it. */\n reset(): void {\n this.#open = false\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * VideoRenderPipeline \u2014 end-to-end video render for MoQ/MMT players.\n *\n * Pipeline: MoQ object \u2192 MmtpDecodePipeline \u2192 keyframe gate \u2192 WebCodecs\n * VideoDecoder \u2192 consumer's onFrame(VideoFrame).\n *\n * Replaces:\n * - moqtail's `decodeQueueSize > 10 drop-deltas` hack in webcodecs-backend.ts\n * - hang-mmt-fec's \"reorderWindow = 0 // TODO\" in video/decoder.ts\n *\n * Pacing uses FrameCountPacer (anchor on first decoded picture, frame count \u00D7\n * 1/fps) with threshold = mapper.pacingThresholdMs. Reorder window comes from\n * mapper.d (groups per FEC block), which is the natural bound for out-of-order\n * delivery within a block after converting catalog groups to frame count.\n */\n\nimport { MmtpDecodePipeline, type PipelineFecClient } from '@blockcast/mmt-container/mmtp-decode-pipeline'\nimport type { MfuReassemblyPolicy } from '@blockcast/mmt-container/mfu-reassembly-policy'\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\nimport { FrameCountPacer } from '../pacing/frame-pacer.js'\nimport { KeyframeGate } from './keyframe-gate.js'\n\nexport interface VideoFrameMeta {\n timestamp: number\n keyframe: boolean\n packetId: number\n}\n\nexport type VideoFrameCallback = (frame: VideoFrame, meta: VideoFrameMeta) => void\n\nexport function deriveVideoReorderWindow(\n framerate: number,\n mapper: Pick<FecBlockMapper, 'd' | 'gopDurationMs'>,\n): number {\n const framesPerGroup = Math.ceil(framerate * mapper.gopDurationMs / 1000)\n return mapper.d * framesPerGroup\n}\n\nexport interface VideoRenderPipelineOpts {\n codec: string\n /** MMTP packet_id for this video track \u2014 from catalog. */\n packetId?: number\n description?: Uint8Array\n width: number\n height: number\n /** Frames per second (used for pacing + default reorder window). */\n framerate: number\n mapper: FecBlockMapper\n fecClient?: PipelineFecClient\n /** Reliable MoQ is clock-free; datagram deadlines come from catalog/in-band timing. */\n reassemblyPolicy: MfuReassemblyPolicy\n /** Receives VideoFrames ready to render. */\n onFrame: VideoFrameCallback\n /** Called on decoder errors. */\n onError?: (err: unknown) => void\n /** Reorder window in frames. Defaults to one catalog-derived FEC block. */\n reorderWindow?: number\n /** Warmup frames before pacing engages. Default 3 (absorbs decoder init jitter). */\n warmupFrames?: number\n /** Optional decoder hardware acceleration hint. */\n hardwareAcceleration?: HardwareAcceleration\n}\n\nexport class VideoRenderPipeline {\n readonly #pacer: FrameCountPacer\n readonly #gate = new KeyframeGate()\n readonly #opts: VideoRenderPipelineOpts\n\n #decoder: VideoDecoder | null = null\n #pipeline: MmtpDecodePipeline | null = null\n #closed = false\n\n constructor(opts: VideoRenderPipelineOpts) {\n if (!(opts.framerate > 0)) throw new Error('framerate must be > 0')\n this.#opts = opts\n this.#pacer = new FrameCountPacer({\n frameDurationMs: 1000 / opts.framerate,\n mapper: opts.mapper,\n warmupFrames: opts.warmupFrames ?? 3,\n })\n this.#init()\n }\n\n feedMoqObject(packet: Uint8Array): void {\n this.#pipeline?.feedMoqObject(packet)\n }\n\n paceSleepMs(): number {\n return this.#pacer.computeSleepMs()\n }\n\n get pacerSnapshot() {\n return this.#pacer.snapshot\n }\n\n /** Request a fresh keyframe after a decoder error (or source switch). */\n reset(): void {\n this.#gate.reset()\n this.#pacer.reset()\n try { this.#decoder?.reset() } catch { /* may be closed */ }\n // Re-configure so the decoder is ready for the next keyframe.\n this.#configureDecoder()\n }\n\n close(): void {\n if (this.#closed) return\n this.#closed = true\n try { this.#decoder?.close() } catch { /* noop */ }\n try { this.#pipeline?.dispose() } catch { /* noop */ }\n this.#decoder = null\n this.#pipeline = null\n }\n\n // \u2500\u2500\u2500 Private \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n #init(): void {\n this.#configureDecoder()\n\n const pipeline = new MmtpDecodePipeline()\n const reorderWindow = this.#opts.reorderWindow\n ?? deriveVideoReorderWindow(this.#opts.framerate, this.#opts.mapper)\n pipeline.configure({\n codec: this.#opts.codec,\n packetId: this.#opts.packetId,\n description: this.#opts.description,\n reorderWindow,\n framerate: this.#opts.framerate,\n groupDurationMs: this.#opts.mapper.gopDurationMs,\n reassemblyPolicy: this.#opts.reassemblyPolicy,\n })\n if (this.#opts.fecClient) pipeline.setFecClient(this.#opts.fecClient)\n\n pipeline.onFrame((codecData, meta) => {\n if (this.#closed || !this.#decoder) return\n if (codecData.byteLength === 0) return\n if (!this.#gate.allow(meta.keyframe)) return\n\n this.#pacer.notifyFrame()\n try {\n this.#decoder.decode(new EncodedVideoChunk({\n type: meta.keyframe ? 'key' : 'delta',\n data: codecData,\n timestamp: meta.timestamp,\n }))\n } catch (err) {\n this.#opts.onError?.(err)\n this.reset()\n }\n })\n this.#pipeline = pipeline\n }\n\n #configureDecoder(): void {\n try { this.#decoder?.close() } catch { /* noop */ }\n\n const userMeta: VideoFrameMeta = { timestamp: 0, keyframe: false, packetId: 0 }\n const decoder = new VideoDecoder({\n output: (frame) => {\n // WebCodecs VideoFrame doesn't carry our frame meta; the consumer\n // rematches via frame.timestamp if needed.\n userMeta.timestamp = frame.timestamp\n this.#opts.onFrame(frame, userMeta)\n },\n error: (err) => {\n this.#opts.onError?.(err)\n this.#gate.reset()\n },\n })\n const config: VideoDecoderConfig = {\n codec: this.#opts.codec,\n codedWidth: this.#opts.width,\n codedHeight: this.#opts.height,\n ...(this.#opts.description ? { description: this.#opts.description } : {}),\n ...(this.#opts.hardwareAcceleration ? { hardwareAcceleration: this.#opts.hardwareAcceleration } : {}),\n }\n decoder.configure(config)\n this.#decoder = decoder\n }\n}\n", "/**\n * FrameCountPacer \u2014 media-agnostic wall-clock pacer for MMT A/V decode loops.\n *\n * Why not use MPU timestamps? Under FEC interleaving, encoders pack multiple\n * Access Units (AUs) per MMTP MPU with a single mpu_timestamp. The observed\n * timestamp delta between MPUs is a fraction of the real per-AU duration\n * (e.g. 10.22ms between MPUs for 21.33ms AAC frames at D=2 interleave).\n * Pacing on that delta sees \"media \u2248 wall\" and never sleeps, letting burst\n * relay delivery saturate the decoder / ring buffer.\n *\n * The Sony libatsc3 pattern (atsc3_mmt_context_mfu_depacketizer) solves this\n * by anchoring wall-clock time to the first received AU and counting AUs:\n * media_elapsed_ms = au_count \u00D7 nominal_au_duration_ms\n * The MPU timestamp is used elsewhere as an absolute-time anchor, never as\n * an incremental clock.\n *\n * The sleep threshold comes from the catalog's FEC interleaveDepthMs (via\n * FecBlockMapper.pacingThresholdMs). When no FEC is configured, a\n * NullBlockMapper yields threshold=0 and this pacer never sleeps.\n *\n * One pacer instance = one track (e.g. audio, video) on one player.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\nexport interface FrameCountPacerOpts {\n /** Required: AU duration (audio) or picture interval (video), in ms. */\n frameDurationMs: number\n /** FEC mapper from @blockcast/mmt-fec. NullBlockMapper disables pacing. */\n mapper: FecBlockMapper\n /**\n * Number of frames to let through before pacing starts. Lets startup\n * burst drain populate the ring buffer / decoder queue. Default 0.\n */\n warmupFrames?: number\n}\n\nexport interface PacerSnapshot {\n /** Frames observed (from notifyFrame). */\n frameCount: number\n /** Computed media time: frameCount \u00D7 frameDurationMs. */\n mediaMs: number\n /** Wall-clock time since anchor. */\n wallMs: number\n /** mediaMs - wallMs. Positive \u2192 decoder ahead of realtime. */\n aheadMs: number\n /** Pacing threshold (ms) from the FEC mapper. */\n thresholdMs: number\n /** Has the first frame been observed (wall anchor set)? */\n anchored: boolean\n}\n\nexport class FrameCountPacer {\n readonly frameDurationMs: number\n readonly thresholdMs: number\n readonly warmupFrames: number\n\n #frameCount = 0\n #anchorWallMs = 0\n #anchored = false\n\n constructor(opts: FrameCountPacerOpts) {\n if (!(opts.frameDurationMs > 0)) {\n throw new Error('frameDurationMs must be > 0')\n }\n this.frameDurationMs = opts.frameDurationMs\n this.thresholdMs = opts.mapper.pacingThresholdMs\n this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0)\n }\n\n /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */\n notifyFrame(): void {\n if (!this.#anchored) {\n this.#anchorWallMs = now()\n this.#anchored = true\n }\n this.#frameCount++\n }\n\n /**\n * Compute sleep ms before reading the next MoQ object.\n * Returns 0 when:\n * - pacing is disabled (NullBlockMapper \u2192 threshold=0)\n * - the anchor is not yet set\n * - still in warmup\n * - the caller reports a standing backlog (see `observedDelayMs`)\n * - decoder is not ahead of wall clock by more than threshold\n * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).\n *\n * `observedDelayMs` is how far this frame is behind the producer's own clock,\n * measured by the caller against a producer-supplied sending instant (for\n * MMTP, the header send-wall timestamp). It is optional because the anchored\n * rate below is blind to it: `#anchorWallMs` is the wall time of the FIRST\n * frame read, so `aheadMs` measures consumption against the join instant and\n * not against the producer. Once `aheadMs` reaches the threshold the loop\n * consumes at exactly the source rate, and recovering latency present at join\n * requires consuming FASTER than the source rate. So without this argument a\n * consumer that joined a live stream mid-block keeps that join latency for the\n * life of the subscription.\n *\n * A caller that can measure the real distance from the producer passes it and\n * gets a drain: while the backlog exceeds the threshold there is nothing to\n * pace against, so return 0 and let the transport set the rate. At the live\n * edge the anchored-rate behavior below applies unchanged.\n *\n * Callers with no producer clock omit it and keep the pure ATSC3-style\n * anchored pacing (see the class doc), where a first-frame anchor is correct\n * because a broadcast push has no live edge to catch up to.\n */\n computeSleepMs(observedDelayMs?: number): number {\n if (this.thresholdMs <= 0) return 0\n if (!this.#anchored) return 0\n if (this.#frameCount <= this.warmupFrames) return 0\n // Only a finite measurement can license the drain. NaN/Infinity mean the\n // caller could not measure the distance, which is not the same claim as\n // \"the consumer is at the edge\", so fall through to anchored pacing.\n if (observedDelayMs !== undefined && Number.isFinite(observedDelayMs) && observedDelayMs > this.thresholdMs) {\n return 0\n }\n\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = now() - this.#anchorWallMs\n const aheadMs = mediaMs - wallMs\n if (aheadMs <= this.thresholdMs) return 0\n return aheadMs - this.thresholdMs / 2\n }\n\n /** Reset anchor + counter. Call on source switch (MoQ \u2194 multicast). */\n reset(): void {\n this.#frameCount = 0\n this.#anchorWallMs = 0\n this.#anchored = false\n }\n\n get snapshot(): PacerSnapshot {\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0\n return {\n frameCount: this.#frameCount,\n mediaMs,\n wallMs,\n aheadMs: mediaMs - wallMs,\n thresholdMs: this.thresholdMs,\n anchored: this.#anchored,\n }\n }\n}\n\nfunction now(): number {\n // performance.now() is available in main thread, workers, and AudioWorklet.\n // Fall back to Date.now() for exotic runtimes (shouldn't happen in browsers).\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return performance.now()\n }\n return Date.now()\n}\n", "/**\n * KeyframeGate \u2014 drops delta frames until a keyframe arrives.\n *\n * Required when subscribing mid-stream: the relay may deliver P/B frames\n * from the middle of a GOP before the first IDR. Feeding those into\n * VideoDecoder before an init yields \"key frame required\" errors.\n *\n * Call reset() to re-arm the gate after a decoder error (requestKeyframe).\n */\n\nexport class KeyframeGate {\n #open = false\n\n get isOpen(): boolean {\n return this.#open\n }\n\n /**\n * Decide whether to pass a frame through.\n * Opens permanently once the first keyframe is seen.\n */\n allow(keyframe: boolean): boolean {\n if (this.#open) return true\n if (keyframe) {\n this.#open = true\n return true\n }\n return false\n }\n\n /** Re-close the gate. Next keyframe reopens it. */\n reset(): void {\n this.#open = false\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }