@meframe/core 0.0.2 → 0.0.3

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 (137) hide show
  1. package/dist/Meframe.d.ts.map +1 -1
  2. package/dist/Meframe.js +2 -1
  3. package/dist/Meframe.js.map +1 -1
  4. package/dist/config/defaults.d.ts.map +1 -1
  5. package/dist/config/defaults.js +2 -1
  6. package/dist/config/defaults.js.map +1 -1
  7. package/dist/config/types.d.ts +3 -0
  8. package/dist/config/types.d.ts.map +1 -1
  9. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  10. package/dist/orchestrator/Orchestrator.js +2 -1
  11. package/dist/orchestrator/Orchestrator.js.map +1 -1
  12. package/dist/orchestrator/types.d.ts +1 -0
  13. package/dist/orchestrator/types.d.ts.map +1 -1
  14. package/dist/stages/compose/types.d.ts +2 -1
  15. package/dist/stages/compose/types.d.ts.map +1 -1
  16. package/dist/stages/demux/MP4Demuxer.d.ts +0 -1
  17. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  18. package/dist/utils/time-utils.d.ts +3 -2
  19. package/dist/utils/time-utils.d.ts.map +1 -1
  20. package/dist/utils/time-utils.js +2 -1
  21. package/dist/utils/time-utils.js.map +1 -1
  22. package/dist/vite-plugin.d.ts +5 -3
  23. package/dist/vite-plugin.d.ts.map +1 -1
  24. package/dist/vite-plugin.js +109 -52
  25. package/dist/vite-plugin.js.map +1 -1
  26. package/dist/worker/WorkerPool.d.ts +7 -0
  27. package/dist/worker/WorkerPool.d.ts.map +1 -1
  28. package/dist/worker/WorkerPool.js +29 -5
  29. package/dist/worker/WorkerPool.js.map +1 -1
  30. package/dist/{stages/demux → workers}/MP4Demuxer.js +4 -13
  31. package/dist/workers/MP4Demuxer.js.map +1 -0
  32. package/dist/workers/WorkerChannel.js +486 -0
  33. package/dist/workers/WorkerChannel.js.map +1 -0
  34. package/dist/{assets/video-demux.worker-D019I7GQ.js → workers/mp4box.all.js} +4 -912
  35. package/dist/workers/mp4box.all.js.map +1 -0
  36. package/dist/{assets/audio-compose.worker-nGVvHD5Q.js → workers/stages/compose/audio-compose.worker.js} +7 -481
  37. package/dist/workers/stages/compose/audio-compose.worker.js.map +1 -0
  38. package/dist/{assets/video-compose.worker-DPzsC21d.js → workers/stages/compose/video-compose.worker.js} +7 -481
  39. package/dist/workers/stages/compose/video-compose.worker.js.map +1 -0
  40. package/dist/{assets/decode.worker-DpWHsc7R.js → workers/stages/decode/decode.worker.js} +7 -481
  41. package/dist/workers/stages/decode/decode.worker.js.map +1 -0
  42. package/dist/{stages → workers/stages}/demux/audio-demux.worker.js +184 -4
  43. package/dist/workers/stages/demux/audio-demux.worker.js.map +1 -0
  44. package/dist/{stages → workers/stages}/demux/video-demux.worker.js +2 -3
  45. package/dist/workers/stages/demux/video-demux.worker.js.map +1 -0
  46. package/dist/{stages → workers/stages}/encode/encode.worker.js +238 -4
  47. package/dist/workers/stages/encode/encode.worker.js.map +1 -0
  48. package/dist/{stages/mux/MP4Muxer.js → workers/stages/mux/mux.worker.js} +244 -5
  49. package/dist/workers/stages/mux/mux.worker.js.map +1 -0
  50. package/package.json +21 -21
  51. package/dist/assets/audio-compose.worker-nGVvHD5Q.js.map +0 -1
  52. package/dist/assets/audio-demux.worker-xwWBtbAe.js +0 -8299
  53. package/dist/assets/audio-demux.worker-xwWBtbAe.js.map +0 -1
  54. package/dist/assets/decode.worker-DpWHsc7R.js.map +0 -1
  55. package/dist/assets/encode.worker-nfOb3kw6.js +0 -1026
  56. package/dist/assets/encode.worker-nfOb3kw6.js.map +0 -1
  57. package/dist/assets/mux.worker-uEMQY066.js +0 -8019
  58. package/dist/assets/mux.worker-uEMQY066.js.map +0 -1
  59. package/dist/assets/video-compose.worker-DPzsC21d.js.map +0 -1
  60. package/dist/assets/video-demux.worker-D019I7GQ.js.map +0 -1
  61. package/dist/model/types.js +0 -5
  62. package/dist/model/types.js.map +0 -1
  63. package/dist/plugins/BackpressureMonitor.js +0 -62
  64. package/dist/plugins/BackpressureMonitor.js.map +0 -1
  65. package/dist/stages/compose/AudioDucker.js +0 -161
  66. package/dist/stages/compose/AudioDucker.js.map +0 -1
  67. package/dist/stages/compose/AudioMixer.js +0 -373
  68. package/dist/stages/compose/AudioMixer.js.map +0 -1
  69. package/dist/stages/compose/FilterProcessor.js +0 -226
  70. package/dist/stages/compose/FilterProcessor.js.map +0 -1
  71. package/dist/stages/compose/LayerRenderer.js +0 -215
  72. package/dist/stages/compose/LayerRenderer.js.map +0 -1
  73. package/dist/stages/compose/TransitionProcessor.js +0 -189
  74. package/dist/stages/compose/TransitionProcessor.js.map +0 -1
  75. package/dist/stages/compose/VideoComposer.js +0 -186
  76. package/dist/stages/compose/VideoComposer.js.map +0 -1
  77. package/dist/stages/compose/audio-compose.worker.d.ts +0 -79
  78. package/dist/stages/compose/audio-compose.worker.d.ts.map +0 -1
  79. package/dist/stages/compose/audio-compose.worker.js +0 -540
  80. package/dist/stages/compose/audio-compose.worker.js.map +0 -1
  81. package/dist/stages/compose/audio-compose.worker2.js +0 -5
  82. package/dist/stages/compose/audio-compose.worker2.js.map +0 -1
  83. package/dist/stages/compose/video-compose.worker.d.ts +0 -60
  84. package/dist/stages/compose/video-compose.worker.d.ts.map +0 -1
  85. package/dist/stages/compose/video-compose.worker.js +0 -379
  86. package/dist/stages/compose/video-compose.worker.js.map +0 -1
  87. package/dist/stages/compose/video-compose.worker2.js +0 -5
  88. package/dist/stages/compose/video-compose.worker2.js.map +0 -1
  89. package/dist/stages/decode/AudioChunkDecoder.js +0 -82
  90. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  91. package/dist/stages/decode/BaseDecoder.js +0 -130
  92. package/dist/stages/decode/BaseDecoder.js.map +0 -1
  93. package/dist/stages/decode/VideoChunkDecoder.js +0 -199
  94. package/dist/stages/decode/VideoChunkDecoder.js.map +0 -1
  95. package/dist/stages/decode/decode.worker.d.ts +0 -70
  96. package/dist/stages/decode/decode.worker.d.ts.map +0 -1
  97. package/dist/stages/decode/decode.worker.js +0 -423
  98. package/dist/stages/decode/decode.worker.js.map +0 -1
  99. package/dist/stages/decode/decode.worker2.js +0 -5
  100. package/dist/stages/decode/decode.worker2.js.map +0 -1
  101. package/dist/stages/demux/MP3FrameParser.js +0 -186
  102. package/dist/stages/demux/MP3FrameParser.js.map +0 -1
  103. package/dist/stages/demux/MP4Demuxer.js.map +0 -1
  104. package/dist/stages/demux/audio-demux.worker.d.ts +0 -51
  105. package/dist/stages/demux/audio-demux.worker.d.ts.map +0 -1
  106. package/dist/stages/demux/audio-demux.worker.js.map +0 -1
  107. package/dist/stages/demux/audio-demux.worker2.js +0 -5
  108. package/dist/stages/demux/audio-demux.worker2.js.map +0 -1
  109. package/dist/stages/demux/video-demux.worker.d.ts +0 -51
  110. package/dist/stages/demux/video-demux.worker.d.ts.map +0 -1
  111. package/dist/stages/demux/video-demux.worker.js.map +0 -1
  112. package/dist/stages/demux/video-demux.worker2.js +0 -5
  113. package/dist/stages/demux/video-demux.worker2.js.map +0 -1
  114. package/dist/stages/encode/AudioChunkEncoder.js +0 -37
  115. package/dist/stages/encode/AudioChunkEncoder.js.map +0 -1
  116. package/dist/stages/encode/BaseEncoder.js +0 -164
  117. package/dist/stages/encode/BaseEncoder.js.map +0 -1
  118. package/dist/stages/encode/VideoChunkEncoder.js +0 -50
  119. package/dist/stages/encode/VideoChunkEncoder.js.map +0 -1
  120. package/dist/stages/encode/encode.worker.d.ts +0 -3
  121. package/dist/stages/encode/encode.worker.d.ts.map +0 -1
  122. package/dist/stages/encode/encode.worker.js.map +0 -1
  123. package/dist/stages/encode/encode.worker2.js +0 -5
  124. package/dist/stages/encode/encode.worker2.js.map +0 -1
  125. package/dist/stages/mux/MP4Muxer.js.map +0 -1
  126. package/dist/stages/mux/mux.worker.d.ts +0 -65
  127. package/dist/stages/mux/mux.worker.d.ts.map +0 -1
  128. package/dist/stages/mux/mux.worker.js +0 -219
  129. package/dist/stages/mux/mux.worker.js.map +0 -1
  130. package/dist/stages/mux/mux.worker2.js +0 -5
  131. package/dist/stages/mux/mux.worker2.js.map +0 -1
  132. package/dist/stages/mux/utils.js +0 -34
  133. package/dist/stages/mux/utils.js.map +0 -1
  134. package/dist/worker/worker-registry.d.ts +0 -12
  135. package/dist/worker/worker-registry.d.ts.map +0 -1
  136. package/dist/worker/worker-registry.js +0 -20
  137. package/dist/worker/worker-registry.js.map +0 -1
