@meframe/core 0.0.1 → 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 (159) hide show
  1. package/README.md +17 -4
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +2 -4
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/cache/CacheManager.d.ts.map +1 -1
  6. package/dist/cache/CacheManager.js +8 -1
  7. package/dist/cache/CacheManager.js.map +1 -1
  8. package/dist/config/defaults.d.ts.map +1 -1
  9. package/dist/config/defaults.js +2 -9
  10. package/dist/config/defaults.js.map +1 -1
  11. package/dist/config/types.d.ts +3 -4
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/controllers/PlaybackController.d.ts +4 -2
  14. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  15. package/dist/controllers/PlaybackController.js +7 -13
  16. package/dist/controllers/PlaybackController.js.map +1 -1
  17. package/dist/controllers/PreRenderService.d.ts +3 -2
  18. package/dist/controllers/PreRenderService.d.ts.map +1 -1
  19. package/dist/controllers/PreRenderService.js.map +1 -1
  20. package/dist/controllers/PreviewHandle.d.ts +2 -0
  21. package/dist/controllers/PreviewHandle.d.ts.map +1 -1
  22. package/dist/controllers/PreviewHandle.js +6 -0
  23. package/dist/controllers/PreviewHandle.js.map +1 -1
  24. package/dist/controllers/index.d.ts +1 -1
  25. package/dist/controllers/index.d.ts.map +1 -1
  26. package/dist/controllers/types.d.ts +2 -12
  27. package/dist/controllers/types.d.ts.map +1 -1
  28. package/dist/event/events.d.ts +5 -59
  29. package/dist/event/events.d.ts.map +1 -1
  30. package/dist/event/events.js +1 -6
  31. package/dist/event/events.js.map +1 -1
  32. package/dist/model/CompositionModel.js +1 -2
  33. package/dist/model/CompositionModel.js.map +1 -1
  34. package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
  35. package/dist/orchestrator/CompositionPlanner.js +1 -0
  36. package/dist/orchestrator/CompositionPlanner.js.map +1 -1
  37. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  38. package/dist/orchestrator/Orchestrator.js +3 -13
  39. package/dist/orchestrator/Orchestrator.js.map +1 -1
  40. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  41. package/dist/orchestrator/VideoClipSession.js +4 -5
  42. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  43. package/dist/orchestrator/types.d.ts +1 -1
  44. package/dist/orchestrator/types.d.ts.map +1 -1
  45. package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
  46. package/dist/stages/compose/GlobalAudioSession.js +3 -2
  47. package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
  48. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  49. package/dist/stages/compose/types.d.ts +3 -1
  50. package/dist/stages/compose/types.d.ts.map +1 -1
  51. package/dist/stages/decode/AudioChunkDecoder.d.ts.map +1 -1
  52. package/dist/stages/decode/VideoChunkDecoder.d.ts +0 -1
  53. package/dist/stages/decode/VideoChunkDecoder.d.ts.map +1 -1
  54. package/dist/stages/demux/MP4Demuxer.d.ts +2 -1
  55. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  56. package/dist/stages/load/EventHandlers.d.ts +2 -11
  57. package/dist/stages/load/EventHandlers.d.ts.map +1 -1
  58. package/dist/stages/load/EventHandlers.js +1 -24
  59. package/dist/stages/load/EventHandlers.js.map +1 -1
  60. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  61. package/dist/stages/load/ResourceLoader.js +11 -13
  62. package/dist/stages/load/ResourceLoader.js.map +1 -1
  63. package/dist/stages/load/TaskManager.d.ts +1 -1
  64. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  65. package/dist/stages/load/TaskManager.js +3 -2
  66. package/dist/stages/load/TaskManager.js.map +1 -1
  67. package/dist/stages/load/types.d.ts +2 -0
  68. package/dist/stages/load/types.d.ts.map +1 -1
  69. package/dist/utils/time-utils.d.ts +3 -2
  70. package/dist/utils/time-utils.d.ts.map +1 -1
  71. package/dist/utils/time-utils.js +2 -1
  72. package/dist/utils/time-utils.js.map +1 -1
  73. package/dist/vite-plugin.d.ts +19 -0
  74. package/dist/vite-plugin.d.ts.map +1 -0
  75. package/dist/vite-plugin.js +145 -0
  76. package/dist/vite-plugin.js.map +1 -0
  77. package/dist/worker/WorkerPool.d.ts +7 -4
  78. package/dist/worker/WorkerPool.d.ts.map +1 -1
  79. package/dist/worker/WorkerPool.js +29 -18
  80. package/dist/worker/WorkerPool.js.map +1 -1
  81. package/dist/{stages/demux → workers}/MP4Demuxer.js +17 -15
  82. package/dist/workers/MP4Demuxer.js.map +1 -0
  83. package/dist/workers/WorkerChannel.js +486 -0
  84. package/dist/workers/WorkerChannel.js.map +1 -0
  85. package/dist/workers/mp4box.all.js +7049 -0
  86. package/dist/workers/mp4box.all.js.map +1 -0
  87. package/dist/workers/stages/compose/audio-compose.worker.js +1063 -0
  88. package/dist/workers/stages/compose/audio-compose.worker.js.map +1 -0
  89. package/dist/workers/stages/compose/video-compose.worker.js +1209 -0
  90. package/dist/workers/stages/compose/video-compose.worker.js.map +1 -0
  91. package/dist/{stages → workers/stages}/decode/decode.worker.js +401 -20
  92. package/dist/workers/stages/decode/decode.worker.js.map +1 -0
  93. package/dist/{stages → workers/stages}/demux/audio-demux.worker.js +184 -4
  94. package/dist/workers/stages/demux/audio-demux.worker.js.map +1 -0
  95. package/dist/{stages → workers/stages}/demux/video-demux.worker.js +7 -30
  96. package/dist/workers/stages/demux/video-demux.worker.js.map +1 -0
  97. package/dist/{stages → workers/stages}/encode/encode.worker.js +238 -5
  98. package/dist/workers/stages/encode/encode.worker.js.map +1 -0
  99. package/dist/{stages/mux/MP4Muxer.js → workers/stages/mux/mux.worker.js} +244 -5
  100. package/dist/workers/stages/mux/mux.worker.js.map +1 -0
  101. package/package.json +27 -21
  102. package/dist/model/types.js +0 -5
  103. package/dist/model/types.js.map +0 -1
  104. package/dist/plugins/BackpressureMonitor.js +0 -62
  105. package/dist/plugins/BackpressureMonitor.js.map +0 -1
  106. package/dist/stages/compose/AudioDucker.js +0 -161
  107. package/dist/stages/compose/AudioDucker.js.map +0 -1
  108. package/dist/stages/compose/AudioMixer.js +0 -373
  109. package/dist/stages/compose/AudioMixer.js.map +0 -1
  110. package/dist/stages/compose/FilterProcessor.js +0 -226
  111. package/dist/stages/compose/FilterProcessor.js.map +0 -1
  112. package/dist/stages/compose/LayerRenderer.js +0 -215
  113. package/dist/stages/compose/LayerRenderer.js.map +0 -1
  114. package/dist/stages/compose/TransitionProcessor.js +0 -189
  115. package/dist/stages/compose/TransitionProcessor.js.map +0 -1
  116. package/dist/stages/compose/VideoComposer.js +0 -186
  117. package/dist/stages/compose/VideoComposer.js.map +0 -1
  118. package/dist/stages/compose/audio-compose.worker.d.ts +0 -79
  119. package/dist/stages/compose/audio-compose.worker.d.ts.map +0 -1
  120. package/dist/stages/compose/audio-compose.worker.js +0 -541
  121. package/dist/stages/compose/audio-compose.worker.js.map +0 -1
  122. package/dist/stages/compose/video-compose.worker.d.ts +0 -60
  123. package/dist/stages/compose/video-compose.worker.d.ts.map +0 -1
  124. package/dist/stages/compose/video-compose.worker.js +0 -369
  125. package/dist/stages/compose/video-compose.worker.js.map +0 -1
  126. package/dist/stages/decode/AudioChunkDecoder.js +0 -83
  127. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  128. package/dist/stages/decode/BaseDecoder.js +0 -130
  129. package/dist/stages/decode/BaseDecoder.js.map +0 -1
  130. package/dist/stages/decode/VideoChunkDecoder.js +0 -209
  131. package/dist/stages/decode/VideoChunkDecoder.js.map +0 -1
  132. package/dist/stages/decode/decode.worker.d.ts +0 -70
  133. package/dist/stages/decode/decode.worker.d.ts.map +0 -1
  134. package/dist/stages/decode/decode.worker.js.map +0 -1
  135. package/dist/stages/demux/MP3FrameParser.js +0 -186
  136. package/dist/stages/demux/MP3FrameParser.js.map +0 -1
  137. package/dist/stages/demux/MP4Demuxer.js.map +0 -1
  138. package/dist/stages/demux/audio-demux.worker.d.ts +0 -51
  139. package/dist/stages/demux/audio-demux.worker.d.ts.map +0 -1
  140. package/dist/stages/demux/audio-demux.worker.js.map +0 -1
  141. package/dist/stages/demux/video-demux.worker.d.ts +0 -48
  142. package/dist/stages/demux/video-demux.worker.d.ts.map +0 -1
  143. package/dist/stages/demux/video-demux.worker.js.map +0 -1
  144. package/dist/stages/encode/AudioChunkEncoder.js +0 -37
  145. package/dist/stages/encode/AudioChunkEncoder.js.map +0 -1
  146. package/dist/stages/encode/BaseEncoder.js +0 -164
  147. package/dist/stages/encode/BaseEncoder.js.map +0 -1
  148. package/dist/stages/encode/VideoChunkEncoder.js +0 -50
  149. package/dist/stages/encode/VideoChunkEncoder.js.map +0 -1
  150. package/dist/stages/encode/encode.worker.d.ts +0 -3
  151. package/dist/stages/encode/encode.worker.d.ts.map +0 -1
  152. package/dist/stages/encode/encode.worker.js.map +0 -1
  153. package/dist/stages/mux/MP4Muxer.js.map +0 -1
  154. package/dist/stages/mux/mux.worker.d.ts +0 -65
  155. package/dist/stages/mux/mux.worker.d.ts.map +0 -1
  156. package/dist/stages/mux/mux.worker.js +0 -219
  157. package/dist/stages/mux/mux.worker.js.map +0 -1
  158. package/dist/stages/mux/utils.js +0 -34
  159. package/dist/stages/mux/utils.js.map +0 -1
