@blockcast/mmt-render 0.1.0-main.5dd8358ae902

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 (76) hide show
  1. package/README.md +207 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/audio/audio-buffer-plan.d.ts +27 -0
  4. package/dist/audio/audio-buffer-plan.d.ts.map +1 -0
  5. package/dist/audio/audio-render-pipeline.d.ts +83 -0
  6. package/dist/audio/audio-render-pipeline.d.ts.map +1 -0
  7. package/dist/audio/audio-render-pipeline.js +372 -0
  8. package/dist/audio/audio-render-pipeline.js.map +7 -0
  9. package/dist/audio/audio-ring-buffer.d.ts +55 -0
  10. package/dist/audio/audio-ring-buffer.d.ts.map +1 -0
  11. package/dist/audio/audio-ring-buffer.js +255 -0
  12. package/dist/audio/audio-ring-buffer.js.map +7 -0
  13. package/dist/audio/index.d.ts +9 -0
  14. package/dist/audio/index.d.ts.map +1 -0
  15. package/dist/audio/index.js +640 -0
  16. package/dist/audio/index.js.map +7 -0
  17. package/dist/audio/render-messages.d.ts +94 -0
  18. package/dist/audio/render-messages.d.ts.map +1 -0
  19. package/dist/audio/render-messages.js +1 -0
  20. package/dist/audio/render-messages.js.map +7 -0
  21. package/dist/audio/render-worklet.d.ts +13 -0
  22. package/dist/audio/render-worklet.d.ts.map +1 -0
  23. package/dist/audio/worklet-url.d.ts +25 -0
  24. package/dist/audio/worklet-url.d.ts.map +1 -0
  25. package/dist/audio/worklet-url.js +11 -0
  26. package/dist/audio/worklet-url.js.map +7 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +5 -0
  30. package/dist/index.js.map +7 -0
  31. package/dist/pacing/au-duration.d.ts +18 -0
  32. package/dist/pacing/au-duration.d.ts.map +1 -0
  33. package/dist/pacing/au-duration.js +23 -0
  34. package/dist/pacing/au-duration.js.map +7 -0
  35. package/dist/pacing/frame-pacer.d.ts +71 -0
  36. package/dist/pacing/frame-pacer.d.ts.map +1 -0
  37. package/dist/pacing/frame-pacer.js +89 -0
  38. package/dist/pacing/frame-pacer.js.map +7 -0
  39. package/dist/pacing/index.d.ts +6 -0
  40. package/dist/pacing/index.d.ts.map +1 -0
  41. package/dist/pacing/index.js +166 -0
  42. package/dist/pacing/index.js.map +7 -0
  43. package/dist/pacing/interleave-timing.d.ts +64 -0
  44. package/dist/pacing/interleave-timing.d.ts.map +1 -0
  45. package/dist/pacing/interleave-timing.js +58 -0
  46. package/dist/pacing/interleave-timing.js.map +7 -0
  47. package/dist/video/index.d.ts +4 -0
  48. package/dist/video/index.d.ts.map +1 -0
  49. package/dist/video/index.js +247 -0
  50. package/dist/video/index.js.map +7 -0
  51. package/dist/video/keyframe-gate.d.ts +21 -0
  52. package/dist/video/keyframe-gate.d.ts.map +1 -0
  53. package/dist/video/keyframe-gate.js +39 -0
  54. package/dist/video/keyframe-gate.js.map +7 -0
  55. package/dist/video/video-render-pipeline.d.ts +60 -0
  56. package/dist/video/video-render-pipeline.d.ts.map +1 -0
  57. package/dist/video/video-render-pipeline.js +247 -0
  58. package/dist/video/video-render-pipeline.js.map +7 -0
  59. package/dist/worklet/render-worklet.js +403 -0
  60. package/dist/worklet/render-worklet.js.map +7 -0
  61. package/package.json +101 -0
  62. package/src/audio/audio-buffer-plan.ts +62 -0
  63. package/src/audio/audio-render-pipeline.ts +313 -0
  64. package/src/audio/audio-ring-buffer.ts +281 -0
  65. package/src/audio/index.ts +14 -0
  66. package/src/audio/render-messages.ts +110 -0
  67. package/src/audio/render-worklet.ts +161 -0
  68. package/src/audio/worklet-url.ts +30 -0
  69. package/src/index.ts +13 -0
  70. package/src/pacing/au-duration.ts +34 -0
  71. package/src/pacing/frame-pacer.ts +129 -0
  72. package/src/pacing/index.ts +5 -0
  73. package/src/pacing/interleave-timing.ts +95 -0
  74. package/src/video/index.ts +7 -0
  75. package/src/video/keyframe-gate.ts +35 -0
  76. package/src/video/video-render-pipeline.ts +179 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/audio/audio-ring-buffer.ts", "../../src/audio/audio-buffer-plan.ts", "../../src/audio/audio-render-pipeline.ts", "../../src/pacing/frame-pacer.ts", "../../src/pacing/au-duration.ts", "../../src/audio/worklet-url.ts"],