@@ -1,379 +0,0 @@
1
- import { WorkerChannel } from "../../worker/WorkerChannel.js";
2
- import { WorkerMessageType, WorkerState } from "../../worker/types.js";
3
- import { VideoComposer } from "./VideoComposer.js";
4
- import { frameIndexFromTimestamp, quantizeTimestampToFrame, frameDurationFromFps } from "../../utils/time-utils.js";
5
- function resolveActiveLayers(layers, timestamp, _frame) {
6
- return layers.filter((layer) => {
7
- if (layer.status !== "ready") {
8
- return false;
9
- }
10
- return layer.activeRanges.some(
11
- (range) => timestamp >= range.startUs && timestamp < range.endUs
12
- );
13
- });
14
- }
15
- function materializeLayer(layer, frame) {
16
- const baseLayer = {
17
- id: layer.layerId,
18
- type: layer.type,
19
- zIndex: layer.zIndex ?? 0,
20
- visible: true,
21
- opacity: layer.opacity ?? 1
22
- };
23
- if (layer.type === "video") {
24
- return {
25
- ...baseLayer,
26
- type: "video",
27
- videoFrame: frame
28
- };
29
- }
30
- if (layer.type === "text") {
31
- const payload = layer.payload;
32
- return {
33
- ...baseLayer,
34
- type: "text",
35
- text: payload.text,
36
- fontFamily: payload.fontFamily,
37
- fontSize: payload.fontSize,
38
- fontWeight: payload.fontWeight,
39
- color: payload.color,
40
- strokeColor: payload.strokeColor,
41
- strokeWidth: payload.strokeWidth,
42
- lineHeight: payload.lineHeight,
43
- textAlign: payload.align,
44
- verticalAlign: "bottom"
45
- // Subtitles positioned at bottom
46
- };
47
- }
48
- if (layer.type === "image") {
49
- const payload = layer.payload;
50
- const source = payload.bitmapHandle ?? null;
51
- if (source) {
52
- return {
53
- ...baseLayer,
54
- type: "image",
55
- source
56
- };
57
- }
58
- }
59
- return baseLayer;
60
- }
61
- class VideoComposeWorker {
62
- channel;
63
- composer = null;
64
- composeStream = null;
65
- downstreamPorts = /* @__PURE__ */ new Map();
66
- upstreamPorts = /* @__PURE__ */ new Map();
67
- instructionRegistry = /* @__PURE__ */ new Map();
68
- pendingReplay = /* @__PURE__ */ new Map();
69
- streamState = /* @__PURE__ */ new Map();
70
- constructor() {
71
- this.channel = new WorkerChannel(self, {
72
- name: "VideoComposeWorker",
73
- timeout: 3e4
74
- });
75
- this.setupHandlers();
76
- }
77
- setupHandlers() {
78
- this.channel.registerHandler("configure", this.handleConfigure.bind(this));
79
- this.channel.registerHandler("connect", this.handleConnect.bind(this));
80
- this.channel.registerHandler("flush", this.handleFlush.bind(this));
81
- this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
82
- this.channel.registerHandler("install_instructions", this.handleInstallInstructions.bind(this));
83
- this.channel.registerHandler("sync_clip", this.handleSyncClip.bind(this));
84
- this.channel.registerHandler("dispose_clip", this.handleDisposeClip.bind(this));
85
- this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
86
- }
87
- /**
88
- * Unified connect handler used by stream pipeline
89
- */
90
- async handleConnect(payload) {
91
- const { port, direction, clipId = "default" } = payload;
92
- if (direction === "upstream") {
93
- this.upstreamPorts.set(clipId, port);
94
- const channel = new WorkerChannel(port, {
95
- name: "VideoCompose-Decode",
96
- timeout: 3e4
97
- });
98
- channel.receiveStream(this.handleReceiveStream.bind(this));
99
- }
100
- if (direction === "downstream") {
101
- this.downstreamPorts.set(clipId, port);
102
- }
103
- return { success: true };
104
- }
105
- /**
106
- * Configure composer
107
- * According to docs/impl/14-config, only reinitialize when initial=true
108
- */
109
- async handleConfigure(payload) {
110
- const { config, initial } = payload;
111
- const hasValidDimensions = config.width > 0 && config.height > 0;
112
- const hasValidFps = config.fps > 0;
113
- if (!hasValidDimensions || !hasValidFps) {
114
- throw new Error(
115
- `VideoComposeWorker: invalid canvas config width=${config.width}, height=${config.height}, fps=${config.fps}`
116
- );
117
- }
118
- if (initial) {
119
- this.channel.state = WorkerState.Ready;
120
- }
121
- if (initial || !this.composer) {
122
- if (this.composer) {
123
- this.composer.dispose();
124
- this.composeStream = null;
125
- }
126
- this.composer = new VideoComposer(config);
127
- } else {
128
- this.composer.updateConfig(config);
129
- }
130
- this.channel.notify("configured", {
131
- config: this.composer.config,
132
- initialized: initial || false
133
- });
134
- return {
135
- success: true,
136
- config: this.composer.config
137
- };
138
- }
139
- async handleReceiveStream(stream, metadata) {
140
- const { clipId = "default" } = metadata || {};
141
- if (!this.composer) {
142
- console.error("[VideoComposeWorker] Composer not configured");
143
- return;
144
- }
145
- const instruction = this.instructionRegistry.get(clipId);
146
- if (!instruction) {
147
- console.warn("[VideoComposeWorker] No instructions for clip", clipId);
148
- return;
149
- }
150
- const filteredStream = stream.pipeThrough(
151
- new TransformStream({
152
- transform: (wrappedFrame, controller) => {
153
- try {
154
- const frame = wrappedFrame.frame || wrappedFrame;
155
- const gopSerial = wrappedFrame.gopSerial;
156
- const isKeyframe = wrappedFrame.isKeyframe;
157
- const timestamp = frame.timestamp ?? 0;
158
- if (this.shouldSkipFrame(clipId, timestamp)) {
159
- frame.close();
160
- return;
161
- }
162
- const request = this.buildComposeRequest(clipId, instruction, frame, timestamp);
163
- if (!request) {
164
- frame.close();
165
- return;
166
- }
167
- request.gopSerial = gopSerial;
168
- request.isKeyframe = isKeyframe;
169
- controller.enqueue(request);
170
- } catch (error) {
171
- const frame = wrappedFrame.frame || wrappedFrame;
172
- frame?.close?.();
173
- throw error;
174
- }
175
- }
176
- })
177
- );
178
- const { composeStream, cacheStream } = this.composer.createStreams();
179
- this.channel.sendStream(cacheStream, metadata);
180
- filteredStream.pipeTo(composeStream).catch((error) => {
181
- console.error("[VideoComposeWorker] compose stream error", clipId, error);
182
- });
183
- }
184
- // private handleGetStream(): ReadableStream<VideoFrame> | undefined {
185
- // return this.composer?.createStreams()?.cacheStream;
186
- // }
187
- /**
188
- * Flush the composition pipeline
189
- */
190
- async handleFlush() {
191
- try {
192
- this.channel.notify("flush_complete", {});
193
- return { success: true };
194
- } catch (error) {
195
- throw {
196
- code: "FLUSH_ERROR",
197
- message: error.message
198
- };
199
- }
200
- }
201
- /**
202
- * Get composer statistics
203
- */
204
- async handleGetStats() {
205
- return {
206
- configured: this.composer !== null,
207
- config: this.composer?.config,
208
- streaming: this.composeStream !== null
209
- };
210
- }
211
- /**
212
- * Dispose worker and cleanup resources
213
- */
214
- async handleDispose() {
215
- if (this.composer) {
216
- this.composer.dispose();
217
- this.composer = null;
218
- }
219
- this.composeStream = null;
220
- this.downstreamPorts.get("default")?.close();
221
- this.upstreamPorts.get("default")?.close();
222
- this.downstreamPorts.clear();
223
- this.upstreamPorts.clear();
224
- this.channel.state = WorkerState.Disposed;
225
- return { success: true };
226
- }
227
- async handleInstallInstructions(data) {
228
- const { clipId, revision } = data;
229
- const current = this.instructionRegistry.get(clipId);
230
- if (current && current.revision > revision) {
231
- return { success: false };
232
- }
233
- this.instructionRegistry.set(clipId, data);
234
- return { success: true };
235
- }
236
- async handleSyncClip(payload) {
237
- const { clipId, revision, range } = payload;
238
- const current = this.instructionRegistry.get(clipId);
239
- if (!current || current.revision > revision) {
240
- return { success: false };
241
- }
242
- this.pendingReplay.set(clipId, { ...range, revision });
243
- this.channel.notify("sync_ack", { clipId, revision });
244
- return { success: true };
245
- }
246
- async handleDisposeClip(payload) {
247
- const { clipId } = payload;
248
- this.instructionRegistry.delete(clipId);
249
- this.pendingReplay.delete(clipId);
250
- this.downstreamPorts.get(clipId)?.close();
251
- this.upstreamPorts.get(clipId)?.close();
252
- this.downstreamPorts.delete(clipId);
253
- this.upstreamPorts.delete(clipId);
254
- return { success: true };
255
- }
256
- /**
257
- * Check if frame should be skipped (outside dirty range)
258
- * Returns true if frame is NOT in the dirty range and should use cached version
259
- */
260
- shouldSkipFrame(clipId, timestamp) {
261
- const dirtyRange = this.pendingReplay.get(clipId);
262
- if (!dirtyRange) {
263
- return false;
264
- }
265
- if (timestamp >= dirtyRange.startUs && timestamp <= dirtyRange.endUs) {
266
- return false;
267
- }
268
- if (timestamp > dirtyRange.endUs) {
269
- this.pendingReplay.delete(clipId);
270
- }
271
- return true;
272
- }
273
- buildComposeRequest(clipId, instruction, frame, _timestamp) {
274
- const normalizedTime = this.computeTimelineTimestamp(clipId, frame, instruction.baseConfig);
275
- const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
276
- const clipDurationUs = instruction.baseConfig.timeline?.clipDurationUs ?? Infinity;
277
- const clipEndUs = clipStartUs + clipDurationUs;
278
- if (normalizedTime < clipStartUs || normalizedTime >= clipEndUs) {
279
- return null;
280
- }
281
- const clipRelativeTime = normalizedTime - clipStartUs;
282
- const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
283
- if (!activeLayers.length) {
284
- return null;
285
- }
286
- const layers = activeLayers.map((layer) => materializeLayer(layer, frame));
287
- return {
288
- timeUs: normalizedTime,
289
- layers,
290
- transition: VideoComposeWorker.buildTransition(
291
- instruction.transitions,
292
- clipRelativeTime,
293
- instruction.baseConfig.timeline
294
- )
295
- };
296
- }
297
- static buildTransition(transitions, timeUs, _timeline) {
298
- const entry = transitions.find((transition) => {
299
- const { startUs, endUs } = transition.range;
300
- return timeUs >= startUs && timeUs < endUs;
301
- });
302
- if (!entry) {
303
- return void 0;
304
- }
305
- const durationUs = entry.range.endUs - entry.range.startUs;
306
- const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;
307
- return {
308
- type: entry.params.type,
309
- progress: Math.min(Math.max(progress, 0), 1),
310
- easing: entry.params.easing,
311
- params: entry.params.payload,
312
- direction: entry.params.payload?.direction
313
- };
314
- }
315
- computeTimelineTimestamp(clipId, frame, config) {
316
- const key = clipId;
317
- let state = this.streamState.get(key);
318
- if (!state) {
319
- state = {
320
- baseTimestamp: null,
321
- lastSourceTimestamp: null,
322
- nextFrameIndex: 0
323
- };
324
- this.streamState.set(key, state);
325
- }
326
- const timeline = config.timeline;
327
- if (!timeline) {
328
- const ts = frame.timestamp ?? 0;
329
- state.lastSourceTimestamp = frame.timestamp ?? null;
330
- return ts;
331
- }
332
- const { clipStartUs, compositionFps } = timeline;
333
- const sourceTimestamp = frame.timestamp ?? null;
334
- if (sourceTimestamp !== null && state.lastSourceTimestamp !== null && sourceTimestamp < state.lastSourceTimestamp) {
335
- state.baseTimestamp = null;
336
- state.nextFrameIndex = 0;
337
- }
338
- if (state.baseTimestamp === null) {
339
- state.baseTimestamp = sourceTimestamp ?? 0;
340
- state.nextFrameIndex = 0;
341
- if (state.baseTimestamp > 1e3) {
342
- console.warn(
343
- `[VideoComposeWorker] First frame timestamp is ${state.baseTimestamp}us, expected ~0. Check MP4Demuxer normalization.`
344
- );
345
- }
346
- }
347
- const frameDuration = frameDurationFromFps(compositionFps);
348
- let frameIndex = state.nextFrameIndex;
349
- if (sourceTimestamp !== null) {
350
- const approxIndex = frameIndexFromTimestamp(
351
- state.baseTimestamp,
352
- sourceTimestamp,
353
- compositionFps,
354
- "nearest"
355
- );
356
- frameIndex = Math.max(frameIndex, approxIndex);
357
- }
358
- const rawTimeline = clipStartUs + frameIndex * frameDuration;
359
- const timelineTime = quantizeTimestampToFrame(
360
- rawTimeline,
361
- clipStartUs,
362
- compositionFps,
363
- "nearest"
364
- );
365
- state.nextFrameIndex = frameIndex + 1;
366
- state.lastSourceTimestamp = sourceTimestamp;
367
- return timelineTime;
368
- }
369
- }
370
- const worker = new VideoComposeWorker();
371
- self.addEventListener("beforeunload", () => {
372
- worker["handleDispose"]();
373
- });
374
- const videoCompose_worker = null;
375
- export {
376
- VideoComposeWorker,
377
- videoCompose_worker as default
378
- };
379
- //# sourceMappingURL=video-compose.worker.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"video-compose.worker.js","sources":["../../../src/stages/compose/video-compose.worker.ts"],"sourcesContent":["import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { VideoComposer } from './VideoComposer';\nimport type { VideoComposeConfig, ComposeRequest, Layer } from './types';\nimport type {\n ClipInstructionSet,\n SerializedLayerPlan,\n SerializedTransitionPlan,\n SerializedImageLayerPayload,\n SerializedTextLayerPayload,\n} from './instructions';\nimport {\n frameDurationFromFps,\n frameIndexFromTimestamp,\n quantizeTimestampToFrame,\n} from '../../utils/time-utils';\nimport type { TimeUs } from '../../model/types';\n\nfunction resolveActiveLayers(\n layers: SerializedLayerPlan[],\n timestamp: number,\n _frame: VideoFrame\n): SerializedLayerPlan[] {\n return layers.filter((layer) => {\n if (layer.status !== 'ready') {\n return false;\n }\n return layer.activeRanges.some(\n (range) => timestamp >= range.startUs && timestamp < range.endUs\n );\n });\n}\n\nfunction materializeLayer(layer: SerializedLayerPlan, frame: VideoFrame): Layer {\n const baseLayer: Layer = {\n id: layer.layerId,\n type: layer.type as any,\n zIndex: layer.zIndex ?? 0,\n visible: true,\n opacity: layer.opacity ?? 1,\n };\n\n if (layer.type === 'video') {\n return {\n ...baseLayer,\n type: 'video',\n videoFrame: frame,\n } as Layer;\n }\n\n if (layer.type === 'text') {\n const payload = layer.payload as SerializedTextLayerPayload;\n return {\n ...baseLayer,\n type: 'text',\n text: payload.text,\n fontFamily: payload.fontFamily,\n fontSize: payload.fontSize,\n fontWeight: payload.fontWeight,\n color: payload.color,\n strokeColor: payload.strokeColor,\n strokeWidth: payload.strokeWidth,\n lineHeight: payload.lineHeight,\n textAlign: payload.align,\n verticalAlign: 'bottom', // Subtitles positioned at bottom\n } as Layer;\n }\n\n if (layer.type === 'image') {\n const payload = layer.payload as SerializedImageLayerPayload;\n const source = payload.bitmapHandle ?? null;\n if (source) {\n return {\n ...baseLayer,\n type: 'image',\n source,\n } as Layer;\n }\n }\n\n return baseLayer;\n}\n\n/**\n * VideoComposeWorker - Visual composition in the pipeline\n * Receives decoded video frames and outputs composed frames\n *\n * Pipeline: DecodeWorker → VideoComposeWorker → EncodeWorker\n *\n * Features:\n * - Multi-layer composition with Canvas2D/WebGL\n * - Transition effects and filters\n * - Text and image overlay support\n * - Stream-based processing with configurable backpressure\n */\nexport class VideoComposeWorker {\n private channel: WorkerChannel;\n private composer: VideoComposer | null = null;\n private composeStream: TransformStream<ComposeRequest, VideoFrame> | null = null;\n\n private downstreamPorts = new Map<string, MessagePort>();\n private upstreamPorts = new Map<string, MessagePort>();\n private instructionRegistry = new Map<string, ClipInstructionSet>();\n private pendingReplay = new Map<string, { startUs: number; endUs: number; revision: number }>();\n private streamState = new Map<string, StreamState>();\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as any, {\n name: 'VideoComposeWorker',\n timeout: 30000,\n });\n\n this.setupHandlers();\n }\n\n private setupHandlers(): void {\n // Register message handlers\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n this.channel.registerHandler('connect' as any, this.handleConnect.bind(this));\n this.channel.registerHandler('flush', this.handleFlush.bind(this));\n // this.channel.registerHandler('get_stream', this.handleGetStream.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler('install_instructions', this.handleInstallInstructions.bind(this));\n this.channel.registerHandler('sync_clip', this.handleSyncClip.bind(this));\n this.channel.registerHandler('dispose_clip', this.handleDisposeClip.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n }\n\n /**\n * Unified connect handler used by stream pipeline\n */\n private async handleConnect(payload: {\n direction: 'upstream' | 'downstream';\n port: MessagePort;\n streamType: 'video' | 'audio' | 'frame' | 'chunk';\n clipId?: string;\n }): Promise<{ success: boolean }> {\n const { port, direction, clipId = 'default' } = payload;\n if (direction === 'upstream') {\n this.upstreamPorts.set(clipId, port);\n const channel = new WorkerChannel(port, {\n name: 'VideoCompose-Decode',\n timeout: 30000,\n });\n channel.receiveStream(this.handleReceiveStream.bind(this));\n }\n if (direction === 'downstream') {\n this.downstreamPorts.set(clipId, port);\n }\n return { success: true };\n }\n\n /**\n * Configure composer\n * According to docs/impl/14-config, only reinitialize when initial=true\n */\n private async handleConfigure(payload: {\n config: VideoComposeConfig;\n initial?: boolean;\n }): Promise<{\n success: boolean;\n config: VideoComposeConfig;\n }> {\n const { config, initial } = payload;\n\n const hasValidDimensions = config.width > 0 && config.height > 0;\n const hasValidFps = config.fps > 0;\n\n if (!hasValidDimensions || !hasValidFps) {\n throw new Error(\n `VideoComposeWorker: invalid canvas config width=${config.width}, height=${config.height}, fps=${config.fps}`\n );\n }\n\n // Set worker state to ready on initial configuration\n if (initial) {\n this.channel.state = WorkerState.Ready;\n }\n\n if (initial || !this.composer) {\n // Initial configuration or composer doesn't exist\n if (this.composer) {\n this.composer.dispose();\n this.composeStream = null;\n }\n\n // Create new composer\n this.composer = new VideoComposer(config);\n } else {\n // Just update configuration\n this.composer.updateConfig(config);\n }\n\n // Notify configuration complete\n this.channel.notify('configured', {\n config: this.composer.config,\n initialized: initial || false,\n });\n\n return {\n success: true,\n config: this.composer.config,\n };\n }\n\n private async handleReceiveStream(\n stream: ReadableStream,\n metadata?: Record<string, any>\n ): Promise<void> {\n const { clipId = 'default' } = metadata || {};\n\n if (!this.composer) {\n console.error('[VideoComposeWorker] Composer not configured');\n return;\n }\n\n const instruction = this.instructionRegistry.get(clipId);\n if (!instruction) {\n console.warn('[VideoComposeWorker] No instructions for clip', clipId);\n return;\n }\n\n // TODO: ENCODE\n // const downstreamPort = this.downstreamPorts.get(clipId);\n // if (downstreamPort) {\n // const channel = new WorkerChannel(downstreamPort, {\n // name: 'VideoCompose-Encoder',\n // timeout: 30000,\n // });\n // channel.sendStream(encodeStream, {\n // streamType: 'video',\n // ...metadata,\n // });\n // }\n\n const filteredStream = stream.pipeThrough(\n new TransformStream<any, ComposeRequest>({\n transform: (wrappedFrame, controller) => {\n try {\n // Extract frame and metadata from wrapped object\n const frame = wrappedFrame.frame || wrappedFrame;\n const gopSerial = wrappedFrame.gopSerial;\n const isKeyframe = wrappedFrame.isKeyframe;\n\n const timestamp = frame.timestamp ?? 0;\n if (this.shouldSkipFrame(clipId, timestamp)) {\n frame.close();\n return;\n }\n\n const request = this.buildComposeRequest(clipId, instruction, frame, timestamp);\n if (!request) {\n frame.close();\n return;\n }\n (request as any).gopSerial = gopSerial;\n (request as any).isKeyframe = isKeyframe;\n controller.enqueue(request);\n } catch (error) {\n const frame = wrappedFrame.frame || wrappedFrame;\n frame?.close?.();\n throw error;\n }\n },\n })\n );\n\n const { composeStream, cacheStream } = this.composer.createStreams();\n this.channel.sendStream(cacheStream, metadata);\n\n filteredStream.pipeTo(composeStream).catch((error) => {\n console.error('[VideoComposeWorker] compose stream error', clipId, error);\n });\n }\n\n // private handleGetStream(): ReadableStream<VideoFrame> | undefined {\n // return this.composer?.createStreams()?.cacheStream;\n // }\n\n /**\n * Flush the composition pipeline\n */\n private async handleFlush(): Promise<{ success: boolean }> {\n try {\n // Flush any pending frames in the stream\n // The stream will handle this automatically\n\n this.channel.notify('flush_complete', {});\n return { success: true };\n } catch (error: any) {\n throw {\n code: 'FLUSH_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Get composer statistics\n */\n private async handleGetStats(): Promise<{\n configured: boolean;\n config?: VideoComposeConfig;\n streaming: boolean;\n }> {\n return {\n configured: this.composer !== null,\n config: this.composer?.config,\n streaming: this.composeStream !== null,\n };\n }\n\n /**\n * Dispose worker and cleanup resources\n */\n private async handleDispose(): Promise<{ success: boolean }> {\n // Dispose composer\n if (this.composer) {\n this.composer.dispose();\n this.composer = null;\n }\n\n this.composeStream = null;\n\n // Close connections\n this.downstreamPorts.get('default')?.close();\n this.upstreamPorts.get('default')?.close();\n this.downstreamPorts.clear();\n this.upstreamPorts.clear();\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n\n private async handleInstallInstructions(data: ClipInstructionSet): Promise<{ success: boolean }> {\n const { clipId, revision } = data;\n const current = this.instructionRegistry.get(clipId);\n if (current && current.revision > revision) {\n return { success: false };\n }\n\n this.instructionRegistry.set(clipId, data);\n return { success: true };\n }\n\n private async handleSyncClip(payload: {\n clipId: string;\n revision: number;\n range: { startUs: number; endUs: number };\n }): Promise<{ success: boolean }> {\n const { clipId, revision, range } = payload;\n const current = this.instructionRegistry.get(clipId);\n if (!current || current.revision > revision) {\n return { success: false };\n }\n\n this.pendingReplay.set(clipId, { ...range, revision });\n this.channel.notify('sync_ack', { clipId, revision });\n return { success: true };\n }\n\n private async handleDisposeClip(payload: { clipId: string }): Promise<{ success: boolean }> {\n const { clipId } = payload;\n this.instructionRegistry.delete(clipId);\n this.pendingReplay.delete(clipId);\n this.downstreamPorts.get(clipId)?.close();\n this.upstreamPorts.get(clipId)?.close();\n this.downstreamPorts.delete(clipId);\n this.upstreamPorts.delete(clipId);\n return { success: true };\n }\n\n /**\n * Check if frame should be skipped (outside dirty range)\n * Returns true if frame is NOT in the dirty range and should use cached version\n */\n private shouldSkipFrame(clipId: string, timestamp: number): boolean {\n const dirtyRange = this.pendingReplay.get(clipId);\n if (!dirtyRange) {\n return false; // No dirty range, don't skip (first render or no updates)\n }\n\n // Frame is within dirty range → don't skip, re-compose\n if (timestamp >= dirtyRange.startUs && timestamp <= dirtyRange.endUs) {\n return false;\n }\n\n // Passed dirty range end → clean up\n if (timestamp > dirtyRange.endUs) {\n this.pendingReplay.delete(clipId);\n }\n\n // Frame outside dirty range → skip, use cache\n return true;\n }\n\n private buildComposeRequest(\n clipId: string,\n instruction: ClipInstructionSet,\n frame: VideoFrame,\n _timestamp: number\n ): ComposeRequest | null {\n const normalizedTime = this.computeTimelineTimestamp(clipId, frame, instruction.baseConfig);\n const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;\n const clipDurationUs = instruction.baseConfig.timeline?.clipDurationUs ?? Infinity;\n const clipEndUs = clipStartUs + clipDurationUs;\n\n // Check if frame is within clip boundary\n if (normalizedTime < clipStartUs || normalizedTime >= clipEndUs) {\n return null;\n }\n\n const clipRelativeTime = normalizedTime - clipStartUs;\n const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime, frame);\n\n if (!activeLayers.length) {\n return null;\n }\n const layers = activeLayers.map((layer) => materializeLayer(layer, frame));\n return {\n timeUs: normalizedTime,\n layers,\n transition: VideoComposeWorker.buildTransition(\n instruction.transitions,\n clipRelativeTime,\n instruction.baseConfig.timeline\n ),\n };\n }\n\n private static buildTransition(\n transitions: SerializedTransitionPlan[],\n timeUs: TimeUs,\n _timeline?: VideoComposeConfig['timeline']\n ) {\n const entry = transitions.find((transition) => {\n const { startUs, endUs } = transition.range;\n return timeUs >= startUs && timeUs < endUs;\n });\n if (!entry) {\n return undefined;\n }\n const durationUs = entry.range.endUs - entry.range.startUs;\n const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;\n return {\n type: entry.params.type,\n progress: Math.min(Math.max(progress, 0), 1),\n easing: entry.params.easing,\n params: entry.params.payload,\n direction: entry.params.payload?.direction,\n } as any;\n }\n\n private computeTimelineTimestamp(\n clipId: string,\n frame: VideoFrame,\n config: VideoComposeConfig\n ): TimeUs {\n const key = clipId;\n let state = this.streamState.get(key);\n if (!state) {\n state = {\n baseTimestamp: null,\n lastSourceTimestamp: null,\n nextFrameIndex: 0,\n };\n this.streamState.set(key, state);\n }\n\n const timeline = config.timeline;\n if (!timeline) {\n const ts = frame.timestamp ?? 0;\n state.lastSourceTimestamp = frame.timestamp ?? null;\n return ts;\n }\n\n const { clipStartUs, compositionFps } = timeline;\n const sourceTimestamp = frame.timestamp ?? null;\n\n // Detect stream reset (e.g., after seek)\n if (\n sourceTimestamp !== null &&\n state.lastSourceTimestamp !== null &&\n sourceTimestamp < state.lastSourceTimestamp\n ) {\n state.baseTimestamp = null;\n state.nextFrameIndex = 0;\n }\n\n // Initialize base timestamp (should be 0 or close to 0 after demuxer normalization)\n if (state.baseTimestamp === null) {\n state.baseTimestamp = sourceTimestamp ?? 0;\n state.nextFrameIndex = 0;\n\n // Warn if base is not near zero (indicates demuxer normalization issue)\n if (state.baseTimestamp > 1000) {\n console.warn(\n `[VideoComposeWorker] First frame timestamp is ${state.baseTimestamp}us, expected ~0. ` +\n `Check MP4Demuxer normalization.`\n );\n }\n }\n\n // Since demuxer normalizes to 0, we can directly calculate frame index\n const frameDuration = frameDurationFromFps(compositionFps);\n let frameIndex = state.nextFrameIndex;\n\n if (sourceTimestamp !== null) {\n // Calculate frame index from normalized timestamp\n const approxIndex = frameIndexFromTimestamp(\n state.baseTimestamp,\n sourceTimestamp,\n compositionFps,\n 'nearest'\n );\n frameIndex = Math.max(frameIndex, approxIndex);\n }\n\n // Map to timeline position\n const rawTimeline = clipStartUs + frameIndex * frameDuration;\n const timelineTime = quantizeTimestampToFrame(\n rawTimeline,\n clipStartUs,\n compositionFps,\n 'nearest'\n );\n\n state.nextFrameIndex = frameIndex + 1;\n state.lastSourceTimestamp = sourceTimestamp;\n\n return timelineTime;\n }\n}\n\ninterface StreamState {\n baseTimestamp: number | null;\n lastSourceTimestamp: number | null;\n nextFrameIndex: number;\n}\n\n// Initialize worker\nconst worker = new VideoComposeWorker();\n\n// Handle worker termination\nself.addEventListener('beforeunload', () => {\n worker['handleDispose']();\n});\n\nexport default null; // Required for TypeScript worker compilation\n"],"names":[],"mappings":"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"}
@@ -1,5 +0,0 @@
1
- const videoComposeWorkerUrl = "" + new URL("../../assets/video-compose.worker-DPzsC21d.js", import.meta.url).href;
2
- export {
3
- videoComposeWorkerUrl as default
4
- };
5
- //# sourceMappingURL=video-compose.worker2.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"video-compose.worker2.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -1,82 +0,0 @@
1
- import { BaseDecoder } from "./BaseDecoder.js";
2
- class AudioChunkDecoder extends BaseDecoder {
3
- // Default values
4
- static DEFAULT_HIGH_WATER_MARK = 20;
5
- static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
6
- // Exposed properties
7
- trackId;
8
- // Backpressure configuration
9
- highWaterMark;
10
- decodeQueueThreshold;
11
- constructor(trackId, config) {
12
- super(config);
13
- this.trackId = trackId;
14
- this.highWaterMark = config?.backpressure?.highWaterMark ?? AudioChunkDecoder.DEFAULT_HIGH_WATER_MARK;
15
- this.decodeQueueThreshold = AudioChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
16
- }
17
- // Computed properties
18
- get isConfigured() {
19
- return this.isReady;
20
- }
21
- get state() {
22
- return this.decoder?.state || "unconfigured";
23
- }
24
- /**
25
- * Update configuration - can be called before or after initialization
26
- */
27
- async updateConfig(config) {
28
- if (!this.isReady && config.codec) {
29
- await this.configure(config);
30
- await this.processBufferedChunks();
31
- return;
32
- }
33
- }
34
- // Implement abstract methods
35
- async isConfigSupported(config) {
36
- const result = await AudioDecoder.isConfigSupported({
37
- codec: config.codec,
38
- sampleRate: config.sampleRate,
39
- numberOfChannels: config.numberOfChannels
40
- });
41
- return { supported: result.supported ?? false };
42
- }
43
- createDecoder(init) {
44
- return new AudioDecoder(init);
45
- }
46
- getDecoderType() {
47
- return "Audio";
48
- }
49
- async configureDecoder(config) {
50
- if (!this.decoder) return;
51
- await this.decoder.configure({
52
- codec: config.codec,
53
- sampleRate: config.sampleRate,
54
- numberOfChannels: config.numberOfChannels,
55
- ...config.description && { description: config.description }
56
- });
57
- }
58
- decode(chunk) {
59
- this.decoder?.decode(chunk);
60
- }
61
- /**
62
- * Configure the decoder with codec info (can be called after creation)
63
- */
64
- async configure(config) {
65
- if (this.isReady) {
66
- await this.reconfigure(config);
67
- return;
68
- }
69
- this.config = config;
70
- await this.initialize();
71
- }
72
- /**
73
- * Process any buffered chunks after configuration
74
- * Note: Audio doesn't buffer in current implementation, but keeping for interface consistency
75
- */
76
- async processBufferedChunks() {
77
- }
78
- }
79
- export {
80
- AudioChunkDecoder
81
- };
82
- //# sourceMappingURL=AudioChunkDecoder.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AudioChunkDecoder.js","sources":["../../../src/stages/decode/AudioChunkDecoder.ts"],"sourcesContent":["import { AudioDecoderConfig } from './types';\nimport { BaseDecoder } from './BaseDecoder';\n\n/**\n * Audio decoder with streaming support\n * Extends BaseDecoder for common WebCodecs operations\n */\nexport class AudioChunkDecoder extends BaseDecoder<\n AudioDecoder,\n AudioDecoderConfig,\n EncodedAudioChunk,\n AudioData\n> {\n // Default values\n private static readonly DEFAULT_HIGH_WATER_MARK = 20;\n private static readonly DEFAULT_DECODE_QUEUE_THRESHOLD = 16;\n\n // Exposed properties\n readonly trackId: string;\n\n // Backpressure configuration\n protected readonly highWaterMark: number;\n protected readonly decodeQueueThreshold: number;\n\n constructor(trackId: string, config?: Partial<AudioDecoderConfig>) {\n // Initialize with empty config, will be configured later\n super(config as AudioDecoderConfig);\n\n this.trackId = trackId;\n\n // Set backpressure configuration\n this.highWaterMark =\n config?.backpressure?.highWaterMark ?? AudioChunkDecoder.DEFAULT_HIGH_WATER_MARK;\n this.decodeQueueThreshold = AudioChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;\n }\n\n // Computed properties\n get isConfigured(): boolean {\n return this.isReady;\n }\n\n get state(): string {\n return this.decoder?.state || 'unconfigured';\n }\n\n /**\n * Update configuration - can be called before or after initialization\n */\n async updateConfig(config: Partial<AudioDecoderConfig>): Promise<void> {\n // If decoder is not ready and we have codec info, configure it\n if (!this.isReady && config.codec) {\n await this.configure(config as AudioDecoderConfig);\n await this.processBufferedChunks();\n return;\n }\n\n // Note: AudioDecoder doesn't have many runtime-configurable options\n // Backpressure settings are readonly in this implementation\n // if (config.backpressure) {\n // console.warn('Backpressure settings cannot be changed at runtime');\n // }\n }\n\n // Implement abstract methods\n protected async isConfigSupported(config: AudioDecoderConfig): Promise<{ supported: boolean }> {\n const result = await AudioDecoder.isConfigSupported({\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n });\n return { supported: result.supported ?? false };\n }\n\n protected createDecoder(init: {\n output: (data: AudioData) => void;\n error: (error: DOMException) => void;\n }): AudioDecoder {\n return new AudioDecoder(init);\n }\n\n protected getDecoderType(): string {\n return 'Audio';\n }\n\n protected async configureDecoder(config: AudioDecoderConfig): Promise<void> {\n if (!this.decoder) return;\n\n await this.decoder.configure({\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n ...(config.description && { description: config.description }),\n });\n }\n\n protected decode(chunk: EncodedAudioChunk): void {\n this.decoder?.decode(chunk);\n }\n\n /**\n * Configure the decoder with codec info (can be called after creation)\n */\n async configure(config: AudioDecoderConfig): Promise<void> {\n if (this.isReady) {\n // If already configured, reconfigure\n await this.reconfigure(config);\n return;\n }\n\n this.config = config as any;\n\n // Initialize decoder with new config\n await this.initialize();\n }\n\n /**\n * Process any buffered chunks after configuration\n * Note: Audio doesn't buffer in current implementation, but keeping for interface consistency\n */\n async processBufferedChunks(): Promise<void> {\n // No-op for audio in current implementation\n }\n}\n"],"names":[],"mappings":";AAOO,MAAM,0BAA0B,YAKrC;AAAA;AAAA,EAEA,OAAwB,0BAA0B;AAAA,EAClD,OAAwB,iCAAiC;AAAA;AAAA,EAGhD;AAAA;AAAA,EAGU;AAAA,EACA;AAAA,EAEnB,YAAY,SAAiB,QAAsC;AAEjE,UAAM,MAA4B;AAElC,SAAK,UAAU;AAGf,SAAK,gBACH,QAAQ,cAAc,iBAAiB,kBAAkB;AAC3D,SAAK,uBAAuB,kBAAkB;AAAA,EAChD;AAAA;AAAA,EAGA,IAAI,eAAwB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,QAAgB;AAClB,WAAO,KAAK,SAAS,SAAS;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,QAAoD;AAErE,QAAI,CAAC,KAAK,WAAW,OAAO,OAAO;AACjC,YAAM,KAAK,UAAU,MAA4B;AACjD,YAAM,KAAK,sBAAA;AACX;AAAA,IACF;AAAA,EAOF;AAAA;AAAA,EAGA,MAAgB,kBAAkB,QAA6D;AAC7F,UAAM,SAAS,MAAM,aAAa,kBAAkB;AAAA,MAClD,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,kBAAkB,OAAO;AAAA,IAAA,CAC1B;AACD,WAAO,EAAE,WAAW,OAAO,aAAa,MAAA;AAAA,EAC1C;AAAA,EAEU,cAAc,MAGP;AACf,WAAO,IAAI,aAAa,IAAI;AAAA,EAC9B;AAAA,EAEU,iBAAyB;AACjC,WAAO;AAAA,EACT;AAAA,EAEA,MAAgB,iBAAiB,QAA2C;AAC1E,QAAI,CAAC,KAAK,QAAS;AAEnB,UAAM,KAAK,QAAQ,UAAU;AAAA,MAC3B,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,kBAAkB,OAAO;AAAA,MACzB,GAAI,OAAO,eAAe,EAAE,aAAa,OAAO,YAAA;AAAA,IAAY,CAC7D;AAAA,EACH;AAAA,EAEU,OAAO,OAAgC;AAC/C,SAAK,SAAS,OAAO,KAAK;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,QAA2C;AACzD,QAAI,KAAK,SAAS;AAEhB,YAAM,KAAK,YAAY,MAAM;AAC7B;AAAA,IACF;AAEA,SAAK,SAAS;AAGd,UAAM,KAAK,WAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,wBAAuC;AAAA,EAE7C;AACF;"}
@@ -1,130 +0,0 @@
1
- class BaseDecoder {
2
- decoder;
3
- config;
4
- controller = null;
5
- constructor(config) {
6
- this.config = config;
7
- }
8
- async initialize() {
9
- if (this.decoder?.state === "configured") {
10
- return;
11
- }
12
- const isSupported = await this.isConfigSupported(this.config);
13
- if (!isSupported.supported) {
14
- throw new Error(
15
- `Codec not supported: ${this.config.codecString || this.config.codec}`
16
- );
17
- }
18
- this.decoder = this.createDecoder({
19
- output: this.handleOutput.bind(this),
20
- error: this.handleError.bind(this)
21
- });
22
- await this.configureDecoder(this.config);
23
- }
24
- async reconfigure(config) {
25
- this.config = { ...this.config, ...config };
26
- if (!this.decoder) {
27
- await this.initialize();
28
- return;
29
- }
30
- if (this.decoder.state === "configured") {
31
- await this.decoder.flush();
32
- }
33
- const isSupported = await this.isConfigSupported(this.config);
34
- if (!isSupported.supported) {
35
- throw new Error(
36
- `New configuration not supported: ${this.config.codecString || this.config.codec}`
37
- );
38
- }
39
- await this.configureDecoder(this.config);
40
- }
41
- async flush() {
42
- if (!this.decoder) return;
43
- await this.decoder.flush();
44
- }
45
- async reset() {
46
- if (!this.decoder) return;
47
- this.decoder.reset();
48
- this.onReset();
49
- }
50
- async close() {
51
- if (!this.decoder) return;
52
- if (this.decoder.state === "configured") {
53
- await this.decoder.flush();
54
- }
55
- this.decoder.close();
56
- this.decoder = void 0;
57
- }
58
- get isReady() {
59
- return this.decoder?.state === "configured";
60
- }
61
- get queueSize() {
62
- return this.decoder?.decodeQueueSize ?? 0;
63
- }
64
- handleOutput(data) {
65
- if (!this.controller) {
66
- this.closeIfPossible(data);
67
- return;
68
- }
69
- try {
70
- this.controller.enqueue(data);
71
- } catch (error) {
72
- if (error instanceof TypeError && /Cannot enqueue a chunk into a readable stream that is closed/.test(error.message)) {
73
- this.closeIfPossible(data);
74
- return;
75
- }
76
- throw error;
77
- }
78
- }
79
- handleError(error) {
80
- console.error(`${this.getDecoderType()} decoder error:`, error);
81
- this.controller?.error(error);
82
- }
83
- closeIfPossible(data) {
84
- data?.close?.();
85
- data?.frame?.close?.();
86
- }
87
- onReset() {
88
- }
89
- createStream() {
90
- return new TransformStream(
91
- {
92
- start: async (controller) => {
93
- this.controller = controller;
94
- if (!this.isReady) {
95
- await this.initialize();
96
- }
97
- },
98
- transform: async (input) => {
99
- if (!this.decoder || this.decoder.state !== "configured") {
100
- throw new Error("Decoder not configured");
101
- }
102
- if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
103
- await new Promise((resolve) => {
104
- const check = () => {
105
- if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
106
- resolve();
107
- } else {
108
- setTimeout(check, 10);
109
- }
110
- };
111
- check();
112
- });
113
- }
114
- this.decode(input);
115
- },
116
- flush: async () => {
117
- await this.flush();
118
- }
119
- },
120
- {
121
- highWaterMark: this.highWaterMark,
122
- size: () => 1
123
- }
124
- );
125
- }
126
- }
127
- export {
128
- BaseDecoder
129
- };
130
- //# sourceMappingURL=BaseDecoder.js.map