@@ -0,0 +1,1209 @@
1
+ import { W as WorkerChannel, a as WorkerMessageType, b as WorkerState } from "../../WorkerChannel.js";
2
+ const MICROSECONDS_PER_SECOND = 1e6;
3
+ const DEFAULT_FPS = 30;
4
+ function normalizeFps(value) {
5
+ if (!Number.isFinite(value) || value <= 0) {
6
+ return DEFAULT_FPS;
7
+ }
8
+ return value;
9
+ }
10
+ function frameDurationFromFps(fps) {
11
+ const normalized = normalizeFps(fps);
12
+ const duration = MICROSECONDS_PER_SECOND / normalized;
13
+ return Math.max(Math.round(duration), 1);
14
+ }
15
+ function frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy = "nearest") {
16
+ const frameDurationUs = frameDurationFromFps(fps);
17
+ if (frameDurationUs <= 0) {
18
+ return 0;
19
+ }
20
+ const delta = timestampUs - baseTimestampUs;
21
+ const rawIndex = delta / frameDurationUs;
22
+ if (!Number.isFinite(rawIndex)) {
23
+ return 0;
24
+ }
25
+ switch (strategy) {
26
+ case "floor":
27
+ return Math.floor(rawIndex);
28
+ case "ceil":
29
+ return Math.ceil(rawIndex);
30
+ default:
31
+ return Math.round(rawIndex);
32
+ }
33
+ }
34
+ function quantizeTimestampToFrame(timestampUs, baseTimestampUs, fps, strategy = "nearest") {
35
+ const frameDurationUs = frameDurationFromFps(fps);
36
+ const index = frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy);
37
+ return baseTimestampUs + index * frameDurationUs;
38
+ }
39
+ class LayerRenderer {
40
+ ctx;
41
+ width;
42
+ height;
43
+ constructor(ctx, width, height) {
44
+ this.ctx = ctx;
45
+ this.width = width;
46
+ this.height = height;
47
+ this.ensureHighQualityRendering();
48
+ }
49
+ ensureHighQualityRendering() {
50
+ this.ctx.imageSmoothingEnabled = true;
51
+ this.ctx.imageSmoothingQuality = "high";
52
+ }
53
+ /**
54
+ * Render a single layer with all its properties
55
+ */
56
+ async renderLayer(layer) {
57
+ if (!layer.visible || layer.opacity <= 0) return;
58
+ this.ctx.save();
59
+ try {
60
+ this.ensureHighQualityRendering();
61
+ this.ctx.globalAlpha = layer.opacity;
62
+ if (layer.blendMode) {
63
+ this.ctx.globalCompositeOperation = layer.blendMode;
64
+ }
65
+ if (layer.transform) {
66
+ this.applyTransform(layer.transform);
67
+ }
68
+ switch (layer.type) {
69
+ case "video":
70
+ await this.renderVideoLayer(layer);
71
+ break;
72
+ case "image":
73
+ await this.renderImageLayer(layer);
74
+ break;
75
+ case "text":
76
+ await this.renderTextLayer(layer);
77
+ break;
78
+ }
79
+ if (layer.mask) {
80
+ this.applyMask(layer.mask);
81
+ }
82
+ } finally {
83
+ this.ctx.restore();
84
+ }
85
+ }
86
+ applyTransform(transform) {
87
+ const centerX = this.width * (transform.anchorX ?? 0.5);
88
+ const centerY = this.height * (transform.anchorY ?? 0.5);
89
+ this.ctx.translate(transform.x + centerX, transform.y + centerY);
90
+ if (transform.rotation) {
91
+ this.ctx.rotate(transform.rotation);
92
+ }
93
+ this.ctx.scale(transform.scaleX, transform.scaleY);
94
+ if (transform.skewX || transform.skewY) {
95
+ this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);
96
+ }
97
+ this.ctx.translate(-centerX, -centerY);
98
+ }
99
+ async renderVideoLayer(layer) {
100
+ const { videoFrame, crop } = layer;
101
+ const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;
102
+ const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;
103
+ const scaleX = this.width / videoWidth;
104
+ const scaleY = this.height / videoHeight;
105
+ const scale = Math.min(scaleX, scaleY);
106
+ const renderWidth = Math.round(videoWidth * scale);
107
+ const renderHeight = Math.round(videoHeight * scale);
108
+ const renderX = Math.round((this.width - renderWidth) / 2);
109
+ const renderY = Math.round((this.height - renderHeight) / 2);
110
+ if (crop) {
111
+ this.ctx.drawImage(
112
+ videoFrame,
113
+ crop.x,
114
+ crop.y,
115
+ crop.width,
116
+ crop.height,
117
+ renderX,
118
+ renderY,
119
+ renderWidth,
120
+ renderHeight
121
+ );
122
+ } else {
123
+ this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);
124
+ }
125
+ }
126
+ async renderImageLayer(layer) {
127
+ const { source, crop } = layer;
128
+ if (source instanceof ImageData) {
129
+ if (crop) {
130
+ const tempCanvas = new OffscreenCanvas(crop.width, crop.height);
131
+ const tempCtx = tempCanvas.getContext("2d");
132
+ tempCtx.putImageData(source, -crop.x, -crop.y);
133
+ this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);
134
+ } else {
135
+ this.ctx.putImageData(source, 0, 0);
136
+ }
137
+ } else {
138
+ if (!source) {
139
+ return;
140
+ }
141
+ if (crop) {
142
+ this.ctx.drawImage(
143
+ source,
144
+ crop.x,
145
+ crop.y,
146
+ crop.width,
147
+ crop.height,
148
+ 0,
149
+ 0,
150
+ this.width,
151
+ this.height
152
+ );
153
+ } else {
154
+ this.ctx.drawImage(source, 0, 0, this.width, this.height);
155
+ }
156
+ }
157
+ }
158
+ async renderTextLayer(layer) {
159
+ const fontSize = layer.fontSize ?? 16;
160
+ const fontFamily = layer.fontFamily ?? "sans-serif";
161
+ const fontWeight = layer.fontWeight ?? "normal";
162
+ const fontStyle = layer.fontStyle ?? "normal";
163
+ this.ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
164
+ this.ctx.fillStyle = layer.color ?? "#000000";
165
+ this.ctx.textAlign = layer.textAlign ?? "left";
166
+ this.ctx.textBaseline = layer.verticalAlign ?? "top";
167
+ if (layer.letterSpacing && typeof this.ctx.letterSpacing !== "undefined") {
168
+ this.ctx.letterSpacing = `${layer.letterSpacing}px`;
169
+ }
170
+ this.ensureHighQualityRendering();
171
+ const baseX = this.calculateTextX(layer.textAlign);
172
+ const baseY = this.calculateTextY(layer.verticalAlign, fontSize);
173
+ const x = Math.round(baseX) + 0.5;
174
+ const y = Math.round(baseY) + 0.5;
175
+ if (layer.shadow) {
176
+ this.ctx.shadowColor = layer.shadow.color;
177
+ this.ctx.shadowOffsetX = layer.shadow.offsetX;
178
+ this.ctx.shadowOffsetY = layer.shadow.offsetY;
179
+ this.ctx.shadowBlur = layer.shadow.blur;
180
+ }
181
+ if (layer.strokeColor && layer.strokeWidth && layer.strokeWidth > 0) {
182
+ this.drawEnhancedStroke(layer.text, x, y, layer.strokeColor, layer.strokeWidth);
183
+ }
184
+ this.ctx.fillText(layer.text, x, y);
185
+ if (layer.shadow) {
186
+ this.ctx.shadowColor = "transparent";
187
+ this.ctx.shadowOffsetX = 0;
188
+ this.ctx.shadowOffsetY = 0;
189
+ this.ctx.shadowBlur = 0;
190
+ }
191
+ }
192
+ /**
193
+ * Draw enhanced multi-layer stroke for better text visibility
194
+ */
195
+ drawEnhancedStroke(text, x, y, strokeColor, strokeWidth) {
196
+ this.ctx.save();
197
+ this.ctx.strokeStyle = strokeColor;
198
+ this.ctx.lineJoin = "round";
199
+ this.ctx.lineCap = "round";
200
+ this.ctx.miterLimit = 2;
201
+ const layers = [1.1, 1];
202
+ layers.forEach((multiplier) => {
203
+ this.ctx.lineWidth = strokeWidth * multiplier;
204
+ this.ctx.strokeText(text, x, y);
205
+ });
206
+ this.ctx.restore();
207
+ }
208
+ calculateTextX(align) {
209
+ switch (align) {
210
+ case "center":
211
+ return this.width / 2;
212
+ case "right":
213
+ return this.width;
214
+ default:
215
+ return 0;
216
+ }
217
+ }
218
+ calculateTextY(align, fontSize = 16) {
219
+ switch (align) {
220
+ case "middle":
221
+ return this.height / 2;
222
+ case "bottom":
223
+ return this.height * 0.85;
224
+ default:
225
+ return fontSize;
226
+ }
227
+ }
228
+ applyMask(mask) {
229
+ this.ctx.globalCompositeOperation = mask.invert ? "source-out" : "destination-in";
230
+ if (mask.source) {
231
+ this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);
232
+ } else if (mask.shape === "circle") {
233
+ this.ctx.beginPath();
234
+ this.ctx.arc(
235
+ this.width / 2,
236
+ this.height / 2,
237
+ Math.min(this.width, this.height) / 2,
238
+ 0,
239
+ Math.PI * 2
240
+ );
241
+ this.ctx.fill();
242
+ }
243
+ }
244
+ updateDimensions(width, height) {
245
+ this.width = width;
246
+ this.height = height;
247
+ this.ensureHighQualityRendering();
248
+ }
249
+ }
250
+ class TransitionProcessor {
251
+ width;
252
+ height;
253
+ constructor(width, height) {
254
+ this.width = width;
255
+ this.height = height;
256
+ }
257
+ /**
258
+ * Apply transition effect to the canvas context
259
+ * Returns true if transition was applied, false if not needed
260
+ */
261
+ applyTransition(ctx, transition) {
262
+ if (!transition || transition.progress <= 0) return false;
263
+ const progress = this.calculateEasedProgress(transition.progress, transition.easing);
264
+ switch (transition.type) {
265
+ case "fade":
266
+ return this.applyFade(ctx, progress);
267
+ case "slide":
268
+ return this.applySlide(ctx, progress, transition.direction);
269
+ case "wipe":
270
+ return this.applyWipe(ctx, progress, transition.direction);
271
+ case "zoom":
272
+ return this.applyZoom(ctx, progress, transition.direction);
273
+ case "rotate":
274
+ return this.applyRotate(ctx, progress);
275
+ case "dissolve":
276
+ return this.applyDissolve(ctx, progress);
277
+ default:
278
+ return false;
279
+ }
280
+ }
281
+ calculateEasedProgress(progress, easing) {
282
+ switch (easing) {
283
+ case "ease-in":
284
+ return progress * progress;
285
+ case "ease-out":
286
+ return 1 - (1 - progress) * (1 - progress);
287
+ case "ease-in-out":
288
+ return progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
289
+ default:
290
+ return progress;
291
+ }
292
+ }
293
+ applyFade(ctx, progress) {
294
+ ctx.globalAlpha = progress;
295
+ return true;
296
+ }
297
+ applySlide(ctx, progress, direction) {
298
+ const distance = 1 - progress;
299
+ switch (direction) {
300
+ case "left":
301
+ ctx.translate(-this.width * distance, 0);
302
+ break;
303
+ case "right":
304
+ ctx.translate(this.width * distance, 0);
305
+ break;
306
+ case "up":
307
+ ctx.translate(0, -this.height * distance);
308
+ break;
309
+ case "down":
310
+ ctx.translate(0, this.height * distance);
311
+ break;
312
+ default:
313
+ ctx.translate(-this.width * distance, 0);
314
+ }
315
+ return true;
316
+ }
317
+ applyWipe(ctx, progress, direction) {
318
+ ctx.save();
319
+ ctx.beginPath();
320
+ switch (direction) {
321
+ case "left":
322
+ ctx.rect(0, 0, this.width * progress, this.height);
323
+ break;
324
+ case "right":
325
+ ctx.rect(this.width * (1 - progress), 0, this.width * progress, this.height);
326
+ break;
327
+ case "up":
328
+ ctx.rect(0, 0, this.width, this.height * progress);
329
+ break;
330
+ case "down":
331
+ ctx.rect(0, this.height * (1 - progress), this.width, this.height * progress);
332
+ break;
333
+ default:
334
+ ctx.rect(0, 0, this.width * progress, this.height);
335
+ }
336
+ ctx.clip();
337
+ return true;
338
+ }
339
+ applyZoom(ctx, progress, direction) {
340
+ const scale = direction === "out" ? 1 + (1 - progress) : progress;
341
+ const centerX = this.width / 2;
342
+ const centerY = this.height / 2;
343
+ ctx.translate(centerX, centerY);
344
+ ctx.scale(scale, scale);
345
+ ctx.translate(-centerX, -centerY);
346
+ if (direction === "out") {
347
+ ctx.globalAlpha = progress;
348
+ }
349
+ return true;
350
+ }
351
+ applyRotate(ctx, progress) {
352
+ const rotation = (1 - progress) * Math.PI * 2;
353
+ const centerX = this.width / 2;
354
+ const centerY = this.height / 2;
355
+ ctx.translate(centerX, centerY);
356
+ ctx.rotate(rotation);
357
+ ctx.translate(-centerX, -centerY);
358
+ return true;
359
+ }
360
+ applyDissolve(ctx, progress) {
361
+ ctx.globalAlpha = progress;
362
+ ctx.globalCompositeOperation = "multiply";
363
+ return true;
364
+ }
365
+ /**
366
+ * Create a transition mask for advanced effects
367
+ */
368
+ createTransitionMask(transition, canvas) {
369
+ const ctx = canvas.getContext("2d");
370
+ if (!ctx) return null;
371
+ const imageData = ctx.createImageData(this.width, this.height);
372
+ const data = imageData.data;
373
+ const progress = this.calculateEasedProgress(transition.progress, transition.easing);
374
+ for (let y = 0; y < this.height; y++) {
375
+ for (let x = 0; x < this.width; x++) {
376
+ const index = (y * this.width + x) * 4;
377
+ let alpha = 255;
378
+ switch (transition.type) {
379
+ case "wipe":
380
+ alpha = this.calculateWipeAlpha(x, y, progress, transition.direction);
381
+ break;
382
+ case "dissolve":
383
+ alpha = Math.random() < progress ? 255 : 0;
384
+ break;
385
+ default:
386
+ alpha = Math.floor(255 * progress);
387
+ }
388
+ data[index] = 255;
389
+ data[index + 1] = 255;
390
+ data[index + 2] = 255;
391
+ data[index + 3] = alpha;
392
+ }
393
+ }
394
+ return imageData;
395
+ }
396
+ calculateWipeAlpha(x, y, progress, direction) {
397
+ let position = 0;
398
+ switch (direction) {
399
+ case "left":
400
+ position = x / this.width;
401
+ break;
402
+ case "right":
403
+ position = 1 - x / this.width;
404
+ break;
405
+ case "up":
406
+ position = y / this.height;
407
+ break;
408
+ case "down":
409
+ position = 1 - y / this.height;
410
+ break;
411
+ case "in": {
412
+ const cx = x - this.width / 2;
413
+ const cy = y - this.height / 2;
414
+ const maxDist = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
415
+ position = Math.sqrt(cx * cx + cy * cy) / maxDist;
416
+ break;
417
+ }
418
+ case "out": {
419
+ const cx2 = x - this.width / 2;
420
+ const cy2 = y - this.height / 2;
421
+ const maxDist2 = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
422
+ position = 1 - Math.sqrt(cx2 * cx2 + cy2 * cy2) / maxDist2;
423
+ break;
424
+ }
425
+ default:
426
+ position = x / this.width;
427
+ }
428
+ return position < progress ? 255 : 0;
429
+ }
430
+ updateDimensions(width, height) {
431
+ this.width = width;
432
+ this.height = height;
433
+ }
434
+ }
435
+ class FilterProcessor {
436
+ filterCache = /* @__PURE__ */ new Map();
437
+ /**
438
+ * Apply filters to canvas context
439
+ * Combines multiple filters into a single CSS filter string for performance
440
+ */
441
+ applyFilters(ctx, filters) {
442
+ if (!filters || filters.length === 0) {
443
+ ctx.filter = "none";
444
+ return;
445
+ }
446
+ const cacheKey = this.generateCacheKey(filters);
447
+ let filterString = this.filterCache.get(cacheKey);
448
+ if (!filterString) {
449
+ filterString = this.buildFilterString(filters);
450
+ this.filterCache.set(cacheKey, filterString);
451
+ }
452
+ ctx.filter = filterString;
453
+ }
454
+ /**
455
+ * Build CSS filter string from filter array
456
+ */
457
+ buildFilterString(filters) {
458
+ const filterStrings = [];
459
+ for (const filter of filters) {
460
+ const filterStr = this.buildSingleFilter(filter);
461
+ if (filterStr) {
462
+ filterStrings.push(filterStr);
463
+ }
464
+ }
465
+ return filterStrings.length > 0 ? filterStrings.join(" ") : "none";
466
+ }
467
+ buildSingleFilter(filter) {
468
+ switch (filter.type) {
469
+ case "blur":
470
+ return `blur(${filter.value ?? 0}px)`;
471
+ case "brightness":
472
+ return `brightness(${filter.value ?? 1})`;
473
+ case "contrast":
474
+ return `contrast(${filter.value ?? 1})`;
475
+ case "grayscale":
476
+ return `grayscale(${filter.value ?? 0})`;
477
+ case "hue-rotate":
478
+ return `hue-rotate(${filter.value ?? 0}deg)`;
479
+ case "saturate":
480
+ return `saturate(${filter.value ?? 1})`;
481
+ case "sepia":
482
+ return `sepia(${filter.value ?? 0})`;
483
+ case "custom":
484
+ return this.buildCustomFilter(filter);
485
+ default:
486
+ console.warn(`Unknown filter type: ${filter.type}`);
487
+ return null;
488
+ }
489
+ }
490
+ /**
491
+ * Build custom filter from params
492
+ */
493
+ buildCustomFilter(filter) {
494
+ if (!filter.params) return null;
495
+ const { type, ...params } = filter.params;
496
+ switch (type) {
497
+ case "drop-shadow":
498
+ return `drop-shadow(${params.offsetX}px ${params.offsetY}px ${params.blur}px ${params.color})`;
499
+ case "opacity":
500
+ return `opacity(${params.value})`;
501
+ case "invert":
502
+ return `invert(${params.value})`;
503
+ default:
504
+ return null;
505
+ }
506
+ }
507
+ /**
508
+ * Apply color matrix transformation for advanced effects
509
+ * This allows for more complex color manipulations than CSS filters
510
+ */
511
+ applyColorMatrix(imageData, matrix) {
512
+ if (matrix.length !== 20) {
513
+ throw new Error("Color matrix must have 20 values (4x5 matrix)");
514
+ }
515
+ const data = imageData.data;
516
+ const length = data.length;
517
+ for (let i = 0; i < length; i += 4) {
518
+ const r = data[i];
519
+ const g = data[i + 1];
520
+ const b = data[i + 2];
521
+ const a = data[i + 3];
522
+ const m = matrix;
523
+ data[i] = this.clamp(r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255);
524
+ data[i + 1] = this.clamp(r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255);
525
+ data[i + 2] = this.clamp(r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255);
526
+ data[i + 3] = this.clamp(r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255);
527
+ }
528
+ return imageData;
529
+ }
530
+ /**
531
+ * Predefined color matrices for common effects
532
+ */
533
+ getPresetMatrix(preset) {
534
+ switch (preset) {
535
+ case "vintage":
536
+ return [
537
+ 0.393,
538
+ 0.769,
539
+ 0.189,
540
+ 0,
541
+ 0,
542
+ 0.349,
543
+ 0.686,
544
+ 0.168,
545
+ 0,
546
+ 0,
547
+ 0.272,
548
+ 0.534,
549
+ 0.131,
550
+ 0,
551
+ 0,
552
+ 0,
553
+ 0,
554
+ 0,
555
+ 1,
556
+ 0
557
+ ];
558
+ case "noir":
559
+ return [
560
+ 0.25,
561
+ 0.25,
562
+ 0.25,
563
+ 0,
564
+ 0,
565
+ 0.25,
566
+ 0.25,
567
+ 0.25,
568
+ 0,
569
+ 0,
570
+ 0.25,
571
+ 0.25,
572
+ 0.25,
573
+ 0,
574
+ 0,
575
+ 0,
576
+ 0,
577
+ 0,
578
+ 1,
579
+ 0
580
+ ];
581
+ case "cool":
582
+ return [0.8, 0, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0];
583
+ case "warm":
584
+ return [1.2, 0, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 0, 1, 0];
585
+ default:
586
+ return null;
587
+ }
588
+ }
589
+ /**
590
+ * Apply Gaussian blur manually (for cases where CSS filter is not enough)
591
+ */
592
+ applyGaussianBlur(imageData, radius) {
593
+ const output = new ImageData(
594
+ new Uint8ClampedArray(imageData.data),
595
+ imageData.width,
596
+ imageData.height
597
+ );
598
+ const width = imageData.width;
599
+ const height = imageData.height;
600
+ const data = imageData.data;
601
+ const outData = output.data;
602
+ for (let y = 0; y < height; y++) {
603
+ for (let x = 0; x < width; x++) {
604
+ let r = 0, g = 0, b = 0, a = 0;
605
+ let count = 0;
606
+ for (let dx = -radius; dx <= radius; dx++) {
607
+ const nx = Math.min(Math.max(x + dx, 0), width - 1);
608
+ const idx2 = (y * width + nx) * 4;
609
+ r += data[idx2];
610
+ g += data[idx2 + 1];
611
+ b += data[idx2 + 2];
612
+ a += data[idx2 + 3];
613
+ count++;
614
+ }
615
+ const idx = (y * width + x) * 4;
616
+ outData[idx] = r / count;
617
+ outData[idx + 1] = g / count;
618
+ outData[idx + 2] = b / count;
619
+ outData[idx + 3] = a / count;
620
+ }
621
+ }
622
+ for (let x = 0; x < width; x++) {
623
+ for (let y = 0; y < height; y++) {
624
+ let r = 0, g = 0, b = 0, a = 0;
625
+ let count = 0;
626
+ for (let dy = -radius; dy <= radius; dy++) {
627
+ const ny = Math.min(Math.max(y + dy, 0), height - 1);
628
+ const idx2 = (ny * width + x) * 4;
629
+ r += outData[idx2];
630
+ g += outData[idx2 + 1];
631
+ b += outData[idx2 + 2];
632
+ a += outData[idx2 + 3];
633
+ count++;
634
+ }
635
+ const idx = (y * width + x) * 4;
636
+ data[idx] = r / count;
637
+ data[idx + 1] = g / count;
638
+ data[idx + 2] = b / count;
639
+ data[idx + 3] = a / count;
640
+ }
641
+ }
642
+ return imageData;
643
+ }
644
+ clamp(value) {
645
+ return Math.min(255, Math.max(0, Math.round(value)));
646
+ }
647
+ generateCacheKey(filters) {
648
+ return filters.map((f) => `${f.type}:${f.value ?? "default"}`).join("|");
649
+ }
650
+ clearCache() {
651
+ this.filterCache.clear();
652
+ }
653
+ getCacheSize() {
654
+ return this.filterCache.size;
655
+ }
656
+ }
657
+ class VideoComposer {
658
+ config;
659
+ canvas;
660
+ ctx;
661
+ layerRenderer;
662
+ transitionProcessor;
663
+ filterProcessor;
664
+ timelineContext;
665
+ constructor(config) {
666
+ this.config = this.applyDefaults(config);
667
+ this.canvas = new OffscreenCanvas(this.config.width, this.config.height);
668
+ const ctx = this.canvas.getContext("2d", {
669
+ alpha: true,
670
+ desynchronized: true,
671
+ willReadFrequently: false,
672
+ colorSpace: "srgb"
673
+ });
674
+ if (!ctx) {
675
+ throw new Error("Failed to create 2D rendering context");
676
+ }
677
+ this.ctx = ctx;
678
+ this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
679
+ this.ctx.imageSmoothingQuality = "high";
680
+ this.layerRenderer = new LayerRenderer(ctx, this.config.width, this.config.height);
681
+ this.transitionProcessor = new TransitionProcessor(this.config.width, this.config.height);
682
+ this.filterProcessor = new FilterProcessor();
683
+ this.timelineContext = this.config.timeline;
684
+ }
685
+ applyDefaults(config) {
686
+ return {
687
+ width: config.width || 720,
688
+ height: config.height || 1280,
689
+ fps: config.fps || 30,
690
+ backgroundColor: config.backgroundColor ?? "#000",
691
+ renderer: config.renderer ?? "canvas2d",
692
+ enableSmoothing: config.enableSmoothing ?? true,
693
+ enableHardwareAcceleration: config.enableHardwareAcceleration ?? true,
694
+ revision: config.revision ?? 0,
695
+ inputHighWaterMark: config.inputHighWaterMark ?? 3,
696
+ outputHighWaterMark: config.outputHighWaterMark ?? 1,
697
+ maxLayers: config.maxLayers ?? 100,
698
+ timeline: config.timeline ?? {
699
+ clipId: "default",
700
+ trackId: "main",
701
+ clipStartUs: 0,
702
+ clipDurationUs: Infinity,
703
+ compositionFps: 30
704
+ }
705
+ };
706
+ }
707
+ createStreams(_instruction) {
708
+ if (_instruction?.baseConfig.timeline) {
709
+ this.timelineContext = _instruction.baseConfig.timeline;
710
+ }
711
+ const stream = new TransformStream(
712
+ {
713
+ transform: async (request, controller) => {
714
+ const result = await this.composeFrame(request);
715
+ controller.enqueue({
716
+ frame: result.frame,
717
+ metadata: result.metadata
718
+ });
719
+ },
720
+ flush: async () => {
721
+ this.filterProcessor.clearCache();
722
+ }
723
+ },
724
+ {
725
+ highWaterMark: this.config.inputHighWaterMark
726
+ },
727
+ {
728
+ highWaterMark: this.config.outputHighWaterMark
729
+ }
730
+ );
731
+ return {
732
+ composeStream: stream.writable,
733
+ cacheStream: stream.readable
734
+ };
735
+ }
736
+ async composeFrame(request) {
737
+ if (request.layers.length > this.config.maxLayers) {
738
+ throw new Error(`Too many layers: ${request.layers.length} > ${this.config.maxLayers}`);
739
+ }
740
+ this.clearCanvas();
741
+ if (request.transition) {
742
+ this.ctx.save();
743
+ this.transitionProcessor.applyTransition(this.ctx, request.transition);
744
+ }
745
+ for (const layer of request.layers) {
746
+ if (!layer.visible || layer.opacity <= 0) {
747
+ if (layer.type === "video") {
748
+ const vf = layer.videoFrame;
749
+ vf?.close?.();
750
+ }
751
+ continue;
752
+ }
753
+ try {
754
+ if (layer.filters && layer.filters.length > 0) {
755
+ this.ctx.save();
756
+ this.filterProcessor.applyFilters(this.ctx, layer.filters);
757
+ }
758
+ await this.layerRenderer.renderLayer(layer);
759
+ if (layer.filters && layer.filters.length > 0) {
760
+ this.ctx.restore();
761
+ }
762
+ } finally {
763
+ if (layer.type === "video") {
764
+ const vf = layer.videoFrame;
765
+ vf?.close?.();
766
+ }
767
+ }
768
+ }
769
+ if (request.transition) {
770
+ this.ctx.restore();
771
+ }
772
+ const frame = await this.createOutputFrame(request.timeUs);
773
+ const gopSerial = request.gopSerial;
774
+ const isKeyframe = request.isKeyframe;
775
+ return {
776
+ frame,
777
+ timeUs: request.timeUs,
778
+ metadata: {
779
+ layerCount: request.layers.length,
780
+ renderTime: 0,
781
+ gpuAccelerated: this.config.enableHardwareAcceleration && this.config.renderer !== "canvas2d",
782
+ ...typeof gopSerial === "number" && { gopSerial },
783
+ ...typeof isKeyframe === "boolean" && { isKeyframe }
784
+ }
785
+ };
786
+ }
787
+ async composeTransition(fromRequest, toRequest, transition) {
788
+ await this.composeFrame(fromRequest);
789
+ const toFrameRequest = {
790
+ ...toRequest,
791
+ transition
792
+ };
793
+ return this.composeFrame(toFrameRequest);
794
+ }
795
+ clearCanvas() {
796
+ if (this.config.backgroundColor) {
797
+ this.ctx.fillStyle = this.config.backgroundColor;
798
+ this.ctx.fillRect(0, 0, this.config.width, this.config.height);
799
+ } else {
800
+ this.ctx.clearRect(0, 0, this.config.width, this.config.height);
801
+ }
802
+ }
803
+ // private sortLayers(layers: Layer[]): Layer[] {
804
+ // return [...layers].sort((a, b) => a.zIndex - b.zIndex);
805
+ // }
806
+ async createOutputFrame(timeUs) {
807
+ const duration = frameDurationFromFps(this.timelineContext.compositionFps);
808
+ const frame = new VideoFrame(this.canvas, {
809
+ timestamp: timeUs,
810
+ duration,
811
+ alpha: "discard",
812
+ visibleRect: { x: 0, y: 0, width: this.canvas.width, height: this.canvas.height }
813
+ });
814
+ return frame;
815
+ }
816
+ updateConfig(config) {
817
+ Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
818
+ if (config.width || config.height) {
819
+ this.canvas.width = this.config.width;
820
+ this.canvas.height = this.config.height;
821
+ this.layerRenderer.updateDimensions(this.config.width, this.config.height);
822
+ this.transitionProcessor.updateDimensions(this.config.width, this.config.height);
823
+ }
824
+ if (config.enableSmoothing !== void 0) {
825
+ this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
826
+ }
827
+ if (config.timeline) {
828
+ this.timelineContext = config.timeline;
829
+ }
830
+ }
831
+ dispose() {
832
+ this.filterProcessor.clearCache();
833
+ }
834
+ }
835
+ function resolveActiveLayers(layers, timestamp, _frame) {
836
+ return layers.filter((layer) => {
837
+ if (layer.status !== "ready") {
838
+ return false;
839
+ }
840
+ return layer.activeRanges.some(
841
+ (range) => timestamp >= range.startUs && timestamp < range.endUs
842
+ );
843
+ });
844
+ }
845
+ function materializeLayer(layer, frame) {
846
+ const baseLayer = {
847
+ id: layer.layerId,
848
+ type: layer.type,
849
+ zIndex: layer.zIndex ?? 0,
850
+ visible: true,
851
+ opacity: layer.opacity ?? 1
852
+ };
853
+ if (layer.type === "video") {
854
+ return {
855
+ ...baseLayer,
856
+ type: "video",
857
+ videoFrame: frame
858
+ };
859
+ }
860
+ if (layer.type === "text") {
861
+ const payload = layer.payload;
862
+ return {
863
+ ...baseLayer,
864
+ type: "text",
865
+ text: payload.text,
866
+ fontFamily: payload.fontFamily,
867
+ fontSize: payload.fontSize,
868
+ fontWeight: payload.fontWeight,
869
+ color: payload.color,
870
+ strokeColor: payload.strokeColor,
871
+ strokeWidth: payload.strokeWidth,
872
+ lineHeight: payload.lineHeight,
873
+ textAlign: payload.align,
874
+ verticalAlign: "bottom"
875
+ // Subtitles positioned at bottom
876
+ };
877
+ }
878
+ if (layer.type === "image") {
879
+ const payload = layer.payload;
880
+ const source = payload.bitmapHandle ?? null;
881
+ if (source) {
882
+ return {
883
+ ...baseLayer,
884
+ type: "image",
885
+ source
886
+ };
887
+ }
888
+ }
889
+ return baseLayer;
890
+ }
891
+ class VideoComposeWorker {
892
+ channel;
893
+ composer = null;
894
+ composeStream = null;
895
+ downstreamPorts = /* @__PURE__ */ new Map();
896
+ upstreamPorts = /* @__PURE__ */ new Map();
897
+ instructionRegistry = /* @__PURE__ */ new Map();
898
+ pendingReplay = /* @__PURE__ */ new Map();
899
+ streamState = /* @__PURE__ */ new Map();
900
+ constructor() {
901
+ this.channel = new WorkerChannel(self, {
902
+ name: "VideoComposeWorker",
903
+ timeout: 3e4
904
+ });
905
+ this.setupHandlers();
906
+ }
907
+ setupHandlers() {
908
+ this.channel.registerHandler("configure", this.handleConfigure.bind(this));
909
+ this.channel.registerHandler("connect", this.handleConnect.bind(this));
910
+ this.channel.registerHandler("flush", this.handleFlush.bind(this));
911
+ this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
912
+ this.channel.registerHandler("install_instructions", this.handleInstallInstructions.bind(this));
913
+ this.channel.registerHandler("sync_clip", this.handleSyncClip.bind(this));
914
+ this.channel.registerHandler("dispose_clip", this.handleDisposeClip.bind(this));
915
+ this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
916
+ }
917
+ /**
918
+ * Unified connect handler used by stream pipeline
919
+ */
920
+ async handleConnect(payload) {
921
+ const { port, direction, clipId = "default" } = payload;
922
+ if (direction === "upstream") {
923
+ this.upstreamPorts.set(clipId, port);
924
+ const channel = new WorkerChannel(port, {
925
+ name: "VideoCompose-Decode",
926
+ timeout: 3e4
927
+ });
928
+ channel.receiveStream(this.handleReceiveStream.bind(this));
929
+ }
930
+ if (direction === "downstream") {
931
+ this.downstreamPorts.set(clipId, port);
932
+ }
933
+ return { success: true };
934
+ }
935
+ /**
936
+ * Configure composer
937
+ * According to docs/impl/14-config, only reinitialize when initial=true
938
+ */
939
+ async handleConfigure(payload) {
940
+ const { config, initial } = payload;
941
+ const hasValidDimensions = config.width > 0 && config.height > 0;
942
+ const hasValidFps = config.fps > 0;
943
+ if (!hasValidDimensions || !hasValidFps) {
944
+ throw new Error(
945
+ `VideoComposeWorker: invalid canvas config width=${config.width}, height=${config.height}, fps=${config.fps}`
946
+ );
947
+ }
948
+ if (initial) {
949
+ this.channel.state = WorkerState.Ready;
950
+ }
951
+ if (initial || !this.composer) {
952
+ if (this.composer) {
953
+ this.composer.dispose();
954
+ this.composeStream = null;
955
+ }
956
+ this.composer = new VideoComposer(config);
957
+ } else {
958
+ this.composer.updateConfig(config);
959
+ }
960
+ this.channel.notify("configured", {
961
+ config: this.composer.config,
962
+ initialized: initial || false
963
+ });
964
+ return {
965
+ success: true,
966
+ config: this.composer.config
967
+ };
968
+ }
969
+ async handleReceiveStream(stream, metadata) {
970
+ const { clipId = "default" } = metadata || {};
971
+ if (!this.composer) {
972
+ console.error("[VideoComposeWorker] Composer not configured");
973
+ return;
974
+ }
975
+ const instruction = this.instructionRegistry.get(clipId);
976
+ if (!instruction) {
977
+ console.warn("[VideoComposeWorker] No instructions for clip", clipId);
978
+ return;
979
+ }
980
+ const filteredStream = stream.pipeThrough(
981
+ new TransformStream({
982
+ transform: (wrappedFrame, controller) => {
983
+ try {
984
+ const frame = wrappedFrame.frame || wrappedFrame;
985
+ const gopSerial = wrappedFrame.gopSerial;
986
+ const isKeyframe = wrappedFrame.isKeyframe;
987
+ const timestamp = frame.timestamp ?? 0;
988
+ if (this.shouldSkipFrame(clipId, timestamp)) {
989
+ frame.close();
990
+ return;
991
+ }
992
+ const request = this.buildComposeRequest(clipId, instruction, frame, timestamp);
993
+ if (!request) {
994
+ frame.close();
995
+ return;
996
+ }
997
+ request.gopSerial = gopSerial;
998
+ request.isKeyframe = isKeyframe;
999
+ controller.enqueue(request);
1000
+ } catch (error) {
1001
+ const frame = wrappedFrame.frame || wrappedFrame;
1002
+ frame?.close?.();
1003
+ throw error;
1004
+ }
1005
+ }
1006
+ })
1007
+ );
1008
+ const { composeStream, cacheStream } = this.composer.createStreams();
1009
+ this.channel.sendStream(cacheStream, metadata);
1010
+ filteredStream.pipeTo(composeStream).catch((error) => {
1011
+ console.error("[VideoComposeWorker] compose stream error", clipId, error);
1012
+ });
1013
+ }
1014
+ // private handleGetStream(): ReadableStream<VideoFrame> | undefined {
1015
+ // return this.composer?.createStreams()?.cacheStream;
1016
+ // }
1017
+ /**
1018
+ * Flush the composition pipeline
1019
+ */
1020
+ async handleFlush() {
1021
+ try {
1022
+ this.channel.notify("flush_complete", {});
1023
+ return { success: true };
1024
+ } catch (error) {
1025
+ throw {
1026
+ code: "FLUSH_ERROR",
1027
+ message: error.message
1028
+ };
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Get composer statistics
1033
+ */
1034
+ async handleGetStats() {
1035
+ return {
1036
+ configured: this.composer !== null,
1037
+ config: this.composer?.config,
1038
+ streaming: this.composeStream !== null
1039
+ };
1040
+ }
1041
+ /**
1042
+ * Dispose worker and cleanup resources
1043
+ */
1044
+ async handleDispose() {
1045
+ if (this.composer) {
1046
+ this.composer.dispose();
1047
+ this.composer = null;
1048
+ }
1049
+ this.composeStream = null;
1050
+ this.downstreamPorts.get("default")?.close();
1051
+ this.upstreamPorts.get("default")?.close();
1052
+ this.downstreamPorts.clear();
1053
+ this.upstreamPorts.clear();
1054
+ this.channel.state = WorkerState.Disposed;
1055
+ return { success: true };
1056
+ }
1057
+ async handleInstallInstructions(data) {
1058
+ const { clipId, revision } = data;
1059
+ const current = this.instructionRegistry.get(clipId);
1060
+ if (current && current.revision > revision) {
1061
+ return { success: false };
1062
+ }
1063
+ this.instructionRegistry.set(clipId, data);
1064
+ return { success: true };
1065
+ }
1066
+ async handleSyncClip(payload) {
1067
+ const { clipId, revision, range } = payload;
1068
+ const current = this.instructionRegistry.get(clipId);
1069
+ if (!current || current.revision > revision) {
1070
+ return { success: false };
1071
+ }
1072
+ this.pendingReplay.set(clipId, { ...range, revision });
1073
+ this.channel.notify("sync_ack", { clipId, revision });
1074
+ return { success: true };
1075
+ }
1076
+ async handleDisposeClip(payload) {
1077
+ const { clipId } = payload;
1078
+ this.instructionRegistry.delete(clipId);
1079
+ this.pendingReplay.delete(clipId);
1080
+ this.downstreamPorts.get(clipId)?.close();
1081
+ this.upstreamPorts.get(clipId)?.close();
1082
+ this.downstreamPorts.delete(clipId);
1083
+ this.upstreamPorts.delete(clipId);
1084
+ return { success: true };
1085
+ }
1086
+ /**
1087
+ * Check if frame should be skipped (outside dirty range)
1088
+ * Returns true if frame is NOT in the dirty range and should use cached version
1089
+ */
1090
+ shouldSkipFrame(clipId, timestamp) {
1091
+ const dirtyRange = this.pendingReplay.get(clipId);
1092
+ if (!dirtyRange) {
1093
+ return false;
1094
+ }
1095
+ if (timestamp >= dirtyRange.startUs && timestamp <= dirtyRange.endUs) {
1096
+ return false;
1097
+ }
1098
+ if (timestamp > dirtyRange.endUs) {
1099
+ this.pendingReplay.delete(clipId);
1100
+ }
1101
+ return true;
1102
+ }
1103
+ buildComposeRequest(clipId, instruction, frame, _timestamp) {
1104
+ const normalizedTime = this.computeTimelineTimestamp(clipId, frame, instruction.baseConfig);
1105
+ const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
1106
+ const clipDurationUs = instruction.baseConfig.timeline?.clipDurationUs ?? Infinity;
1107
+ const clipEndUs = clipStartUs + clipDurationUs;
1108
+ if (normalizedTime < clipStartUs || normalizedTime >= clipEndUs) {
1109
+ return null;
1110
+ }
1111
+ const clipRelativeTime = normalizedTime - clipStartUs;
1112
+ const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
1113
+ if (!activeLayers.length) {
1114
+ return null;
1115
+ }
1116
+ const layers = activeLayers.map((layer) => materializeLayer(layer, frame));
1117
+ return {
1118
+ timeUs: normalizedTime,
1119
+ layers,
1120
+ transition: VideoComposeWorker.buildTransition(
1121
+ instruction.transitions,
1122
+ clipRelativeTime,
1123
+ instruction.baseConfig.timeline
1124
+ )
1125
+ };
1126
+ }
1127
+ static buildTransition(transitions, timeUs, _timeline) {
1128
+ const entry = transitions.find((transition) => {
1129
+ const { startUs, endUs } = transition.range;
1130
+ return timeUs >= startUs && timeUs < endUs;
1131
+ });
1132
+ if (!entry) {
1133
+ return void 0;
1134
+ }
1135
+ const durationUs = entry.range.endUs - entry.range.startUs;
1136
+ const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;
1137
+ return {
1138
+ type: entry.params.type,
1139
+ progress: Math.min(Math.max(progress, 0), 1),
1140
+ easing: entry.params.easing,
1141
+ params: entry.params.payload,
1142
+ direction: entry.params.payload?.direction
1143
+ };
1144
+ }
1145
+ computeTimelineTimestamp(clipId, frame, config) {
1146
+ const key = clipId;
1147
+ let state = this.streamState.get(key);
1148
+ if (!state) {
1149
+ state = {
1150
+ baseTimestamp: null,
1151
+ lastSourceTimestamp: null,
1152
+ nextFrameIndex: 0
1153
+ };
1154
+ this.streamState.set(key, state);
1155
+ }
1156
+ const timeline = config.timeline;
1157
+ if (!timeline) {
1158
+ const ts = frame.timestamp ?? 0;
1159
+ state.lastSourceTimestamp = frame.timestamp ?? null;
1160
+ return ts;
1161
+ }
1162
+ const { clipStartUs, compositionFps } = timeline;
1163
+ const sourceTimestamp = frame.timestamp ?? null;
1164
+ if (sourceTimestamp !== null && state.lastSourceTimestamp !== null && sourceTimestamp < state.lastSourceTimestamp) {
1165
+ state.baseTimestamp = null;
1166
+ state.nextFrameIndex = 0;
1167
+ }
1168
+ if (state.baseTimestamp === null) {
1169
+ state.baseTimestamp = sourceTimestamp ?? 0;
1170
+ state.nextFrameIndex = 0;
1171
+ if (state.baseTimestamp > 1e3) {
1172
+ console.warn(
1173
+ `[VideoComposeWorker] First frame timestamp is ${state.baseTimestamp}us, expected ~0. Check MP4Demuxer normalization.`
1174
+ );
1175
+ }
1176
+ }
1177
+ const frameDuration = frameDurationFromFps(compositionFps);
1178
+ let frameIndex = state.nextFrameIndex;
1179
+ if (sourceTimestamp !== null) {
1180
+ const approxIndex = frameIndexFromTimestamp(
1181
+ state.baseTimestamp,
1182
+ sourceTimestamp,
1183
+ compositionFps,
1184
+ "nearest"
1185
+ );
1186
+ frameIndex = Math.max(frameIndex, approxIndex);
1187
+ }
1188
+ const rawTimeline = clipStartUs + frameIndex * frameDuration;
1189
+ const timelineTime = quantizeTimestampToFrame(
1190
+ rawTimeline,
1191
+ clipStartUs,
1192
+ compositionFps,
1193
+ "nearest"
1194
+ );
1195
+ state.nextFrameIndex = frameIndex + 1;
1196
+ state.lastSourceTimestamp = sourceTimestamp;
1197
+ return timelineTime;
1198
+ }
1199
+ }
1200
+ const worker = new VideoComposeWorker();
1201
+ self.addEventListener("beforeunload", () => {
1202
+ worker["handleDispose"]();
1203
+ });
1204
+ const videoCompose_worker = null;
1205
+ export {
1206
+ VideoComposeWorker,
1207
+ videoCompose_worker as default
1208
+ };
1209
+ //# sourceMappingURL=video-compose.worker.js.map