@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.
- package/README.md +207 -0
- package/dist/.build-stamp +0 -0
- package/dist/audio/audio-buffer-plan.d.ts +27 -0
- package/dist/audio/audio-buffer-plan.d.ts.map +1 -0
- package/dist/audio/audio-render-pipeline.d.ts +83 -0
- package/dist/audio/audio-render-pipeline.d.ts.map +1 -0
- package/dist/audio/audio-render-pipeline.js +372 -0
- package/dist/audio/audio-render-pipeline.js.map +7 -0
- package/dist/audio/audio-ring-buffer.d.ts +55 -0
- package/dist/audio/audio-ring-buffer.d.ts.map +1 -0
- package/dist/audio/audio-ring-buffer.js +255 -0
- package/dist/audio/audio-ring-buffer.js.map +7 -0
- package/dist/audio/index.d.ts +9 -0
- package/dist/audio/index.d.ts.map +1 -0
- package/dist/audio/index.js +640 -0
- package/dist/audio/index.js.map +7 -0
- package/dist/audio/render-messages.d.ts +94 -0
- package/dist/audio/render-messages.d.ts.map +1 -0
- package/dist/audio/render-messages.js +1 -0
- package/dist/audio/render-messages.js.map +7 -0
- package/dist/audio/render-worklet.d.ts +13 -0
- package/dist/audio/render-worklet.d.ts.map +1 -0
- package/dist/audio/worklet-url.d.ts +25 -0
- package/dist/audio/worklet-url.d.ts.map +1 -0
- package/dist/audio/worklet-url.js +11 -0
- package/dist/audio/worklet-url.js.map +7 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/pacing/au-duration.d.ts +18 -0
- package/dist/pacing/au-duration.d.ts.map +1 -0
- package/dist/pacing/au-duration.js +23 -0
- package/dist/pacing/au-duration.js.map +7 -0
- package/dist/pacing/frame-pacer.d.ts +71 -0
- package/dist/pacing/frame-pacer.d.ts.map +1 -0
- package/dist/pacing/frame-pacer.js +89 -0
- package/dist/pacing/frame-pacer.js.map +7 -0
- package/dist/pacing/index.d.ts +6 -0
- package/dist/pacing/index.d.ts.map +1 -0
- package/dist/pacing/index.js +166 -0
- package/dist/pacing/index.js.map +7 -0
- package/dist/pacing/interleave-timing.d.ts +64 -0
- package/dist/pacing/interleave-timing.d.ts.map +1 -0
- package/dist/pacing/interleave-timing.js +58 -0
- package/dist/pacing/interleave-timing.js.map +7 -0
- package/dist/video/index.d.ts +4 -0
- package/dist/video/index.d.ts.map +1 -0
- package/dist/video/index.js +247 -0
- package/dist/video/index.js.map +7 -0
- package/dist/video/keyframe-gate.d.ts +21 -0
- package/dist/video/keyframe-gate.d.ts.map +1 -0
- package/dist/video/keyframe-gate.js +39 -0
- package/dist/video/keyframe-gate.js.map +7 -0
- package/dist/video/video-render-pipeline.d.ts +60 -0
- package/dist/video/video-render-pipeline.d.ts.map +1 -0
- package/dist/video/video-render-pipeline.js +247 -0
- package/dist/video/video-render-pipeline.js.map +7 -0
- package/dist/worklet/render-worklet.js +403 -0
- package/dist/worklet/render-worklet.js.map +7 -0
- package/package.json +101 -0
- package/src/audio/audio-buffer-plan.ts +62 -0
- package/src/audio/audio-render-pipeline.ts +313 -0
- package/src/audio/audio-ring-buffer.ts +281 -0
- package/src/audio/index.ts +14 -0
- package/src/audio/render-messages.ts +110 -0
- package/src/audio/render-worklet.ts +161 -0
- package/src/audio/worklet-url.ts +30 -0
- package/src/index.ts +13 -0
- package/src/pacing/au-duration.ts +34 -0
- package/src/pacing/frame-pacer.ts +129 -0
- package/src/pacing/index.ts +5 -0
- package/src/pacing/interleave-timing.ts +95 -0
- package/src/video/index.ts +7 -0
- package/src/video/keyframe-gate.ts +35 -0
- 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
|
+
}
|