@blockcast/mmt-render 0.1.0-main.c0e9e40265a1

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,179 @@
1
+ /**
2
+ * VideoRenderPipeline — end-to-end video render for MoQ/MMT players.
3
+ *
4
+ * Pipeline: MoQ object → MmtpDecodePipeline → keyframe gate → WebCodecs
5
+ * VideoDecoder → consumer's onFrame(VideoFrame).
6
+ *
7
+ * Replaces:
8
+ * - moqtail's `decodeQueueSize > 10 drop-deltas` hack in webcodecs-backend.ts
9
+ * - hang-mmt-fec's "reorderWindow = 0 // TODO" in video/decoder.ts
10
+ *
11
+ * Pacing uses FrameCountPacer (anchor on first decoded picture, frame count ×
12
+ * 1/fps) with threshold = mapper.pacingThresholdMs. Reorder window comes from
13
+ * mapper.d (groups per FEC block), which is the natural bound for out-of-order
14
+ * delivery within a block after converting catalog groups to frame count.
15
+ */
16
+
17
+ import { MmtpDecodePipeline, type PipelineFecClient } from '@blockcast/mmt-container/mmtp-decode-pipeline'
18
+ import type { MfuReassemblyPolicy } from '@blockcast/mmt-container/mfu-reassembly-policy'
19
+ import type { FecBlockMapper } from '@blockcast/mmt-fec'
20
+ import { FrameCountPacer } from '../pacing/frame-pacer.js'
21
+ import { KeyframeGate } from './keyframe-gate.js'
22
+
23
+ export interface VideoFrameMeta {
24
+ timestamp: number
25
+ keyframe: boolean
26
+ packetId: number
27
+ }
28
+
29
+ export type VideoFrameCallback = (frame: VideoFrame, meta: VideoFrameMeta) => void
30
+
31
+ export function deriveVideoReorderWindow(
32
+ framerate: number,
33
+ mapper: Pick<FecBlockMapper, 'd' | 'gopDurationMs'>,
34
+ ): number {
35
+ const framesPerGroup = Math.ceil(framerate * mapper.gopDurationMs / 1000)
36
+ return mapper.d * framesPerGroup
37
+ }
38
+
39
+ export interface VideoRenderPipelineOpts {
40
+ codec: string
41
+ /** MMTP packet_id for this video track — from catalog. */
42
+ packetId?: number
43
+ description?: Uint8Array
44
+ width: number
45
+ height: number
46
+ /** Frames per second (used for pacing + default reorder window). */
47
+ framerate: number
48
+ mapper: FecBlockMapper
49
+ fecClient?: PipelineFecClient
50
+ /** Reliable MoQ is clock-free; datagram deadlines come from catalog/in-band timing. */
51
+ reassemblyPolicy: MfuReassemblyPolicy
52
+ /** Receives VideoFrames ready to render. */
53
+ onFrame: VideoFrameCallback
54
+ /** Called on decoder errors. */
55
+ onError?: (err: unknown) => void
56
+ /** Reorder window in frames. Defaults to one catalog-derived FEC block. */
57
+ reorderWindow?: number
58
+ /** Warmup frames before pacing engages. Default 3 (absorbs decoder init jitter). */
59
+ warmupFrames?: number
60
+ /** Optional decoder hardware acceleration hint. */
61
+ hardwareAcceleration?: HardwareAcceleration
62
+ }
63
+
64
+ export class VideoRenderPipeline {
65
+ readonly #pacer: FrameCountPacer
66
+ readonly #gate = new KeyframeGate()
67
+ readonly #opts: VideoRenderPipelineOpts
68
+
69
+ #decoder: VideoDecoder | null = null
70
+ #pipeline: MmtpDecodePipeline | null = null
71
+ #closed = false
72
+
73
+ constructor(opts: VideoRenderPipelineOpts) {
74
+ if (!(opts.framerate > 0)) throw new Error('framerate must be > 0')
75
+ this.#opts = opts
76
+ this.#pacer = new FrameCountPacer({
77
+ frameDurationMs: 1000 / opts.framerate,
78
+ mapper: opts.mapper,
79
+ warmupFrames: opts.warmupFrames ?? 3,
80
+ })
81
+ this.#init()
82
+ }
83
+
84
+ feedMoqObject(packet: Uint8Array): void {
85
+ this.#pipeline?.feedMoqObject(packet)
86
+ }
87
+
88
+ paceSleepMs(): number {
89
+ return this.#pacer.computeSleepMs()
90
+ }
91
+
92
+ get pacerSnapshot() {
93
+ return this.#pacer.snapshot
94
+ }
95
+
96
+ /** Request a fresh keyframe after a decoder error (or source switch). */
97
+ reset(): void {
98
+ this.#gate.reset()
99
+ this.#pacer.reset()
100
+ try { this.#decoder?.reset() } catch { /* may be closed */ }
101
+ // Re-configure so the decoder is ready for the next keyframe.
102
+ this.#configureDecoder()
103
+ }
104
+
105
+ close(): void {
106
+ if (this.#closed) return
107
+ this.#closed = true
108
+ try { this.#decoder?.close() } catch { /* noop */ }
109
+ try { this.#pipeline?.dispose() } catch { /* noop */ }
110
+ this.#decoder = null
111
+ this.#pipeline = null
112
+ }
113
+
114
+ // ─── Private ─────────────────────────────────────────────────────
115
+
116
+ #init(): void {
117
+ this.#configureDecoder()
118
+
119
+ const pipeline = new MmtpDecodePipeline()
120
+ const reorderWindow = this.#opts.reorderWindow
121
+ ?? deriveVideoReorderWindow(this.#opts.framerate, this.#opts.mapper)
122
+ pipeline.configure({
123
+ codec: this.#opts.codec,
124
+ packetId: this.#opts.packetId,
125
+ description: this.#opts.description,
126
+ reorderWindow,
127
+ framerate: this.#opts.framerate,
128
+ groupDurationMs: this.#opts.mapper.gopDurationMs,
129
+ reassemblyPolicy: this.#opts.reassemblyPolicy,
130
+ })
131
+ if (this.#opts.fecClient) pipeline.setFecClient(this.#opts.fecClient)
132
+
133
+ pipeline.onFrame((codecData, meta) => {
134
+ if (this.#closed || !this.#decoder) return
135
+ if (codecData.byteLength === 0) return
136
+ if (!this.#gate.allow(meta.keyframe)) return
137
+
138
+ this.#pacer.notifyFrame()
139
+ try {
140
+ this.#decoder.decode(new EncodedVideoChunk({
141
+ type: meta.keyframe ? 'key' : 'delta',
142
+ data: codecData,
143
+ timestamp: meta.timestamp,
144
+ }))
145
+ } catch (err) {
146
+ this.#opts.onError?.(err)
147
+ this.reset()
148
+ }
149
+ })
150
+ this.#pipeline = pipeline
151
+ }
152
+
153
+ #configureDecoder(): void {
154
+ try { this.#decoder?.close() } catch { /* noop */ }
155
+
156
+ const userMeta: VideoFrameMeta = { timestamp: 0, keyframe: false, packetId: 0 }
157
+ const decoder = new VideoDecoder({
158
+ output: (frame) => {
159
+ // WebCodecs VideoFrame doesn't carry our frame meta; the consumer
160
+ // rematches via frame.timestamp if needed.
161
+ userMeta.timestamp = frame.timestamp
162
+ this.#opts.onFrame(frame, userMeta)
163
+ },
164
+ error: (err) => {
165
+ this.#opts.onError?.(err)
166
+ this.#gate.reset()
167
+ },
168
+ })
169
+ const config: VideoDecoderConfig = {
170
+ codec: this.#opts.codec,
171
+ codedWidth: this.#opts.width,
172
+ codedHeight: this.#opts.height,
173
+ ...(this.#opts.description ? { description: this.#opts.description } : {}),
174
+ ...(this.#opts.hardwareAcceleration ? { hardwareAcceleration: this.#opts.hardwareAcceleration } : {}),
175
+ }
176
+ decoder.configure(config)
177
+ this.#decoder = decoder
178
+ }
179
+ }