@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.
- package/README.md +17 -4
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +2 -4
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +8 -1
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +2 -9
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +3 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +4 -2
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +7 -13
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/PreRenderService.d.ts +3 -2
- package/dist/controllers/PreRenderService.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.js.map +1 -1
- package/dist/controllers/PreviewHandle.d.ts +2 -0
- package/dist/controllers/PreviewHandle.d.ts.map +1 -1
- package/dist/controllers/PreviewHandle.js +6 -0
- package/dist/controllers/PreviewHandle.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/types.d.ts +2 -12
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/event/events.d.ts +5 -59
- package/dist/event/events.d.ts.map +1 -1
- package/dist/event/events.js +1 -6
- package/dist/event/events.js.map +1 -1
- package/dist/model/CompositionModel.js +1 -2
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +1 -0
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +3 -13
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +4 -5
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +1 -1
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +3 -2
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/compose/types.d.ts +3 -1
- package/dist/stages/compose/types.d.ts.map +1 -1
- package/dist/stages/decode/AudioChunkDecoder.d.ts.map +1 -1
- package/dist/stages/decode/VideoChunkDecoder.d.ts +0 -1
- package/dist/stages/decode/VideoChunkDecoder.d.ts.map +1 -1
- package/dist/stages/demux/MP4Demuxer.d.ts +2 -1
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/load/EventHandlers.d.ts +2 -11
- package/dist/stages/load/EventHandlers.d.ts.map +1 -1
- package/dist/stages/load/EventHandlers.js +1 -24
- package/dist/stages/load/EventHandlers.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +11 -13
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +1 -1
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +3 -2
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/stages/load/types.d.ts +2 -0
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/utils/time-utils.d.ts +3 -2
- package/dist/utils/time-utils.d.ts.map +1 -1
- package/dist/utils/time-utils.js +2 -1
- package/dist/utils/time-utils.js.map +1 -1
- package/dist/vite-plugin.d.ts +19 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +145 -0
- package/dist/vite-plugin.js.map +1 -0
- package/dist/worker/WorkerPool.d.ts +7 -4
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +29 -18
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/{stages/demux → workers}/MP4Demuxer.js +17 -15
- package/dist/workers/MP4Demuxer.js.map +1 -0
- package/dist/workers/WorkerChannel.js +486 -0
- package/dist/workers/WorkerChannel.js.map +1 -0
- package/dist/workers/mp4box.all.js +7049 -0
- package/dist/workers/mp4box.all.js.map +1 -0
- package/dist/workers/stages/compose/audio-compose.worker.js +1063 -0
- package/dist/workers/stages/compose/audio-compose.worker.js.map +1 -0
- package/dist/workers/stages/compose/video-compose.worker.js +1209 -0
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -0
- package/dist/{stages → workers/stages}/decode/decode.worker.js +401 -20
- package/dist/workers/stages/decode/decode.worker.js.map +1 -0
- package/dist/{stages → workers/stages}/demux/audio-demux.worker.js +184 -4
- package/dist/workers/stages/demux/audio-demux.worker.js.map +1 -0
- package/dist/{stages → workers/stages}/demux/video-demux.worker.js +7 -30
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -0
- package/dist/{stages → workers/stages}/encode/encode.worker.js +238 -5
- package/dist/workers/stages/encode/encode.worker.js.map +1 -0
- package/dist/{stages/mux/MP4Muxer.js → workers/stages/mux/mux.worker.js} +244 -5
- package/dist/workers/stages/mux/mux.worker.js.map +1 -0
- package/package.json +27 -21
- package/dist/model/types.js +0 -5
- package/dist/model/types.js.map +0 -1
- package/dist/plugins/BackpressureMonitor.js +0 -62
- package/dist/plugins/BackpressureMonitor.js.map +0 -1
- package/dist/stages/compose/AudioDucker.js +0 -161
- package/dist/stages/compose/AudioDucker.js.map +0 -1
- package/dist/stages/compose/AudioMixer.js +0 -373
- package/dist/stages/compose/AudioMixer.js.map +0 -1
- package/dist/stages/compose/FilterProcessor.js +0 -226
- package/dist/stages/compose/FilterProcessor.js.map +0 -1
- package/dist/stages/compose/LayerRenderer.js +0 -215
- package/dist/stages/compose/LayerRenderer.js.map +0 -1
- package/dist/stages/compose/TransitionProcessor.js +0 -189
- package/dist/stages/compose/TransitionProcessor.js.map +0 -1
- package/dist/stages/compose/VideoComposer.js +0 -186
- package/dist/stages/compose/VideoComposer.js.map +0 -1
- package/dist/stages/compose/audio-compose.worker.d.ts +0 -79
- package/dist/stages/compose/audio-compose.worker.d.ts.map +0 -1
- package/dist/stages/compose/audio-compose.worker.js +0 -541
- package/dist/stages/compose/audio-compose.worker.js.map +0 -1
- package/dist/stages/compose/video-compose.worker.d.ts +0 -60
- package/dist/stages/compose/video-compose.worker.d.ts.map +0 -1
- package/dist/stages/compose/video-compose.worker.js +0 -369
- package/dist/stages/compose/video-compose.worker.js.map +0 -1
- package/dist/stages/decode/AudioChunkDecoder.js +0 -83
- package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
- package/dist/stages/decode/BaseDecoder.js +0 -130
- package/dist/stages/decode/BaseDecoder.js.map +0 -1
- package/dist/stages/decode/VideoChunkDecoder.js +0 -209
- package/dist/stages/decode/VideoChunkDecoder.js.map +0 -1
- package/dist/stages/decode/decode.worker.d.ts +0 -70
- package/dist/stages/decode/decode.worker.d.ts.map +0 -1
- package/dist/stages/decode/decode.worker.js.map +0 -1
- package/dist/stages/demux/MP3FrameParser.js +0 -186
- package/dist/stages/demux/MP3FrameParser.js.map +0 -1
- package/dist/stages/demux/MP4Demuxer.js.map +0 -1
- package/dist/stages/demux/audio-demux.worker.d.ts +0 -51
- package/dist/stages/demux/audio-demux.worker.d.ts.map +0 -1
- package/dist/stages/demux/audio-demux.worker.js.map +0 -1
- package/dist/stages/demux/video-demux.worker.d.ts +0 -48
- package/dist/stages/demux/video-demux.worker.d.ts.map +0 -1
- package/dist/stages/demux/video-demux.worker.js.map +0 -1
- package/dist/stages/encode/AudioChunkEncoder.js +0 -37
- package/dist/stages/encode/AudioChunkEncoder.js.map +0 -1
- package/dist/stages/encode/BaseEncoder.js +0 -164
- package/dist/stages/encode/BaseEncoder.js.map +0 -1
- package/dist/stages/encode/VideoChunkEncoder.js +0 -50
- package/dist/stages/encode/VideoChunkEncoder.js.map +0 -1
- package/dist/stages/encode/encode.worker.d.ts +0 -3
- package/dist/stages/encode/encode.worker.d.ts.map +0 -1
- package/dist/stages/encode/encode.worker.js.map +0 -1
- package/dist/stages/mux/MP4Muxer.js.map +0 -1
- package/dist/stages/mux/mux.worker.d.ts +0 -65
- package/dist/stages/mux/mux.worker.d.ts.map +0 -1
- package/dist/stages/mux/mux.worker.js +0 -219
- package/dist/stages/mux/mux.worker.js.map +0 -1
- package/dist/stages/mux/utils.js +0 -34
- 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
|