4
+ "sourcesContent": ["/**\n * Timestamp-anchored FIFO shared by MMT AudioWorklet consumers.\n *\n * PCM remains in decoder emission order. Media timestamps are exact anchors\n * for playout reporting only; they never reposition samples or trigger a\n * guessed discontinuity. The FIFO starts after its full catalog-derived\n * latency is buffered and performs no sample skip/dup clock recovery.\n */\n\nexport interface AudioRingBufferConfig {\n channels: number\n rate: number\n latencyMs: number\n /** Exact PCM startup threshold. Overrides latencyMs when provided. */\n startupFrames?: number\n /** PCM refill threshold after the first underrun. Defaults to startupFrames. */\n resumeFrames?: number\n /** Exact PCM storage ceiling. Must cover the startup and resume thresholds. */\n capacityFrames?: number\n startUnstalled?: boolean\n /** Deprecated compatibility input. Clock recovery is not performed. */\n warmupReads?: number\n diag?: boolean\n}\n\nexport interface AudioRingBufferDiag {\n reads: number\n avgFill: number\n capacity: number\n skips: number\n dups: number\n overflowSamples: number\n}\n\ntype TimestampAnchor = {\n sampleIndex: number\n timestampUs: number\n}\n\nexport class AudioRingBuffer {\n readonly channels: number\n readonly rate: number\n\n #buffer: Float32Array[]\n #writeIndex = 0\n #readIndex = 0\n #stalled: boolean\n #startupFrames: number\n #resumeFrames: number\n #resumeFollowsStartup: boolean\n #hasStarted: boolean\n #capacityHeadroomFrames: number\n #timestampAnchors: TimestampAnchor[] = []\n\n #diagEnabled: boolean\n #diagReads = 0\n #diagFillSum = 0\n #diagOverflowSamples = 0\n\n constructor(config: AudioRingBufferConfig) {\n if (!Number.isInteger(config.channels) || config.channels <= 0) {\n throw new Error('invalid channels')\n }\n if (!Number.isFinite(config.rate) || config.rate <= 0) {\n throw new Error('invalid sample rate')\n }\n if (!Number.isFinite(config.latencyMs) || config.latencyMs <= 0) {\n throw new Error('invalid latency')\n }\n\n const latencyFrames = Math.ceil(config.rate * (config.latencyMs / 1000))\n const startupFrames = config.startupFrames ?? latencyFrames\n const resumeFrames = config.resumeFrames ?? startupFrames\n const thresholdFrames = Math.max(startupFrames, resumeFrames)\n const capacityFrames = config.capacityFrames ?? Math.max(latencyFrames, thresholdFrames)\n if (!Number.isSafeInteger(startupFrames) || startupFrames <= 0) {\n throw new Error('invalid startup frames')\n }\n if (!Number.isSafeInteger(resumeFrames) || resumeFrames <= 0) {\n throw new Error('invalid resume frames')\n }\n if (!Number.isSafeInteger(capacityFrames) || capacityFrames < thresholdFrames) {\n throw new Error('invalid capacity frames')\n }\n\n this.channels = config.channels\n this.rate = config.rate\n this.#startupFrames = startupFrames\n this.#resumeFrames = resumeFrames\n this.#resumeFollowsStartup = config.resumeFrames === undefined\n this.#hasStarted = config.startUnstalled ?? false\n this.#capacityHeadroomFrames = capacityFrames - thresholdFrames\n this.#stalled = !(config.startUnstalled ?? false)\n this.#diagEnabled = config.diag ?? false\n this.#buffer = Array.from(\n { length: config.channels },\n () => new Float32Array(capacityFrames),\n )\n }\n\n get stalled(): boolean { return this.#stalled }\n get startupFrames(): number { return this.#startupFrames }\n get capacity(): number { return this.#buffer[0].length }\n get length(): number { return this.#writeIndex - this.#readIndex }\n\n /** Timestamp of the next media sample after the PCM already emitted. */\n get timestampUs(): number | undefined {\n const anchor = this.#timestampAnchors[0]\n if (!anchor || anchor.sampleIndex > this.#readIndex) return undefined\n return anchor.timestampUs + ((this.#readIndex - anchor.sampleIndex) / this.rate) * 1_000_000\n }\n\n write(timestampUs: number, data: Float32Array[]): void {\n if (data.length !== this.channels) throw new Error('wrong channel count')\n if (!Number.isFinite(timestampUs)) throw new Error('invalid timestamp')\n const samples = data[0].length\n if (samples === 0) return\n if (data.some((channel) => channel.length !== samples)) {\n throw new Error('mismatched channel lengths')\n }\n\n const start = this.#writeIndex\n const end = start + samples\n this.#timestampAnchors.push({ sampleIndex: start, timestampUs })\n\n const overflow = end - this.#readIndex - this.capacity\n if (overflow > 0) {\n this.#stalled = false\n this.#hasStarted = true\n this.#readIndex += overflow\n if (this.#diagEnabled) this.#diagOverflowSamples += overflow\n this.#pruneTimestampAnchors()\n }\n\n const refillFrames = this.#hasStarted ? this.#resumeFrames : this.#startupFrames\n if (this.#stalled && end - this.#readIndex >= refillFrames) {\n this.#stalled = false\n this.#hasStarted = true\n }\n\n for (let channel = 0; channel < this.channels; channel++) {\n const src = data[channel]\n const dst = this.#buffer[channel]\n for (let i = 0; i < samples; i++) {\n dst[(start + i) % dst.length] = src[i]\n }\n }\n this.#writeIndex = end\n }\n\n read(output: Float32Array[]): number {\n if (output.length !== this.channels) throw new Error('wrong channel count')\n if (this.#stalled) return 0\n\n const available = this.length\n if (available === 0) {\n this.#stalled = true\n return 0\n }\n\n const requested = output[0].length\n const readSamples = Math.min(available, requested)\n\n for (let channel = 0; channel < this.channels; channel++) {\n const dst = output[channel]\n const src = this.#buffer[channel]\n for (let i = 0; i < readSamples; i++) {\n dst[i] = src[(this.#readIndex + i) % src.length]\n }\n for (let i = readSamples; i < requested; i++) dst[i] = 0\n }\n\n this.#readIndex += readSamples\n this.#pruneTimestampAnchors()\n if (readSamples < requested) this.#stalled = true\n\n if (this.#diagEnabled) {\n this.#diagReads++\n this.#diagFillSum += available\n }\n return readSamples\n }\n\n /** Clear queued PCM; optionally treat the next fill as post-start recovery. */\n reset(startUnstalled = false, resumeAfterReset = false): void {\n this.#writeIndex = 0\n this.#readIndex = 0\n this.#stalled = !startUnstalled\n this.#hasStarted = startUnstalled || resumeAfterReset\n this.#timestampAnchors = []\n this.#diagReads = 0\n this.#diagFillSum = 0\n this.#diagOverflowSamples = 0\n }\n\n /**\n * Change the startup latency while preserving the configured storage\n * headroom above its buffering thresholds. Active playout keeps draining\n * retained PCM; a stalled buffer waits for its startup or resume threshold.\n */\n resize(latencyMs: number): void {\n if (!Number.isFinite(latencyMs) || latencyMs <= 0) {\n throw new Error('invalid latency')\n }\n const newStartupFrames = Math.ceil(this.rate * (latencyMs / 1000))\n if (!Number.isSafeInteger(newStartupFrames) || newStartupFrames <= 0) {\n throw new Error('invalid latency')\n }\n const newResumeFrames = this.#resumeFollowsStartup ? newStartupFrames : this.#resumeFrames\n const newCapacity =\n Math.max(newStartupFrames, newResumeFrames) + this.#capacityHeadroomFrames\n if (\n !Number.isSafeInteger(newCapacity) ||\n newCapacity < Math.max(newStartupFrames, newResumeFrames)\n ) {\n throw new Error('invalid capacity frames')\n }\n const wasStalled = this.#stalled\n this.#startupFrames = newStartupFrames\n this.#resumeFrames = newResumeFrames\n if (newCapacity === this.capacity) {\n if (wasStalled && this.length >= this.#refillFrames()) {\n this.#stalled = false\n this.#hasStarted = true\n }\n return\n }\n\n const next = Array.from(\n { length: this.channels },\n () => new Float32Array(newCapacity),\n )\n const samplesToKeep = Math.min(this.length, newCapacity)\n const copyStart = this.#writeIndex - samplesToKeep\n for (let channel = 0; channel < this.channels; channel++) {\n const src = this.#buffer[channel]\n const dst = next[channel]\n for (let i = 0; i < samplesToKeep; i++) {\n const sampleIndex = copyStart + i\n dst[sampleIndex % dst.length] = src[sampleIndex % src.length]\n }\n }\n\n this.#buffer = next\n this.#readIndex = copyStart\n this.#pruneTimestampAnchors()\n this.#stalled = samplesToKeep === 0 || (wasStalled && samplesToKeep < this.#refillFrames())\n if (!this.#stalled) this.#hasStarted = true\n }\n\n getDiagAndReset(): AudioRingBufferDiag | null {\n if (!this.#diagEnabled || this.#diagReads === 0) return null\n const result: AudioRingBufferDiag = {\n reads: this.#diagReads,\n avgFill: Math.round(this.#diagFillSum / this.#diagReads),\n capacity: this.capacity,\n skips: 0,\n dups: 0,\n overflowSamples: this.#diagOverflowSamples,\n }\n this.#diagReads = 0\n this.#diagFillSum = 0\n this.#diagOverflowSamples = 0\n return result\n }\n\n #pruneTimestampAnchors(): void {\n let keep = 0\n while (\n keep + 1 < this.#timestampAnchors.length &&\n this.#timestampAnchors[keep + 1].sampleIndex <= this.#readIndex\n ) {\n keep++\n }\n if (keep > 0) this.#timestampAnchors.splice(0, keep)\n }\n\n #refillFrames(): number {\n return this.#hasStarted ? this.#resumeFrames : this.#startupFrames\n }\n}\n", "export interface AudioBurstBufferPlanInput {\n sampleRate: number\n chunkFrames: number\n /** Catalog media-group duration. Integer milliseconds may be rounded. */\n groupDurationMs: number\n /** Optional catalog-derived datagram/FEC delivery span. */\n deliveryWindowMs?: number\n}\n\nexport interface AudioBurstBufferPlan {\n /** PCM required before playout starts or resumes. */\n startupFrames: number\n /** Maximum retained PCM, including the next complete delivery burst. */\n capacityFrames: number\n /** Maximum PCM frames delivered as one catalog-defined burst. */\n deliveryFrames: number\n}\n\nfunction positive(value: number, label: string): number {\n if (!Number.isFinite(value) || value <= 0) throw new Error(`invalid ${label}`)\n return value\n}\n\n/**\n * Convert catalog delivery timing into exact codec-frame FIFO geometry.\n *\n * `groupDurationMs` is commonly rounded to an integer, so media-group units\n * use nearest-unit rounding. FEC delivery windows are upper bounds and use\n * ceiling. Storage holds the startup burst plus one complete successor burst;\n * this prevents an atomic MoQ group from overwriting PCM while the worklet is\n * draining the preceding group without adding a guessed time margin.\n */\nexport function deriveAudioBurstBufferPlan(\n input: AudioBurstBufferPlanInput,\n): AudioBurstBufferPlan {\n const sampleRate = positive(input.sampleRate, 'sample rate')\n const chunkFrames = positive(input.chunkFrames, 'audio chunk size')\n const groupDurationMs = positive(input.groupDurationMs, 'audio group duration')\n if (!Number.isInteger(chunkFrames)) throw new Error('invalid audio chunk size')\n\n const chunkDurationMs = (1000 * chunkFrames) / sampleRate\n const groupChunks = Math.max(1, Math.round(groupDurationMs / chunkDurationMs))\n const deliveryWindowChunks =\n input.deliveryWindowMs === undefined\n ? 0\n : Math.max(\n 1,\n Math.ceil(positive(input.deliveryWindowMs, 'audio delivery window') / chunkDurationMs),\n )\n const deliveryChunks = Math.max(groupChunks, deliveryWindowChunks)\n const deliveryFrames = deliveryChunks * chunkFrames\n const capacityFrames = deliveryFrames + deliveryFrames\n if (!Number.isSafeInteger(deliveryFrames) || !Number.isSafeInteger(capacityFrames)) {\n throw new Error('audio buffer frame count exceeds safe integer range')\n }\n\n return {\n startupFrames: deliveryFrames,\n capacityFrames,\n deliveryFrames,\n }\n}\n", "/**\n * AudioRenderPipeline \u2014 end-to-end audio render for MoQ/MMT players.\n *\n * Pipeline: MoQ object \u2192 MmtpDecodePipeline \u2192 WebCodecs AudioDecoder\n * \u2192 AudioWorkletNode (exact-timestamp FIFO)\n *\n * The consumer:\n * 1. constructs with codec/sampleRate/channels/mapper from catalog\n * 2. awaits ready()\n * 3. calls feedMoqObject(packet) for each arriving MoQ object\n * 4. between objects, awaits paceSleepMs() to throttle burst delivery\n * 5. connects rootNode to an AudioContext destination (or a GainNode)\n * 6. close() on teardown\n *\n * Pacing uses FrameCountPacer (anchor on first decoded AU, frame count \u00D7 AU\n * duration) with threshold = mapper.pacingThresholdMs \u2014 NOT MPU timestamps\n * (ambiguous under FEC interleave).\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 { auDurationMsFromCodec } from '../pacing/au-duration.js'\nimport type {\n ToMain,\n DataMessage,\n InitMessage,\n ResetMessage,\n StartMessage,\n} from './render-messages.js'\nimport { WORKLET_PROCESSOR_NAME, workletModuleUrl } from './worklet-url.js'\n\nexport interface AudioRenderPipelineOpts {\n context: AudioContext\n codec: string\n /** MMTP packet_id for this audio track \u2014 from catalog. */\n packetId?: number\n sampleRate: number\n channels: number\n description?: Uint8Array\n mapper: FecBlockMapper\n fecClient?: PipelineFecClient\n /** Reliable MoQ is clock-free; datagram deadlines come from catalog/in-band timing. */\n reassemblyPolicy: MfuReassemblyPolicy\n /** Catalog-derived PCM startup buffer in milliseconds. */\n latencyMs: number\n /** Exact PCM startup threshold when catalog grouping is codec-aligned. */\n startupFrames?: number\n /** Exact PCM storage ceiling, including one successor delivery burst. */\n capacityFrames?: number\n /** Enable ring-buffer telemetry messages. Default false. */\n diag?: boolean\n /** Called when worklet reports state (playback timestamp + exact buffer accounting). Optional. */\n onState?: (state: {\n timestampUs: number | undefined\n stalled: boolean\n bufferedFrames: number\n writtenFrames: number\n }) => void\n /** Called when worklet reports ring-buffer diag (telemetry). Optional. */\n onDiag?: (diag: {\n reads: number; avgFill: number; capacity: number\n skipsPerSec: number; dupsPerSec: number; overflowSamplesPerSec: number\n underflowSamplesPerSec: number\n }) => void\n /** Warmup frames before pacing engages. Default 0 (audio tolerates instant pacing). */\n warmupFrames?: number\n /** Audio underflow / config event logger. Optional. */\n onError?: (err: unknown) => void\n}\n\nexport class AudioRenderPipeline {\n readonly #context: AudioContext\n readonly #pacer: FrameCountPacer\n readonly #mapper: FecBlockMapper\n readonly #codec: string\n readonly #packetId: number | undefined\n readonly #sampleRate: number\n readonly #channels: number\n readonly #description?: Uint8Array\n readonly #latencyMs: number\n readonly #startupFrames?: number\n readonly #startupThresholdFrames: number\n readonly #capacityFrames?: number\n readonly #diag: boolean\n readonly #warmupFrames: number\n readonly #onState?: AudioRenderPipelineOpts['onState']\n readonly #onDiag?: AudioRenderPipelineOpts['onDiag']\n readonly #onError?: AudioRenderPipelineOpts['onError']\n readonly #reassemblyPolicy: MfuReassemblyPolicy\n\n #decoder: AudioDecoder | null = null\n #pipeline: MmtpDecodePipeline | null = null\n #worklet: AudioWorkletNode | null = null\n #fecClient?: PipelineFecClient\n #closed = false\n #readyPromise: Promise<void> | null = null\n #workletGeneration = 0\n #startupReleased = false\n\n constructor(opts: AudioRenderPipelineOpts) {\n this.#context = opts.context\n this.#mapper = opts.mapper\n this.#codec = opts.codec\n this.#packetId = opts.packetId\n this.#sampleRate = opts.sampleRate\n this.#channels = opts.channels\n this.#description = opts.description\n this.#latencyMs = opts.latencyMs\n this.#startupFrames = opts.startupFrames\n this.#startupThresholdFrames = opts.startupFrames\n ?? Math.ceil(opts.sampleRate * (opts.latencyMs / 1000))\n this.#capacityFrames = opts.capacityFrames\n this.#diag = opts.diag ?? false\n this.#warmupFrames = opts.warmupFrames ?? 0\n this.#onState = opts.onState\n this.#onDiag = opts.onDiag\n this.#onError = opts.onError\n this.#fecClient = opts.fecClient\n this.#reassemblyPolicy = opts.reassemblyPolicy\n\n // AU duration from codec + sampleRate. Keep inside the lib so consumers\n // don't re-derive it.\n const auDurationMs = auDurationMsFromCodec(opts.codec, opts.sampleRate)\n this.#pacer = new FrameCountPacer({\n frameDurationMs: auDurationMs,\n mapper: opts.mapper,\n warmupFrames: this.#warmupFrames,\n })\n }\n\n /** Load worklet module + create decoder + pipeline. Call once. */\n async ready(): Promise<void> {\n if (this.#readyPromise) return this.#readyPromise\n this.#readyPromise = this.#initialize()\n return this.#readyPromise\n }\n\n async #initialize(): Promise<void> {\n // 1. Load shared worklet module.\n await this.#context.audioWorklet.addModule(workletModuleUrl())\n\n // 2. Create worklet node.\n const worklet = new AudioWorkletNode(this.#context, WORKLET_PROCESSOR_NAME, {\n numberOfInputs: 0,\n numberOfOutputs: 1,\n outputChannelCount: [this.#channels],\n })\n const initMsg: InitMessage = {\n type: 'init',\n generation: ++this.#workletGeneration,\n channels: this.#channels,\n rate: this.#sampleRate,\n latencyMs: this.#latencyMs,\n startupFrames: this.#startupFrames,\n capacityFrames: this.#capacityFrames,\n holdUntilStart: true,\n diag: this.#diag,\n }\n worklet.port.postMessage(initMsg)\n worklet.port.onmessage = (ev: MessageEvent<ToMain>) => {\n const m = ev.data\n if (m.type === 'state') {\n if (m.generation !== this.#workletGeneration) return\n this.#onState?.({\n timestampUs: m.timestamp,\n stalled: m.stalled,\n bufferedFrames: m.bufferedFrames,\n writtenFrames: m.writtenFrames,\n })\n } else if (m.type === 'ringBufDiag') {\n if (m.generation !== this.#workletGeneration) return\n this.#onDiag?.({\n reads: m.reads, avgFill: m.avgFill, capacity: m.capacity,\n skipsPerSec: m.skipsPerSec, dupsPerSec: m.dupsPerSec,\n overflowSamplesPerSec: m.overflowSamplesPerSec,\n underflowSamplesPerSec: m.underflowSamplesPerSec,\n })\n } else if (m.type === 'writeAck') {\n if (\n m.generation !== this.#workletGeneration ||\n this.#startupReleased ||\n m.bufferedFrames < this.#startupThresholdFrames\n ) return\n this.#startupReleased = true\n const start: StartMessage = {\n type: 'start',\n generation: this.#workletGeneration,\n }\n worklet.port.postMessage(start)\n }\n }\n this.#worklet = worklet\n\n // 3. Create decoder.\n const decoder = new AudioDecoder({\n output: (data) => this.#onDecoded(data),\n error: (err) => this.#onError?.(err),\n })\n const config: AudioDecoderConfig = {\n codec: this.#codec,\n sampleRate: this.#sampleRate,\n numberOfChannels: this.#channels,\n ...(this.#description ? { description: this.#description } : {}),\n }\n decoder.configure(config)\n this.#decoder = decoder\n\n // 4. Create decode pipeline.\n const pipeline = new MmtpDecodePipeline()\n pipeline.configure({\n codec: this.#codec,\n packetId: this.#packetId,\n description: this.#description,\n reorderWindow: 0, // audio doesn't need reorder \u2014 AAC/Opus frames are self-contained\n reassemblyPolicy: this.#reassemblyPolicy,\n })\n if (this.#fecClient) pipeline.setFecClient(this.#fecClient)\n pipeline.onFrame((codecData, meta) => {\n if (!this.#decoder || this.#decoder.state !== 'configured') return\n if (codecData.byteLength === 0) return\n this.#pacer.notifyFrame()\n try {\n this.#decoder.decode(new EncodedAudioChunk({\n type: 'key',\n data: codecData,\n timestamp: meta.timestamp,\n }))\n } catch (err) {\n this.#onError?.(err)\n }\n })\n this.#pipeline = pipeline\n }\n\n /** Feed one MoQ object (raw MMTP packet). */\n feedMoqObject(packet: Uint8Array): void {\n this.#pipeline?.feedMoqObject(packet)\n }\n\n /** ms to sleep before the next object read. 0 when not anchored / not ahead. */\n paceSleepMs(): number {\n return this.#pacer.computeSleepMs()\n }\n\n /** Pacer snapshot (for diagnostics / UI). */\n get pacerSnapshot() {\n return this.#pacer.snapshot\n }\n\n /** Reset pacer on source switch (MoQ \u2194 multicast). */\n resetPacer(): void {\n this.#pacer.reset()\n }\n\n /** Drop queued PCM and its timestamp epoch at an explicit source boundary. */\n resetPlayout(startUnstalled = false): void {\n this.#pacer.reset()\n // The explicit gate owns only the initial prefill. Source-boundary resets\n // use the worklet's one-frame resume threshold.\n this.#startupReleased = true\n const message: ResetMessage = {\n type: 'reset',\n generation: ++this.#workletGeneration,\n startUnstalled,\n }\n this.#worklet?.port.postMessage(message)\n }\n\n /** Node to connect to destination / gain. */\n get rootNode(): AudioNode {\n if (!this.#worklet) throw new Error('AudioRenderPipeline.ready() must resolve first')\n return this.#worklet\n }\n\n close(): void {\n if (this.#closed) return\n this.#closed = true\n try { this.#decoder?.close() } catch { /* already closed */ }\n try { this.#pipeline?.dispose() } catch { /* noop */ }\n try { this.#worklet?.disconnect() } catch { /* noop */ }\n this.#decoder = null\n this.#pipeline = null\n this.#worklet = 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 #onDecoded(sample: AudioData): void {\n if (this.#closed || !this.#worklet) { sample.close(); return }\n\n const timestamp = sample.timestamp\n const channelData: Float32Array[] = []\n for (let ch = 0; ch < sample.numberOfChannels; ch++) {\n const data = new Float32Array(sample.numberOfFrames)\n sample.copyTo(data, { format: 'f32-planar', planeIndex: ch })\n channelData.push(data)\n }\n sample.close()\n\n const msg: DataMessage = {\n type: 'data',\n data: channelData,\n timestamp,\n }\n this.#worklet.port.postMessage(msg, channelData.map((d) => d.buffer))\n }\n}\n\n// auDurationMsFromCodec lives in ../pacing/au-duration.ts (canonical, no\n// mmt-container dep). Import it from '@blockcast/mmt-render/pacing' directly\n// or via the top-level '@blockcast/mmt-render' export.\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 * Nominal Access Unit duration helpers for common audio + video codecs.\n *\n * Kept in the pacing subpath (no mmt-container dep) so lean consumers like\n * moqtail's player.ts can import the pacer + AU duration without pulling\n * in the full audio-render-pipeline (which depends on @blockcast/mmt-container).\n */\n\n/**\n * Nominal AU duration in ms for common audio codecs at the given sample rate.\n * AAC-LC: 1024 samples. Opus (default 20ms): 960 samples @ 48kHz.\n * Throws for unknown codecs so callers supply correct timing.\n */\nexport function auDurationMsFromCodec(codec: string, sampleRate: number): number {\n if (!(sampleRate > 0)) throw new Error('sampleRate must be > 0')\n if (codec.startsWith('opus') || codec.startsWith('Opus')) {\n return (960 / sampleRate) * 1000\n }\n if (codec.startsWith('mp4a') || codec.startsWith('AAC') || codec.startsWith('aac')) {\n return (1024 / sampleRate) * 1000\n }\n if (codec.startsWith('flac') || codec.startsWith('fLaC')) {\n return (4096 / sampleRate) * 1000\n }\n throw new Error(`unknown audio codec for AU duration derivation: ${codec}`)\n}\n\n/**\n * Nominal picture duration in ms from framerate.\n */\nexport function pictureDurationMs(framerate: number): number {\n if (!(framerate > 0)) throw new Error('framerate must be > 0')\n return 1000 / framerate\n}\n", "/**\n * Helper for loading the prebuilt AudioWorklet bundle.\n *\n * Two equivalent loading patterns supported:\n *\n * 1. Vite-style static URL (recommended for bundled apps):\n * import workletUrl from '@blockcast/mmt-render/audio/worklet.js?url'\n * await ctx.audioWorklet.addModule(workletUrl)\n * Vite resolves the `?url` suffix + the package's \"./audio/worklet.js\"\n * export to the prebuilt IIFE file.\n *\n * 2. Runtime import.meta.url resolution (no bundler):\n * import { workletModuleUrl } from '@blockcast/mmt-render/audio/worklet-url'\n * await ctx.audioWorklet.addModule(workletModuleUrl())\n *\n * Both resolve to dist/worklet/render-worklet.js.\n */\n\n/**\n * Returns a URL (or URL string) pointing to the prebuilt worklet bundle.\n * Resolves relative to this module's location via import.meta.url.\n */\nexport function workletModuleUrl(): string {\n // dist layout: dist/audio/worklet-url.js \u2192 dist/worklet/render-worklet.js\n const base = new URL('../worklet/render-worklet.js', import.meta.url)\n return base.href\n}\n\n/** Processor name registered in the worklet bundle. */\nexport const WORKLET_PROCESSOR_NAME = 'mmt-render'\n"],
5
+ "mappings": "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",
6
+ "names": ["_startupFrames"]
7
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Typed contract for AudioWorklet ↔ main-thread messages.
3
+ * Used by both the worklet processor (render-worklet.ts) and by consumers
4
+ * through audio-render-pipeline.ts.
5
+ */
6
+ /** Main → worklet. */
7
+ export type FromMain = InitMessage | DataMessage | StartMessage | LatencyMessage | ResetMessage | DiagRequestMessage;
8
+ /** Worklet → main. */
9
+ export type ToMain = WriteAckMessage | StateMessage | DiagMessage;
10
+ export interface InitMessage {
11
+ type: 'init';
12
+ /** Main-thread playout generation. State from older generations is stale. */
13
+ generation: number;
14
+ /** Channel count. */
15
+ channels: number;
16
+ /** Sample rate in Hz. */
17
+ rate: number;
18
+ /** Buffer latency in ms. */
19
+ latencyMs: number;
20
+ /** Exact PCM startup threshold derived from catalog delivery units. */
21
+ startupFrames?: number;
22
+ /** Exact PCM storage ceiling, including one successor delivery burst. */
23
+ capacityFrames?: number;
24
+ /** Start in unstalled state (source-switch reconfig). Default false. */
25
+ startUnstalled?: boolean;
26
+ /** Buffer and acknowledge PCM without rendering until a matching start message. */
27
+ holdUntilStart?: boolean;
28
+ /** Deprecated compatibility input. Clock recovery is not performed. */
29
+ warmupReads?: number;
30
+ /** Enable telemetry reporting. Default false. */
31
+ diag?: boolean;
32
+ }
33
+ export interface DataMessage {
34
+ type: 'data';
35
+ /** Per-channel PCM (Float32) for the next chunk. */
36
+ data: Float32Array[];
37
+ /** Presentation timestamp of the first sample in µs. */
38
+ timestamp: number;
39
+ }
40
+ export interface WriteAckMessage {
41
+ type: 'writeAck';
42
+ /** Playout generation that accepted the PCM. */
43
+ generation: number;
44
+ /** Exact unread PCM sample count after accepting the write. */
45
+ bufferedFrames: number;
46
+ /** Cumulative PCM frames accepted since the last init/reset. */
47
+ writtenFrames: number;
48
+ }
49
+ export interface StartMessage {
50
+ type: 'start';
51
+ /** Playout generation to release. Stale start messages are ignored. */
52
+ generation: number;
53
+ }
54
+ export interface LatencyMessage {
55
+ type: 'latency';
56
+ /** New buffer latency in ms. */
57
+ latencyMs: number;
58
+ }
59
+ export interface ResetMessage {
60
+ type: 'reset';
61
+ /** Main-thread playout generation after this reset. */
62
+ generation: number;
63
+ /** Start immediately instead of waiting for a full configured buffer. */
64
+ startUnstalled?: boolean;
65
+ }
66
+ export interface DiagRequestMessage {
67
+ type: 'requestDiag';
68
+ }
69
+ export interface StateMessage {
70
+ type: 'state';
71
+ /** Playout generation that produced this state snapshot. */
72
+ generation: number;
73
+ /** Timestamp of the next MMTP media sample after emitted PCM, in µs. */
74
+ timestamp?: number;
75
+ /** Is the ring buffer waiting to fill? */
76
+ stalled: boolean;
77
+ /** Exact unread PCM sample count. */
78
+ bufferedFrames: number;
79
+ /** Cumulative PCM frames accepted since the last init/reset. */
80
+ writtenFrames: number;
81
+ }
82
+ export interface DiagMessage {
83
+ type: 'ringBufDiag';
84
+ /** Playout generation that produced this diagnostic snapshot. */
85
+ generation: number;
86
+ reads: number;
87
+ avgFill: number;
88
+ capacity: number;
89
+ skipsPerSec: number;
90
+ dupsPerSec: number;
91
+ overflowSamplesPerSec: number;
92
+ underflowSamplesPerSec: number;
93
+ }
94
+ //# sourceMappingURL=render-messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-messages.d.ts","sourceRoot":"","sources":["../../src/audio/render-messages.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,sBAAsB;AACtB,MAAM,MAAM,QAAQ,GAChB,WAAW,GACX,WAAW,GACX,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,kBAAkB,CAAA;AAEtB,sBAAsB;AACtB,MAAM,MAAM,MAAM,GAAG,eAAe,GAAG,YAAY,GAAG,WAAW,CAAA;AAEjE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAA;IAClB,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wEAAwE;IACxE,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,mFAAmF;IACnF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iDAAiD;IACjD,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,IAAI,EAAE,YAAY,EAAE,CAAA;IACpB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,UAAU,CAAA;IAChB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAA;IACtB,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAA;IACf,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAA;IAClB,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,aAAa,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAA;IAClB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,aAAa,CAAA;IACnB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,sBAAsB,EAAE,MAAM,CAAA;CAC/B"}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=render-messages.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared AudioWorklet render processor.
3
+ *
4
+ * This file is bundled into a self-contained IIFE at build time
5
+ * (dist/worklet/render-worklet.js) because AudioWorkletGlobalScope cannot
6
+ * resolve bare specifiers. The AudioRingBuffer dependency is inlined at
7
+ * bundle time; do not import from @blockcast packages at runtime.
8
+ *
9
+ * Consumers load the prebuilt bundle via audioWorklet.addModule(url) where
10
+ * url is obtained from `@blockcast/mmt-render/audio/worklet-url`.
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=render-worklet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-worklet.d.ts","sourceRoot":"","sources":["../../src/audio/render-worklet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Helper for loading the prebuilt AudioWorklet bundle.
3
+ *
4
+ * Two equivalent loading patterns supported:
5
+ *
6
+ * 1. Vite-style static URL (recommended for bundled apps):
7
+ * import workletUrl from '@blockcast/mmt-render/audio/worklet.js?url'
8
+ * await ctx.audioWorklet.addModule(workletUrl)
9
+ * Vite resolves the `?url` suffix + the package's "./audio/worklet.js"
10
+ * export to the prebuilt IIFE file.
11
+ *
12
+ * 2. Runtime import.meta.url resolution (no bundler):
13
+ * import { workletModuleUrl } from '@blockcast/mmt-render/audio/worklet-url'
14
+ * await ctx.audioWorklet.addModule(workletModuleUrl())
15
+ *
16
+ * Both resolve to dist/worklet/render-worklet.js.
17
+ */
18
+ /**
19
+ * Returns a URL (or URL string) pointing to the prebuilt worklet bundle.
20
+ * Resolves relative to this module's location via import.meta.url.
21
+ */
22
+ export declare function workletModuleUrl(): string;
23
+ /** Processor name registered in the worklet bundle. */
24
+ export declare const WORKLET_PROCESSOR_NAME = "mmt-render";
25
+ //# sourceMappingURL=worklet-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worklet-url.d.ts","sourceRoot":"","sources":["../../src/audio/worklet-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC;AAED,uDAAuD;AACvD,eAAO,MAAM,sBAAsB,eAAe,CAAA"}
@@ -0,0 +1,11 @@
1
+ // src/audio/worklet-url.ts
2
+ function workletModuleUrl() {
3
+ const base = new URL("../worklet/render-worklet.js", import.meta.url);
4
+ return base.href;
5
+ }
6
+ var WORKLET_PROCESSOR_NAME = "mmt-render";
7
+ export {
8
+ WORKLET_PROCESSOR_NAME,
9
+ workletModuleUrl
10
+ };
11
+ //# sourceMappingURL=worklet-url.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/audio/worklet-url.ts"],
4
+ "sourcesContent": ["/**\n * Helper for loading the prebuilt AudioWorklet bundle.\n *\n * Two equivalent loading patterns supported:\n *\n * 1. Vite-style static URL (recommended for bundled apps):\n * import workletUrl from '@blockcast/mmt-render/audio/worklet.js?url'\n * await ctx.audioWorklet.addModule(workletUrl)\n * Vite resolves the `?url` suffix + the package's \"./audio/worklet.js\"\n * export to the prebuilt IIFE file.\n *\n * 2. Runtime import.meta.url resolution (no bundler):\n * import { workletModuleUrl } from '@blockcast/mmt-render/audio/worklet-url'\n * await ctx.audioWorklet.addModule(workletModuleUrl())\n *\n * Both resolve to dist/worklet/render-worklet.js.\n */\n\n/**\n * Returns a URL (or URL string) pointing to the prebuilt worklet bundle.\n * Resolves relative to this module's location via import.meta.url.\n */\nexport function workletModuleUrl(): string {\n // dist layout: dist/audio/worklet-url.js \u2192 dist/worklet/render-worklet.js\n const base = new URL('../worklet/render-worklet.js', import.meta.url)\n return base.href\n}\n\n/** Processor name registered in the worklet bundle. */\nexport const WORKLET_PROCESSOR_NAME = 'mmt-render'\n"],
5
+ "mappings": ";AAsBO,SAAS,mBAA2B;AAEzC,QAAM,OAAO,IAAI,IAAI,gCAAgC,YAAY,GAAG;AACpE,SAAO,KAAK;AACd;AAGO,IAAM,yBAAyB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @blockcast/mmt-render — shared MMT render pipeline for MoQ players.
3
+ *
4
+ * Subpath exports:
5
+ * - ./pacing FrameCountPacer, InterleaveTiming
6
+ * - ./audio AudioRenderPipeline, AudioRingBuffer, worklet helpers
7
+ * - ./video VideoRenderPipeline, KeyframeGate
8
+ * - ./audio/worklet.js Prebuilt self-contained AudioWorklet bundle
9
+ */
10
+ export * from './pacing/index.js';
11
+ export * from './audio/index.js';
12
+ export * from './video/index.js';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // src/index.ts
2
+ export * from "./pacing/index.js";
3
+ export * from "./audio/index.js";
4
+ export * from "./video/index.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["/**\n * @blockcast/mmt-render \u2014 shared MMT render pipeline for MoQ players.\n *\n * Subpath exports:\n * - ./pacing FrameCountPacer, InterleaveTiming\n * - ./audio AudioRenderPipeline, AudioRingBuffer, worklet helpers\n * - ./video VideoRenderPipeline, KeyframeGate\n * - ./audio/worklet.js Prebuilt self-contained AudioWorklet bundle\n */\n\nexport * from './pacing/index.js'\nexport * from './audio/index.js'\nexport * from './video/index.js'\n"],
5
+ "mappings": ";AAUA,cAAc;AACd,cAAc;AACd,cAAc;",
6
+ "names": []
7
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Nominal Access Unit duration helpers for common audio + video codecs.
3
+ *
4
+ * Kept in the pacing subpath (no mmt-container dep) so lean consumers like
5
+ * moqtail's player.ts can import the pacer + AU duration without pulling
6
+ * in the full audio-render-pipeline (which depends on @blockcast/mmt-container).
7
+ */
8
+ /**
9
+ * Nominal AU duration in ms for common audio codecs at the given sample rate.
10
+ * AAC-LC: 1024 samples. Opus (default 20ms): 960 samples @ 48kHz.
11
+ * Throws for unknown codecs so callers supply correct timing.
12
+ */
13
+ export declare function auDurationMsFromCodec(codec: string, sampleRate: number): number;
14
+ /**
15
+ * Nominal picture duration in ms from framerate.
16
+ */
17
+ export declare function pictureDurationMs(framerate: number): number;
18
+ //# sourceMappingURL=au-duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"au-duration.d.ts","sourceRoot":"","sources":["../../src/pacing/au-duration.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAY/E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG3D"}
@@ -0,0 +1,23 @@
1
+ // src/pacing/au-duration.ts
2
+ function auDurationMsFromCodec(codec, sampleRate) {
3
+ if (!(sampleRate > 0)) throw new Error("sampleRate must be > 0");
4
+ if (codec.startsWith("opus") || codec.startsWith("Opus")) {
5
+ return 960 / sampleRate * 1e3;
6
+ }
7
+ if (codec.startsWith("mp4a") || codec.startsWith("AAC") || codec.startsWith("aac")) {
8
+ return 1024 / sampleRate * 1e3;
9
+ }
10
+ if (codec.startsWith("flac") || codec.startsWith("fLaC")) {
11
+ return 4096 / sampleRate * 1e3;
12
+ }
13
+ throw new Error(`unknown audio codec for AU duration derivation: ${codec}`);
14
+ }
15
+ function pictureDurationMs(framerate) {
16
+ if (!(framerate > 0)) throw new Error("framerate must be > 0");
17
+ return 1e3 / framerate;
18
+ }
19
+ export {
20
+ auDurationMsFromCodec,
21
+ pictureDurationMs
22
+ };
23
+ //# sourceMappingURL=au-duration.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/pacing/au-duration.ts"],
4
+ "sourcesContent": ["/**\n * Nominal Access Unit duration helpers for common audio + video codecs.\n *\n * Kept in the pacing subpath (no mmt-container dep) so lean consumers like\n * moqtail's player.ts can import the pacer + AU duration without pulling\n * in the full audio-render-pipeline (which depends on @blockcast/mmt-container).\n */\n\n/**\n * Nominal AU duration in ms for common audio codecs at the given sample rate.\n * AAC-LC: 1024 samples. Opus (default 20ms): 960 samples @ 48kHz.\n * Throws for unknown codecs so callers supply correct timing.\n */\nexport function auDurationMsFromCodec(codec: string, sampleRate: number): number {\n if (!(sampleRate > 0)) throw new Error('sampleRate must be > 0')\n if (codec.startsWith('opus') || codec.startsWith('Opus')) {\n return (960 / sampleRate) * 1000\n }\n if (codec.startsWith('mp4a') || codec.startsWith('AAC') || codec.startsWith('aac')) {\n return (1024 / sampleRate) * 1000\n }\n if (codec.startsWith('flac') || codec.startsWith('fLaC')) {\n return (4096 / sampleRate) * 1000\n }\n throw new Error(`unknown audio codec for AU duration derivation: ${codec}`)\n}\n\n/**\n * Nominal picture duration in ms from framerate.\n */\nexport function pictureDurationMs(framerate: number): number {\n if (!(framerate > 0)) throw new Error('framerate must be > 0')\n return 1000 / framerate\n}\n"],
5
+ "mappings": ";AAaO,SAAS,sBAAsB,OAAe,YAA4B;AAC/E,MAAI,EAAE,aAAa,GAAI,OAAM,IAAI,MAAM,wBAAwB;AAC/D,MAAI,MAAM,WAAW,MAAM,KAAK,MAAM,WAAW,MAAM,GAAG;AACxD,WAAQ,MAAM,aAAc;AAAA,EAC9B;AACA,MAAI,MAAM,WAAW,MAAM,KAAK,MAAM,WAAW,KAAK,KAAK,MAAM,WAAW,KAAK,GAAG;AAClF,WAAQ,OAAO,aAAc;AAAA,EAC/B;AACA,MAAI,MAAM,WAAW,MAAM,KAAK,MAAM,WAAW,MAAM,GAAG;AACxD,WAAQ,OAAO,aAAc;AAAA,EAC/B;AACA,QAAM,IAAI,MAAM,mDAAmD,KAAK,EAAE;AAC5E;AAKO,SAAS,kBAAkB,WAA2B;AAC3D,MAAI,EAAE,YAAY,GAAI,OAAM,IAAI,MAAM,uBAAuB;AAC7D,SAAO,MAAO;AAChB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * FrameCountPacer — media-agnostic wall-clock pacer for MMT A/V decode loops.
3
+ *
4
+ * Why not use MPU timestamps? Under FEC interleaving, encoders pack multiple
5
+ * Access Units (AUs) per MMTP MPU with a single mpu_timestamp. The observed
6
+ * timestamp delta between MPUs is a fraction of the real per-AU duration
7
+ * (e.g. 10.22ms between MPUs for 21.33ms AAC frames at D=2 interleave).
8
+ * Pacing on that delta sees "media ≈ wall" and never sleeps, letting burst
9
+ * relay delivery saturate the decoder / ring buffer.
10
+ *
11
+ * The Sony libatsc3 pattern (atsc3_mmt_context_mfu_depacketizer) solves this
12
+ * by anchoring wall-clock time to the first received AU and counting AUs:
13
+ * media_elapsed_ms = au_count × nominal_au_duration_ms
14
+ * The MPU timestamp is used elsewhere as an absolute-time anchor, never as
15
+ * an incremental clock.
16
+ *
17
+ * The sleep threshold comes from the catalog's FEC interleaveDepthMs (via
18
+ * FecBlockMapper.pacingThresholdMs). When no FEC is configured, a
19
+ * NullBlockMapper yields threshold=0 and this pacer never sleeps.
20
+ *
21
+ * One pacer instance = one track (e.g. audio, video) on one player.
22
+ */
23
+ import type { FecBlockMapper } from '@blockcast/mmt-fec';
24
+ export interface FrameCountPacerOpts {
25
+ /** Required: AU duration (audio) or picture interval (video), in ms. */
26
+ frameDurationMs: number;
27
+ /** FEC mapper from @blockcast/mmt-fec. NullBlockMapper disables pacing. */
28
+ mapper: FecBlockMapper;
29
+ /**
30
+ * Number of frames to let through before pacing starts. Lets startup
31
+ * burst drain populate the ring buffer / decoder queue. Default 0.
32
+ */
33
+ warmupFrames?: number;
34
+ }
35
+ export interface PacerSnapshot {
36
+ /** Frames observed (from notifyFrame). */
37
+ frameCount: number;
38
+ /** Computed media time: frameCount × frameDurationMs. */
39
+ mediaMs: number;
40
+ /** Wall-clock time since anchor. */
41
+ wallMs: number;
42
+ /** mediaMs - wallMs. Positive → decoder ahead of realtime. */
43
+ aheadMs: number;
44
+ /** Pacing threshold (ms) from the FEC mapper. */
45
+ thresholdMs: number;
46
+ /** Has the first frame been observed (wall anchor set)? */
47
+ anchored: boolean;
48
+ }
49
+ export declare class FrameCountPacer {
50
+ #private;
51
+ readonly frameDurationMs: number;
52
+ readonly thresholdMs: number;
53
+ readonly warmupFrames: number;
54
+ constructor(opts: FrameCountPacerOpts);
55
+ /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */
56
+ notifyFrame(): void;
57
+ /**
58
+ * Compute sleep ms before reading the next MoQ object.
59
+ * Returns 0 when:
60
+ * - pacing is disabled (NullBlockMapper → threshold=0)
61
+ * - the anchor is not yet set
62
+ * - still in warmup
63
+ * - decoder is not ahead of wall clock by more than threshold
64
+ * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).
65
+ */
66
+ computeSleepMs(): number;
67
+ /** Reset anchor + counter. Call on source switch (MoQ ↔ multicast). */
68
+ reset(): void;
69
+ get snapshot(): PacerSnapshot;
70
+ }
71
+ //# sourceMappingURL=frame-pacer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-pacer.d.ts","sourceRoot":"","sources":["../../src/pacing/frame-pacer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAA;IACvB,2EAA2E;IAC3E,MAAM,EAAE,cAAc,CAAA;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAA;IAClB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAA;IACf,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAA;IACnB,2DAA2D;IAC3D,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;gBAMjB,IAAI,EAAE,mBAAmB;IASrC,kFAAkF;IAClF,WAAW,IAAI,IAAI;IAQnB;;;;;;;;OAQG;IACH,cAAc,IAAI,MAAM;IAYxB,uEAAuE;IACvE,KAAK,IAAI,IAAI;IAMb,IAAI,QAAQ,IAAI,aAAa,CAW5B;CACF"}
@@ -0,0 +1,89 @@
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 __privateWrapper = (obj, member, setter, getter) => ({
9
+ set _(value) {
10
+ __privateSet(obj, member, value, setter);
11
+ },
12
+ get _() {
13
+ return __privateGet(obj, member, getter);
14
+ }
15
+ });
16
+
17
+ // src/pacing/frame-pacer.ts
18
+ var _frameCount, _anchorWallMs, _anchored;
19
+ var FrameCountPacer = class {
20
+ constructor(opts) {
21
+ __privateAdd(this, _frameCount, 0);
22
+ __privateAdd(this, _anchorWallMs, 0);
23
+ __privateAdd(this, _anchored, false);
24
+ if (!(opts.frameDurationMs > 0)) {
25
+ throw new Error("frameDurationMs must be > 0");
26
+ }
27
+ this.frameDurationMs = opts.frameDurationMs;
28
+ this.thresholdMs = opts.mapper.pacingThresholdMs;
29
+ this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0);
30
+ }
31
+ /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */
32
+ notifyFrame() {
33
+ if (!__privateGet(this, _anchored)) {
34
+ __privateSet(this, _anchorWallMs, now());
35
+ __privateSet(this, _anchored, true);
36
+ }
37
+ __privateWrapper(this, _frameCount)._++;
38
+ }
39
+ /**
40
+ * Compute sleep ms before reading the next MoQ object.
41
+ * Returns 0 when:
42
+ * - pacing is disabled (NullBlockMapper → threshold=0)
43
+ * - the anchor is not yet set
44
+ * - still in warmup
45
+ * - decoder is not ahead of wall clock by more than threshold
46
+ * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).
47
+ */
48
+ computeSleepMs() {
49
+ if (this.thresholdMs <= 0) return 0;
50
+ if (!__privateGet(this, _anchored)) return 0;
51
+ if (__privateGet(this, _frameCount) <= this.warmupFrames) return 0;
52
+ const mediaMs = __privateGet(this, _frameCount) * this.frameDurationMs;
53
+ const wallMs = now() - __privateGet(this, _anchorWallMs);
54
+ const aheadMs = mediaMs - wallMs;
55
+ if (aheadMs <= this.thresholdMs) return 0;
56
+ return aheadMs - this.thresholdMs / 2;
57
+ }
58
+ /** Reset anchor + counter. Call on source switch (MoQ ↔ multicast). */
59
+ reset() {
60
+ __privateSet(this, _frameCount, 0);
61
+ __privateSet(this, _anchorWallMs, 0);
62
+ __privateSet(this, _anchored, false);
63
+ }
64
+ get snapshot() {
65
+ const mediaMs = __privateGet(this, _frameCount) * this.frameDurationMs;
66
+ const wallMs = __privateGet(this, _anchored) ? now() - __privateGet(this, _anchorWallMs) : 0;
67
+ return {
68
+ frameCount: __privateGet(this, _frameCount),
69
+ mediaMs,
70
+ wallMs,
71
+ aheadMs: mediaMs - wallMs,
72
+ thresholdMs: this.thresholdMs,
73
+ anchored: __privateGet(this, _anchored)
74
+ };
75
+ }
76
+ };
77
+ _frameCount = new WeakMap();
78
+ _anchorWallMs = new WeakMap();
79
+ _anchored = new WeakMap();
80
+ function now() {
81
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
82
+ return performance.now();
83
+ }
84
+ return Date.now();
85
+ }
86
+ export {
87
+ FrameCountPacer
88
+ };
89
+ //# sourceMappingURL=frame-pacer.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/pacing/frame-pacer.ts"],
4
+ "sourcesContent": ["/**\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": ";;;;;;;;;;;;;;;;;AAAA;AAoDO,IAAM,kBAAN,MAAsB;AAAA,EAS3B,YAAY,MAA2B;AAJvC,oCAAc;AACd,sCAAgB;AAChB,kCAAY;AAGV,QAAI,EAAE,KAAK,kBAAkB,IAAI;AAC/B,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,SAAK,kBAAkB,KAAK;AAC5B,SAAK,cAAc,KAAK,OAAO;AAC/B,SAAK,eAAe,KAAK,IAAI,GAAG,KAAK,gBAAgB,CAAC;AAAA,EACxD;AAAA;AAAA,EAGA,cAAoB;AAClB,QAAI,CAAC,mBAAK,YAAW;AACnB,yBAAK,eAAgB,IAAI;AACzB,yBAAK,WAAY;AAAA,IACnB;AACA,2BAAK,aAAL;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,iBAAyB;AACvB,QAAI,KAAK,eAAe,EAAG,QAAO;AAClC,QAAI,CAAC,mBAAK,WAAW,QAAO;AAC5B,QAAI,mBAAK,gBAAe,KAAK,aAAc,QAAO;AAElD,UAAM,UAAU,mBAAK,eAAc,KAAK;AACxC,UAAM,SAAS,IAAI,IAAI,mBAAK;AAC5B,UAAM,UAAU,UAAU;AAC1B,QAAI,WAAW,KAAK,YAAa,QAAO;AACxC,WAAO,UAAU,KAAK,cAAc;AAAA,EACtC;AAAA;AAAA,EAGA,QAAc;AACZ,uBAAK,aAAc;AACnB,uBAAK,eAAgB;AACrB,uBAAK,WAAY;AAAA,EACnB;AAAA,EAEA,IAAI,WAA0B;AAC5B,UAAM,UAAU,mBAAK,eAAc,KAAK;AACxC,UAAM,SAAS,mBAAK,aAAY,IAAI,IAAI,mBAAK,iBAAgB;AAC7D,WAAO;AAAA,MACL,YAAY,mBAAK;AAAA,MACjB;AAAA,MACA;AAAA,MACA,SAAS,UAAU;AAAA,MACnB,aAAa,KAAK;AAAA,MAClB,UAAU,mBAAK;AAAA,IACjB;AAAA,EACF;AACF;AA9DE;AACA;AACA;AA8DF,SAAS,MAAc;AAGrB,MAAI,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,YAAY;AAC/E,WAAO,YAAY,IAAI;AAAA,EACzB;AACA,SAAO,KAAK,IAAI;AAClB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ export { FrameCountPacer } from './frame-pacer.js';
2
+ export type { FrameCountPacerOpts, PacerSnapshot } from './frame-pacer.js';
3
+ export { InterleaveTiming } from './interleave-timing.js';
4
+ export type { InterleaveTimingOpts } from './interleave-timing.js';
5
+ export { auDurationMsFromCodec, pictureDurationMs } from './au-duration.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pacing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAClD,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAClE,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}