@framefields/compositions 0.0.0-stage → 2.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/LICENCE +202 -0
- package/dist/index.d.mts +81 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +898 -0
- package/dist/index.mjs.map +1 -0
- package/dist/layer-logic-DBs4NHoi.mjs +162 -0
- package/dist/layer-logic-DBs4NHoi.mjs.map +1 -0
- package/dist/program/index.d.mts +3401 -0
- package/dist/program/index.d.mts.map +1 -0
- package/dist/program/index.mjs +5454 -0
- package/dist/program/index.mjs.map +1 -0
- package/dist/server.d.mts +2 -0
- package/dist/server.mjs +3 -0
- package/dist/timing-CHy-6wsp.d.mts +53 -0
- package/dist/timing-CHy-6wsp.d.mts.map +1 -0
- package/package.json +63 -3
- package/README.md +0 -3
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import { a as framesToSeconds, c as getFontUrl, i as formatTimecode, n as isStaticVisualMedia, o as secondsToFrames, r as resolveLayerDuration, s as computeRenderParams, t as calculateLayerTransform } from "./layer-logic-DBs4NHoi.mjs";
|
|
2
|
+
import { getActiveMediaMetadata, getMediaType, isSignal, updateClockSignals } from "@framefields/core";
|
|
3
|
+
import { audioRegistry, webgpuRegistry } from "@framefields/node-sdk";
|
|
4
|
+
import { drawCaptionNode, drawGifNode, drawImageNode, drawLottieNode, drawParagraphNode, drawSignalNode, drawSvgNode, drawVideoNode, inputStore, shaderStore, signalRegistry } from "@framefields/webgpu-renderers";
|
|
5
|
+
import { AudioSampleSink } from "mediabunny";
|
|
6
|
+
|
|
7
|
+
//#region src/compositions/canvas-composition.ts
|
|
8
|
+
const segmentRangeCache = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
function getCachedSegmentRanges(segments, fps) {
|
|
10
|
+
const cached = segmentRangeCache.get(segments);
|
|
11
|
+
if (cached && cached.fps === fps) return cached.ranges;
|
|
12
|
+
let currentStart = 0;
|
|
13
|
+
const ranges = segments.map((seg) => {
|
|
14
|
+
const durSec = (seg.endSec ?? 0) - seg.startSec;
|
|
15
|
+
const durFrames = Math.max(1, Math.round(durSec * fps));
|
|
16
|
+
const start = currentStart;
|
|
17
|
+
currentStart += durFrames;
|
|
18
|
+
return {
|
|
19
|
+
seg,
|
|
20
|
+
start,
|
|
21
|
+
duration: durFrames
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
segmentRangeCache.set(segments, {
|
|
25
|
+
fps,
|
|
26
|
+
ranges
|
|
27
|
+
});
|
|
28
|
+
return ranges;
|
|
29
|
+
}
|
|
30
|
+
const frameCtxPool = [];
|
|
31
|
+
function acquireFrameContext() {
|
|
32
|
+
return frameCtxPool.pop() ?? {};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Effect operations carry every config field as a signal. Hand node renderers
|
|
36
|
+
* the plain value for this frame (nested objects/arrays included), keeping only
|
|
37
|
+
* GPU-bound signals intact since those are consumed as textures.
|
|
38
|
+
*/
|
|
39
|
+
function resolveSignalOperation(virtualMedia, frameCtx) {
|
|
40
|
+
const op = virtualMedia.operation;
|
|
41
|
+
const resolved = { ...op };
|
|
42
|
+
for (const [key, sig] of Object.entries(op.signals)) if (isSignal(sig) && !sig.gpuBinding) resolved[key] = sig.get(frameCtx);
|
|
43
|
+
return {
|
|
44
|
+
...virtualMedia,
|
|
45
|
+
operation: resolved
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function drawCompositionTree(ctx, encoder, targetView, targetTexture, targetWidth, targetHeight, virtualMedia, props, inheritedSeekOffset = 0, inheritedClockOffset = 0) {
|
|
49
|
+
const fps = props.fps ?? 24;
|
|
50
|
+
const op = virtualMedia.operation;
|
|
51
|
+
if (!op) return;
|
|
52
|
+
const isCompositor = op.op === "Compositor";
|
|
53
|
+
const frame = props.frame ?? 0;
|
|
54
|
+
const segments = op.timeline?.segments || [];
|
|
55
|
+
const localFrame = frame - (op.startFrame ?? op.timeline?.startFrame ?? 0);
|
|
56
|
+
const isTimelineProvider = op.timeline !== void 0 || op.startFrame !== void 0 || op.op === "compose";
|
|
57
|
+
const resolvedDurationMs = isTimelineProvider ? getActiveMediaMetadata(virtualMedia)?.durationMs ?? props.durationMs ?? void 0 : props.durationMs ?? getActiveMediaMetadata(virtualMedia)?.durationMs ?? void 0;
|
|
58
|
+
if (!(op.op === "source" || op.op === "text" || op.op === "caption")) updateClockSignals(frame, fps, resolvedDurationMs);
|
|
59
|
+
const frameCtx = acquireFrameContext();
|
|
60
|
+
frameCtx.frame = frame;
|
|
61
|
+
frameCtx.fps = fps;
|
|
62
|
+
frameCtx.time = fps > 0 ? frame / fps : 0;
|
|
63
|
+
frameCtx.duration = (resolvedDurationMs ?? 0) / 1e3;
|
|
64
|
+
frameCtx.durationMs = resolvedDurationMs ?? 0;
|
|
65
|
+
frameCtx.progress = resolvedDurationMs && resolvedDurationMs > 0 ? Math.max(0, Math.min(1, frame / fps * 1e3 / resolvedDurationMs)) : 0;
|
|
66
|
+
frameCtx.deltaTime = fps > 0 ? 1 / fps : 0;
|
|
67
|
+
const resolvedElapsedMs = isTimelineProvider ? (localFrame !== void 0 && fps !== void 0 ? localFrame / fps * 1e3 : void 0) ?? props.elapsedMs : props.elapsedMs ?? (localFrame !== void 0 && fps !== void 0 ? localFrame / fps * 1e3 : void 0);
|
|
68
|
+
const effectiveLocalFrame = localFrame + Math.round(inheritedSeekOffset * fps);
|
|
69
|
+
let activeSeg = null;
|
|
70
|
+
let windowStartFrame = 0;
|
|
71
|
+
if (segments.length > 0) {
|
|
72
|
+
const active = getCachedSegmentRanges(segments, fps).find(({ start, duration }) => effectiveLocalFrame >= start && effectiveLocalFrame < start + duration);
|
|
73
|
+
if (active) {
|
|
74
|
+
activeSeg = active.seg;
|
|
75
|
+
windowStartFrame = active.start;
|
|
76
|
+
} else return;
|
|
77
|
+
}
|
|
78
|
+
const segmentLocalFrame = localFrame - windowStartFrame;
|
|
79
|
+
const accumulatedSeekOffsetSec = inheritedSeekOffset + (activeSeg ? activeSeg.startSec - windowStartFrame / fps : 0);
|
|
80
|
+
const accumulatedClockOffsetSec = inheritedClockOffset + windowStartFrame / fps;
|
|
81
|
+
if (typeof op.onRequestFrame === "function") op.onRequestFrame(frameCtx);
|
|
82
|
+
else if (op.effect && typeof op.effect.notifyFrame === "function") op.effect.notifyFrame(frameCtx);
|
|
83
|
+
const CustomRenderer = webgpuRegistry.get(op.op);
|
|
84
|
+
if (CustomRenderer) {
|
|
85
|
+
if (op.signals) virtualMedia = resolveSignalOperation(virtualMedia, frameCtx);
|
|
86
|
+
const inputs = op?.inputs;
|
|
87
|
+
if (inputs) {
|
|
88
|
+
for (const entry of Object.values(inputs)) if (entry?.connectionValid && entry?.outputItem?.type === "LUT") {
|
|
89
|
+
const lutVirtualMedia = entry.outputItem.data;
|
|
90
|
+
if (lutVirtualMedia && typeof lutVirtualMedia === "object" && lutVirtualMedia.operation) {
|
|
91
|
+
const lutW = lutVirtualMedia.metadata?.width ?? targetWidth;
|
|
92
|
+
const lutH = lutVirtualMedia.metadata?.height ?? targetHeight;
|
|
93
|
+
const dummyTex = ctx.renderer.getTemporaryTexture(lutW, lutH);
|
|
94
|
+
await drawCompositionTree(ctx, encoder, dummyTex.createView(), dummyTex, lutW, lutH, lutVirtualMedia, {
|
|
95
|
+
...props,
|
|
96
|
+
virtualMedia: lutVirtualMedia
|
|
97
|
+
}, accumulatedSeekOffsetSec, accumulatedClockOffsetSec);
|
|
98
|
+
}
|
|
99
|
+
} else if (entry?.connectionValid && entry?.outputItem?.type === "Signal") {
|
|
100
|
+
const sigData = entry.outputItem.data;
|
|
101
|
+
if (sigData && typeof sigData === "object") await signalRegistry.ensureAudioSourcesExtracted(ctx.device, sigData.signalConfig ?? sigData, fps, props.renderId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const pass = ctx.renderer.beginFrame(encoder, targetView, {
|
|
105
|
+
r: 0,
|
|
106
|
+
g: 0,
|
|
107
|
+
b: 0,
|
|
108
|
+
a: 0
|
|
109
|
+
}, targetWidth, targetHeight, "load");
|
|
110
|
+
let passEnded = false;
|
|
111
|
+
const originalEnd = pass.end.bind(pass);
|
|
112
|
+
pass.end = () => {
|
|
113
|
+
if (!passEnded) {
|
|
114
|
+
originalEnd();
|
|
115
|
+
passEnded = true;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const renderArgs = {
|
|
119
|
+
ctx,
|
|
120
|
+
encoder,
|
|
121
|
+
pass,
|
|
122
|
+
targetView,
|
|
123
|
+
targetTexture,
|
|
124
|
+
targetWidth,
|
|
125
|
+
targetHeight,
|
|
126
|
+
props: {
|
|
127
|
+
...props,
|
|
128
|
+
virtualMedia,
|
|
129
|
+
inheritedSeekOffset: accumulatedSeekOffsetSec,
|
|
130
|
+
inheritedClockOffset: accumulatedClockOffsetSec,
|
|
131
|
+
durationMs: resolvedDurationMs,
|
|
132
|
+
elapsedMs: resolvedElapsedMs
|
|
133
|
+
},
|
|
134
|
+
drawChild: async (child, overrides, targetViewOverride, targetTextureOverride, targetWidthOverride, targetHeightOverride) => {
|
|
135
|
+
await drawCompositionTree(ctx, encoder, targetViewOverride ?? targetView, targetTextureOverride ?? targetTexture, targetWidthOverride ?? targetWidth, targetHeightOverride ?? targetHeight, child, {
|
|
136
|
+
...props,
|
|
137
|
+
durationMs: resolvedDurationMs,
|
|
138
|
+
elapsedMs: resolvedElapsedMs,
|
|
139
|
+
virtualMedia: child,
|
|
140
|
+
...overrides,
|
|
141
|
+
excludeTextures: [
|
|
142
|
+
...props.excludeTextures || [],
|
|
143
|
+
...overrides?.excludeTextures || [],
|
|
144
|
+
targetTextureOverride ?? targetTexture
|
|
145
|
+
]
|
|
146
|
+
}, isCompositor ? 0 : accumulatedSeekOffsetSec, accumulatedClockOffsetSec);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
await CustomRenderer(renderArgs);
|
|
150
|
+
renderArgs.pass.end();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (op.op === "LUT") return;
|
|
154
|
+
if (op.op === "Signal" || op.op === "SignalMath" || op.dataType === "Signal") {
|
|
155
|
+
const pass = ctx.renderer.beginFrame(encoder, targetView, {
|
|
156
|
+
r: 0,
|
|
157
|
+
g: 0,
|
|
158
|
+
b: 0,
|
|
159
|
+
a: 0
|
|
160
|
+
}, targetWidth, targetHeight, "load");
|
|
161
|
+
await drawSignalNode(ctx, encoder, pass, {
|
|
162
|
+
nodeId: op.nodeId || op.signalConfig?.nodeId || props.renderId,
|
|
163
|
+
func: op.func,
|
|
164
|
+
amplitude: Number(op.amplitude ?? 1),
|
|
165
|
+
frequency: Number(op.frequency ?? 1),
|
|
166
|
+
phase: Number(op.phase ?? 0),
|
|
167
|
+
offset: Number(op.offset ?? 0),
|
|
168
|
+
signalConfig: {
|
|
169
|
+
...op.signalConfig,
|
|
170
|
+
...op
|
|
171
|
+
},
|
|
172
|
+
frame,
|
|
173
|
+
fps,
|
|
174
|
+
elapsedMs: resolvedElapsedMs,
|
|
175
|
+
durationMs: resolvedDurationMs ?? 0,
|
|
176
|
+
width: targetWidth,
|
|
177
|
+
height: targetHeight,
|
|
178
|
+
drawChild: async (child, childProps, childView, childTex, childW, childH) => {
|
|
179
|
+
await drawCompositionTree(ctx, encoder, childView, childTex, childW, childH, child, {
|
|
180
|
+
...props,
|
|
181
|
+
durationMs: resolvedDurationMs,
|
|
182
|
+
elapsedMs: resolvedElapsedMs,
|
|
183
|
+
...childProps,
|
|
184
|
+
excludeTextures: [
|
|
185
|
+
...props.excludeTextures || [],
|
|
186
|
+
...childProps?.excludeTextures || [],
|
|
187
|
+
childTex
|
|
188
|
+
]
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
pass.end();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (op.op === "source" || op.op === "text" || op.op === "caption") {
|
|
196
|
+
const pass = ctx.renderer.beginFrame(encoder, targetView, {
|
|
197
|
+
r: 0,
|
|
198
|
+
g: 0,
|
|
199
|
+
b: 0,
|
|
200
|
+
a: 0
|
|
201
|
+
}, targetWidth, targetHeight, "load");
|
|
202
|
+
if (op.op === "source") {
|
|
203
|
+
const mediaType = getMediaType(virtualMedia);
|
|
204
|
+
const params = computeRenderParams(virtualMedia);
|
|
205
|
+
if (params.sourceUrl) {
|
|
206
|
+
const containerWidth = props.containerWidth ?? ctx.surface.width;
|
|
207
|
+
const containerHeight = props.containerHeight ?? ctx.surface.height;
|
|
208
|
+
if (mediaType === "Image") await drawImageNode(ctx, pass, {
|
|
209
|
+
src: params.sourceUrl,
|
|
210
|
+
dstRect: {
|
|
211
|
+
x: 0,
|
|
212
|
+
y: 0,
|
|
213
|
+
width: containerWidth,
|
|
214
|
+
height: containerHeight
|
|
215
|
+
},
|
|
216
|
+
opacity: Number(op.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
217
|
+
isHeadless: props.isHeadless
|
|
218
|
+
});
|
|
219
|
+
else if (mediaType === "SVG") await drawSvgNode(ctx, pass, {
|
|
220
|
+
src: params.sourceUrl,
|
|
221
|
+
dstRect: {
|
|
222
|
+
x: 0,
|
|
223
|
+
y: 0,
|
|
224
|
+
width: containerWidth,
|
|
225
|
+
height: containerHeight
|
|
226
|
+
},
|
|
227
|
+
opacity: Number(op.opacity ?? 1),
|
|
228
|
+
isHeadless: props.isHeadless
|
|
229
|
+
});
|
|
230
|
+
else if (mediaType === "Lottie") await drawLottieNode(ctx, pass, {
|
|
231
|
+
src: params.sourceUrl,
|
|
232
|
+
dstRect: {
|
|
233
|
+
x: 0,
|
|
234
|
+
y: 0,
|
|
235
|
+
width: containerWidth,
|
|
236
|
+
height: containerHeight
|
|
237
|
+
},
|
|
238
|
+
frame: segmentLocalFrame + Math.round(accumulatedSeekOffsetSec * fps),
|
|
239
|
+
fps,
|
|
240
|
+
opacity: Number(op.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
241
|
+
isHeadless: props.isHeadless
|
|
242
|
+
});
|
|
243
|
+
else if (mediaType === "GIF") await drawGifNode(ctx, pass, {
|
|
244
|
+
src: params.sourceUrl,
|
|
245
|
+
dstRect: {
|
|
246
|
+
x: 0,
|
|
247
|
+
y: 0,
|
|
248
|
+
width: containerWidth,
|
|
249
|
+
height: containerHeight
|
|
250
|
+
},
|
|
251
|
+
frame: segmentLocalFrame + Math.round(accumulatedSeekOffsetSec * fps),
|
|
252
|
+
fps,
|
|
253
|
+
opacity: Number(op.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
254
|
+
isHeadless: props.isHeadless
|
|
255
|
+
});
|
|
256
|
+
else if (mediaType === "Text") {
|
|
257
|
+
const opText = { ...op };
|
|
258
|
+
drawParagraphNode(ctx, pass, {
|
|
259
|
+
text: opText.text || "",
|
|
260
|
+
dstRect: {
|
|
261
|
+
x: Number(opText.x ?? 0),
|
|
262
|
+
y: Number(opText.y ?? 0),
|
|
263
|
+
width: Number(opText.width ?? containerWidth),
|
|
264
|
+
height: Number(opText.height ?? containerHeight)
|
|
265
|
+
},
|
|
266
|
+
fontFamily: opText.fontFamily,
|
|
267
|
+
fontSize: Number(opText.fontSize ?? 48),
|
|
268
|
+
color: opText.fill ?? opText.color ?? "white",
|
|
269
|
+
width: opText.width ? Number(opText.width) : void 0,
|
|
270
|
+
height: opText.height ? Number(opText.height) : void 0,
|
|
271
|
+
lineHeight: Number(opText.lineHeight ?? 1.2),
|
|
272
|
+
align: opText.align,
|
|
273
|
+
verticalAlign: opText.verticalAlign,
|
|
274
|
+
fontWeight: opText.fontWeight,
|
|
275
|
+
fontStyle: opText.fontStyle,
|
|
276
|
+
letterSpacing: opText.letterSpacing,
|
|
277
|
+
shadows: opText.shadows,
|
|
278
|
+
textBackgroundColor: opText.textBackgroundColor,
|
|
279
|
+
borderRadius: opText.borderRadius != null ? Number(opText.borderRadius) : void 0,
|
|
280
|
+
padding: opText.padding != null ? Number(opText.padding) : void 0,
|
|
281
|
+
stroke: opText.stroke,
|
|
282
|
+
strokeWidth: opText.strokeWidth ?? opText.strokeThickness ? Number(opText.strokeWidth ?? opText.strokeThickness) : void 0,
|
|
283
|
+
strokeAlign: opText.strokeAlign,
|
|
284
|
+
opacity: Number(opText.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
285
|
+
isHeadless: props.isHeadless,
|
|
286
|
+
animation: opText.animation ?? opText.textAnimation,
|
|
287
|
+
animators: opText.animators,
|
|
288
|
+
pathOptions: opText.pathOptions,
|
|
289
|
+
typewriter: opText.typewriter,
|
|
290
|
+
marquee: opText.marquee,
|
|
291
|
+
textProgress: opText.textProgress != null ? Number(opText.textProgress) : void 0,
|
|
292
|
+
signals: props.signals ?? opText.signals,
|
|
293
|
+
firstMargin: opText.firstMargin != null ? Number(opText.firstMargin) : void 0,
|
|
294
|
+
lastMargin: opText.lastMargin != null ? Number(opText.lastMargin) : void 0,
|
|
295
|
+
baselineShift: opText.baselineShift != null ? Number(opText.baselineShift) : void 0,
|
|
296
|
+
frame: localFrame,
|
|
297
|
+
fps,
|
|
298
|
+
durationMs: resolvedDurationMs,
|
|
299
|
+
elapsedMs: resolvedElapsedMs,
|
|
300
|
+
isVideoMode: props.isVideoMode
|
|
301
|
+
});
|
|
302
|
+
} else if (mediaType === "Video") {
|
|
303
|
+
const finalPlaybackRate = Math.max(.01, (Number(props.playbackRateOverride) || 1) * (Number(params.speed) || 1));
|
|
304
|
+
const effectiveTrimSec = Math.max(0, (Number(props.trimStartOverride) || 0) + (Number(params.trimStartSec) || 0) + accumulatedSeekOffsetSec);
|
|
305
|
+
const rawTimestampSec = segmentLocalFrame / fps * finalPlaybackRate + effectiveTrimSec;
|
|
306
|
+
const timestampSec = Math.max(0, Number.isFinite(rawTimestampSec) ? rawTimestampSec : 0);
|
|
307
|
+
await drawVideoNode(ctx, pass, {
|
|
308
|
+
nodeId: props.renderId,
|
|
309
|
+
frameKey: `${props.renderId ?? "video"}-${params.sourceUrl}-${Math.round(timestampSec * fps)}`,
|
|
310
|
+
sourceUrl: params.sourceUrl,
|
|
311
|
+
timestampSec,
|
|
312
|
+
dstRect: {
|
|
313
|
+
x: 0,
|
|
314
|
+
y: 0,
|
|
315
|
+
width: containerWidth,
|
|
316
|
+
height: containerHeight
|
|
317
|
+
},
|
|
318
|
+
opacity: Number(op.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
319
|
+
isHeadless: props.isHeadless,
|
|
320
|
+
isPlaying: props.isPlaying,
|
|
321
|
+
forceWait: props.forceWait
|
|
322
|
+
});
|
|
323
|
+
} else if (mediaType === "Caption") {
|
|
324
|
+
const opCap = {
|
|
325
|
+
...op,
|
|
326
|
+
...props
|
|
327
|
+
};
|
|
328
|
+
await drawCaptionNode(ctx, pass, {
|
|
329
|
+
renderId: props.renderId,
|
|
330
|
+
src: params.sourceUrl,
|
|
331
|
+
dstRect: {
|
|
332
|
+
x: 0,
|
|
333
|
+
y: 0,
|
|
334
|
+
width: containerWidth,
|
|
335
|
+
height: containerHeight
|
|
336
|
+
},
|
|
337
|
+
frame: localFrame,
|
|
338
|
+
fps,
|
|
339
|
+
fontFamily: opCap.fontFamily,
|
|
340
|
+
fontSize: Number(opCap.fontSize ?? 48),
|
|
341
|
+
color: opCap.fill ?? opCap.color ?? "white",
|
|
342
|
+
lineHeight: Number(opCap.lineHeight ?? 1.2),
|
|
343
|
+
align: opCap.align === "start" ? "left" : opCap.align === "end" ? "right" : opCap.align ?? "center",
|
|
344
|
+
textAlignVertical: opCap.verticalAlign ?? opCap.textAlignVertical,
|
|
345
|
+
fontWeight: opCap.fontWeight,
|
|
346
|
+
fontStyle: opCap.fontStyle,
|
|
347
|
+
letterSpacing: opCap.letterSpacing,
|
|
348
|
+
shadows: opCap.shadows,
|
|
349
|
+
textBackgroundColor: opCap.background ?? opCap.textBackgroundColor,
|
|
350
|
+
borderRadius: (opCap.borderRadius ?? opCap.strokeRadius) != null ? Number(opCap.borderRadius ?? opCap.strokeRadius) : void 0,
|
|
351
|
+
strokeRadius: (opCap.strokeRadius ?? opCap.borderRadius) != null ? Number(opCap.strokeRadius ?? opCap.borderRadius) : void 0,
|
|
352
|
+
stroke: opCap.stroke,
|
|
353
|
+
strokeWidth: (opCap.strokeWidth ?? opCap.strokeThickness) != null ? Number(opCap.strokeWidth ?? opCap.strokeThickness) : void 0,
|
|
354
|
+
maxWidth: opCap.maxWidth,
|
|
355
|
+
padding: opCap.padding != null ? Number(opCap.padding) : void 0,
|
|
356
|
+
opacity: Number(opCap.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
357
|
+
isHeadless: props.isHeadless,
|
|
358
|
+
isVideoMode: props.isVideoMode
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else if (op.op === "text") {
|
|
363
|
+
const opText = { ...op };
|
|
364
|
+
const containerWidth = props.containerWidth ?? ctx.surface.width;
|
|
365
|
+
const containerHeight = props.containerHeight ?? ctx.surface.height;
|
|
366
|
+
drawParagraphNode(ctx, pass, {
|
|
367
|
+
renderId: props.renderId,
|
|
368
|
+
text: opText.text || "",
|
|
369
|
+
dstRect: {
|
|
370
|
+
x: Number(opText.x ?? 0),
|
|
371
|
+
y: Number(opText.y ?? 0),
|
|
372
|
+
width: Number(opText.width ?? containerWidth),
|
|
373
|
+
height: Number(opText.height ?? containerHeight)
|
|
374
|
+
},
|
|
375
|
+
spans: opText.spans,
|
|
376
|
+
fontFamily: opText.fontFamily,
|
|
377
|
+
fontSize: Number(opText.fontSize ?? 48),
|
|
378
|
+
color: opText.fill ?? opText.color ?? "white",
|
|
379
|
+
width: opText.width ? Number(opText.width) : void 0,
|
|
380
|
+
height: opText.height ? Number(opText.height) : void 0,
|
|
381
|
+
lineHeight: Number(opText.lineHeight ?? 1.2),
|
|
382
|
+
align: opText.align,
|
|
383
|
+
verticalAlign: opText.verticalAlign,
|
|
384
|
+
fontWeight: opText.fontWeight,
|
|
385
|
+
fontStyle: opText.fontStyle,
|
|
386
|
+
letterSpacing: opText.letterSpacing,
|
|
387
|
+
shadows: opText.shadows,
|
|
388
|
+
textBackgroundColor: opText.textBackgroundColor,
|
|
389
|
+
borderRadius: (opText.borderRadius ?? opText.strokeRadius) != null ? Number(opText.borderRadius ?? opText.strokeRadius) : void 0,
|
|
390
|
+
strokeRadius: (opText.strokeRadius ?? opText.borderRadius) != null ? Number(opText.strokeRadius ?? opText.borderRadius) : void 0,
|
|
391
|
+
padding: opText.padding != null ? Number(opText.padding) : void 0,
|
|
392
|
+
stroke: opText.stroke,
|
|
393
|
+
strokeWidth: opText.strokeWidth ?? opText.strokeThickness ? Number(opText.strokeWidth ?? opText.strokeThickness) : void 0,
|
|
394
|
+
strokeAlign: opText.strokeAlign,
|
|
395
|
+
opacity: Number(opText.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
396
|
+
isHeadless: props.isHeadless,
|
|
397
|
+
animation: opText.animation ?? opText.textAnimation,
|
|
398
|
+
animators: opText.animators,
|
|
399
|
+
pathOptions: opText.pathOptions,
|
|
400
|
+
typewriter: opText.typewriter,
|
|
401
|
+
marquee: opText.marquee,
|
|
402
|
+
signals: props.signals ?? opText.signals,
|
|
403
|
+
firstMargin: opText.firstMargin != null ? Number(opText.firstMargin) : void 0,
|
|
404
|
+
lastMargin: opText.lastMargin != null ? Number(opText.lastMargin) : void 0,
|
|
405
|
+
baselineShift: opText.baselineShift != null ? Number(opText.baselineShift) : void 0,
|
|
406
|
+
frame: localFrame,
|
|
407
|
+
fps,
|
|
408
|
+
durationMs: resolvedDurationMs,
|
|
409
|
+
elapsedMs: resolvedElapsedMs,
|
|
410
|
+
isVideoMode: props.isVideoMode
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
pass.end();
|
|
414
|
+
}
|
|
415
|
+
if (virtualMedia.children && virtualMedia.children.length > 0) for (let i = 0; i < virtualMedia.children.length; i++) {
|
|
416
|
+
const child = virtualMedia.children[i];
|
|
417
|
+
if (child) await drawCompositionTree(ctx, encoder, targetView, targetTexture, targetWidth, targetHeight, child, {
|
|
418
|
+
...props,
|
|
419
|
+
durationMs: resolvedDurationMs,
|
|
420
|
+
elapsedMs: resolvedElapsedMs,
|
|
421
|
+
virtualMedia: child,
|
|
422
|
+
renderId: `${props.renderId}-c${i}`
|
|
423
|
+
}, isCompositor ? 0 : accumulatedSeekOffsetSec, accumulatedClockOffsetSec);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/compositions/composition-state.ts
|
|
429
|
+
const DEFAULT_STATE = {
|
|
430
|
+
frame: 0,
|
|
431
|
+
fps: 30,
|
|
432
|
+
isPlaying: false
|
|
433
|
+
};
|
|
434
|
+
var CompositionStateStore = class {
|
|
435
|
+
states = /* @__PURE__ */ new Map();
|
|
436
|
+
listeners = /* @__PURE__ */ new Map();
|
|
437
|
+
setState(key, frame, fps, isPlaying) {
|
|
438
|
+
const current = this.states.get(key);
|
|
439
|
+
const resolvedPlaying = isPlaying !== void 0 ? isPlaying : current?.isPlaying ?? false;
|
|
440
|
+
if (current?.frame === frame && current?.fps === fps && current?.isPlaying === resolvedPlaying) return;
|
|
441
|
+
this.states.set(key, {
|
|
442
|
+
frame,
|
|
443
|
+
fps,
|
|
444
|
+
isPlaying: resolvedPlaying
|
|
445
|
+
});
|
|
446
|
+
this.notify(key);
|
|
447
|
+
}
|
|
448
|
+
getState(key) {
|
|
449
|
+
if (this.states.has(key)) return this.states.get(key);
|
|
450
|
+
for (const [storeKey, value] of this.states.entries()) if (key.startsWith(storeKey)) return value;
|
|
451
|
+
return DEFAULT_STATE;
|
|
452
|
+
}
|
|
453
|
+
subscribe(key, listener) {
|
|
454
|
+
if (!this.listeners.has(key)) this.listeners.set(key, /* @__PURE__ */ new Set());
|
|
455
|
+
this.listeners.get(key).add(listener);
|
|
456
|
+
return () => {
|
|
457
|
+
const set = this.listeners.get(key);
|
|
458
|
+
set?.delete(listener);
|
|
459
|
+
if (set?.size === 0) {
|
|
460
|
+
this.listeners.delete(key);
|
|
461
|
+
this.states.delete(key);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
clearKey(key) {
|
|
466
|
+
this.states.delete(key);
|
|
467
|
+
}
|
|
468
|
+
notify(key) {
|
|
469
|
+
for (const [listenerKey, set] of this.listeners.entries()) if (listenerKey === key || listenerKey.startsWith(key)) set.forEach((l) => l());
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
const compositionStateStore = new CompositionStateStore();
|
|
473
|
+
|
|
474
|
+
//#endregion
|
|
475
|
+
//#region src/utils/normalization.ts
|
|
476
|
+
/**
|
|
477
|
+
* normalizeTimeline recurses through the virtual media tree and "absorbs"
|
|
478
|
+
* generic timeline-shifting operations into standard properties
|
|
479
|
+
* of their children.
|
|
480
|
+
*/
|
|
481
|
+
function normalizeTimeline(vv, fps, context = { timeOffsetSec: 0 }) {
|
|
482
|
+
if (!vv) return vv;
|
|
483
|
+
const { operation, metadata = {}, children = [] } = vv;
|
|
484
|
+
const { timeOffsetSec, durationLimitMs } = context;
|
|
485
|
+
if (!operation) return {
|
|
486
|
+
...vv,
|
|
487
|
+
children: (children || []).map((child) => normalizeTimeline(child, fps, context))
|
|
488
|
+
};
|
|
489
|
+
const timeline = operation.timeline;
|
|
490
|
+
const segments = timeline?.segments || [];
|
|
491
|
+
let totalDurationMs = getActiveMediaMetadata(vv)?.durationMs ?? 0;
|
|
492
|
+
if (segments.length > 0) {
|
|
493
|
+
const totalSegmentsFrames = segments.reduce((acc, seg) => {
|
|
494
|
+
const durSec = seg.endSec != null ? seg.endSec - seg.startSec : 0;
|
|
495
|
+
return acc + Math.max(1, Math.round(durSec * fps));
|
|
496
|
+
}, 0);
|
|
497
|
+
if (totalSegmentsFrames > 0) totalDurationMs = totalSegmentsFrames / fps * 1e3;
|
|
498
|
+
}
|
|
499
|
+
const newOperation = { ...operation };
|
|
500
|
+
const rawStartFrame = timeline?.startFrame ?? newOperation.startFrame;
|
|
501
|
+
if (typeof rawStartFrame === "number") newOperation.startFrame = rawStartFrame - secondsToFrames(timeOffsetSec, fps);
|
|
502
|
+
const newMetadata = {
|
|
503
|
+
...metadata,
|
|
504
|
+
durationMs: totalDurationMs
|
|
505
|
+
};
|
|
506
|
+
const childContext = typeof rawStartFrame === "number" || newOperation.op === "compose" ? {
|
|
507
|
+
timeOffsetSec: 0,
|
|
508
|
+
durationLimitMs: void 0
|
|
509
|
+
} : {
|
|
510
|
+
timeOffsetSec,
|
|
511
|
+
durationLimitMs
|
|
512
|
+
};
|
|
513
|
+
return {
|
|
514
|
+
metadata: newMetadata,
|
|
515
|
+
operation: newOperation,
|
|
516
|
+
children: (children || []).map((child) => normalizeTimeline(child, fps, childContext))
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/utils/audio-extractor.ts
|
|
522
|
+
const verbose = typeof process !== "undefined" && /^(debug|trace)$/i.test(process.env?.LOG_LEVEL ?? "");
|
|
523
|
+
let mediaLogger = {
|
|
524
|
+
debug: (msg, ...args) => {
|
|
525
|
+
if (!verbose) return;
|
|
526
|
+
if (typeof msg === "object" && msg !== null) console.info("[AudioExtractor]", msg, ...args);
|
|
527
|
+
else console.info(`[AudioExtractor] ${msg}`, ...args);
|
|
528
|
+
},
|
|
529
|
+
warn: (msg, ...args) => {
|
|
530
|
+
if (typeof msg === "object" && msg !== null) console.warn("[AudioExtractor]", msg, ...args);
|
|
531
|
+
else console.warn(`[AudioExtractor] ${msg}`, ...args);
|
|
532
|
+
},
|
|
533
|
+
error: (msg, ...args) => {
|
|
534
|
+
if (typeof msg === "object" && msg !== null) console.error("[AudioExtractor]", msg, ...args);
|
|
535
|
+
else console.error(`[AudioExtractor] ${msg}`, ...args);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
if (typeof window === "undefined" || typeof process !== "undefined" && process.versions?.node) import(
|
|
539
|
+
/* webpackIgnore: true */
|
|
540
|
+
/* @vite-ignore */
|
|
541
|
+
"@framefields/server-utils"
|
|
542
|
+
).then((m) => {
|
|
543
|
+
if (m.mediaLogger) mediaLogger = m.mediaLogger;
|
|
544
|
+
}).catch(() => {});
|
|
545
|
+
const yieldToMain = () => {
|
|
546
|
+
if (typeof window === "undefined") return Promise.resolve();
|
|
547
|
+
const sched = globalThis.scheduler;
|
|
548
|
+
return typeof sched?.yield === "function" ? sched.yield() : new Promise((resolve) => setTimeout(resolve, 0));
|
|
549
|
+
};
|
|
550
|
+
const waitForDelayRender = async (timeoutMs = 3e4) => {
|
|
551
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
552
|
+
const start = Date.now();
|
|
553
|
+
return new Promise((resolve) => {
|
|
554
|
+
const check = () => {
|
|
555
|
+
const browserHandles = typeof window !== "undefined" && window.renderer_delayRenderHandles ? window.renderer_delayRenderHandles : [];
|
|
556
|
+
const globalDelays = typeof globalThis !== "undefined" ? globalThis.__GATEWAI_DELAYS__ : null;
|
|
557
|
+
const hasBrowserDelays = browserHandles.length > 0;
|
|
558
|
+
const hasGlobalDelays = globalDelays instanceof Set && globalDelays.size > 0;
|
|
559
|
+
if (!hasBrowserDelays && !hasGlobalDelays) {
|
|
560
|
+
resolve();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (Date.now() - start > timeoutMs) {
|
|
564
|
+
mediaLogger.warn(`[AudioExtractor] waitForDelayRender timed out waiting for handles: ${JSON.stringify({
|
|
565
|
+
browserHandles,
|
|
566
|
+
globalDelays: globalDelays ? Array.from(globalDelays) : []
|
|
567
|
+
})}`);
|
|
568
|
+
resolve();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
setTimeout(check, 50);
|
|
572
|
+
};
|
|
573
|
+
check();
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
/**
|
|
577
|
+
* Decodes all audio samples from a given source URL into raw Float32 mono/stereo channel buffers.
|
|
578
|
+
*/
|
|
579
|
+
async function decodeAudioSource(url) {
|
|
580
|
+
mediaLogger.debug(`[AudioExtractor] decodeAudioSource started for url: ${url}`);
|
|
581
|
+
try {
|
|
582
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined" && !(typeof process !== "undefined" && process.versions && process.versions.node)) {
|
|
583
|
+
mediaLogger.debug(`[AudioExtractor] Browser detected. Fetching arrayBuffer for ${url}...`);
|
|
584
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
585
|
+
if (!AudioContextClass) throw new Error("AudioContext is not supported in this browser");
|
|
586
|
+
const audioCtx = new AudioContextClass();
|
|
587
|
+
try {
|
|
588
|
+
const response = await fetch(url);
|
|
589
|
+
mediaLogger.debug(`[AudioExtractor] Fetch response status for ${url}: ${response.status} (${response.statusText})`);
|
|
590
|
+
if (!response.ok) throw new Error(`Failed to fetch audio source: ${response.statusText}`);
|
|
591
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
592
|
+
mediaLogger.debug(`[AudioExtractor] Fetched ${arrayBuffer.byteLength} bytes. Decoding audio data...`);
|
|
593
|
+
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
|
|
594
|
+
mediaLogger.debug(`[AudioExtractor] Decoded successfully: ${audioBuffer.numberOfChannels} channels, sampleRate: ${audioBuffer.sampleRate}, duration: ${audioBuffer.duration}s`);
|
|
595
|
+
const channels = [];
|
|
596
|
+
for (let c = 0; c < audioBuffer.numberOfChannels; c++) channels.push(new Float32Array(audioBuffer.getChannelData(c)));
|
|
597
|
+
return {
|
|
598
|
+
channels,
|
|
599
|
+
sampleRate: audioBuffer.sampleRate
|
|
600
|
+
};
|
|
601
|
+
} finally {
|
|
602
|
+
audioCtx.close();
|
|
603
|
+
mediaLogger.debug(`[AudioExtractor] AudioContext closed for ${url}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
let inputAcquired = false;
|
|
607
|
+
try {
|
|
608
|
+
mediaLogger.debug(`[AudioExtractor] Server / Node.js detected. Acquiring from inputStore for ${url}...`);
|
|
609
|
+
const input = await inputStore.acquire(url);
|
|
610
|
+
inputAcquired = true;
|
|
611
|
+
mediaLogger.debug(`[AudioExtractor] inputStore acquired. Retrieving primary audio track...`);
|
|
612
|
+
const track = await input.getPrimaryAudioTrack();
|
|
613
|
+
if (!track) {
|
|
614
|
+
mediaLogger.warn(`[AudioExtractor] No primary audio track found in input for ${url}`);
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const generator = new AudioSampleSink(track).samples(0);
|
|
618
|
+
const decodedPlanes = [];
|
|
619
|
+
let totalFrames = 0;
|
|
620
|
+
let sampleRate = 48e3;
|
|
621
|
+
let numChannels = 1;
|
|
622
|
+
mediaLogger.debug(`[AudioExtractor] Starting mediabunny generator loop for ${url}...`);
|
|
623
|
+
for await (const sample of generator) {
|
|
624
|
+
sampleRate = sample.sampleRate;
|
|
625
|
+
numChannels = sample.numberOfChannels;
|
|
626
|
+
const sampleFrames = sample.numberOfFrames;
|
|
627
|
+
if (decodedPlanes.length === 0) for (let c = 0; c < numChannels; c++) decodedPlanes.push([]);
|
|
628
|
+
for (let c = 0; c < numChannels; c++) {
|
|
629
|
+
let planeBuffer;
|
|
630
|
+
try {
|
|
631
|
+
const allocSize = sample.allocationSize({
|
|
632
|
+
planeIndex: c,
|
|
633
|
+
format: "f32-planar"
|
|
634
|
+
});
|
|
635
|
+
planeBuffer = new Float32Array(allocSize / 4);
|
|
636
|
+
sample.copyTo(planeBuffer, {
|
|
637
|
+
planeIndex: c,
|
|
638
|
+
format: "f32-planar"
|
|
639
|
+
});
|
|
640
|
+
} catch {
|
|
641
|
+
const allocSize = sample.allocationSize({ planeIndex: c });
|
|
642
|
+
if (sample.format === "s16" || sample.format === "s16-planar") {
|
|
643
|
+
const int16Buffer = new Int16Array(allocSize / 2);
|
|
644
|
+
sample.copyTo(int16Buffer, { planeIndex: c });
|
|
645
|
+
planeBuffer = new Float32Array(int16Buffer.length);
|
|
646
|
+
for (let i = 0; i < int16Buffer.length; i++) planeBuffer[i] = (int16Buffer[i] ?? 0) / 32768;
|
|
647
|
+
} else if (sample.format === "s32" || sample.format === "s32-planar") {
|
|
648
|
+
const int32Buffer = new Int32Array(allocSize / 4);
|
|
649
|
+
sample.copyTo(int32Buffer, { planeIndex: c });
|
|
650
|
+
planeBuffer = new Float32Array(int32Buffer.length);
|
|
651
|
+
for (let i = 0; i < int32Buffer.length; i++) planeBuffer[i] = (int32Buffer[i] ?? 0) / 2147483648;
|
|
652
|
+
} else if (sample.format === "u8" || sample.format === "u8-planar") {
|
|
653
|
+
const u8Buffer = new Uint8Array(allocSize);
|
|
654
|
+
sample.copyTo(u8Buffer, { planeIndex: c });
|
|
655
|
+
planeBuffer = new Float32Array(u8Buffer.length);
|
|
656
|
+
for (let i = 0; i < u8Buffer.length; i++) planeBuffer[i] = ((u8Buffer[i] ?? 128) - 128) / 128;
|
|
657
|
+
} else {
|
|
658
|
+
planeBuffer = new Float32Array(allocSize / 4);
|
|
659
|
+
sample.copyTo(planeBuffer, { planeIndex: c });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
decodedPlanes[c].push(planeBuffer);
|
|
663
|
+
}
|
|
664
|
+
totalFrames += sampleFrames;
|
|
665
|
+
sample.close();
|
|
666
|
+
}
|
|
667
|
+
mediaLogger.debug(`[AudioExtractor] Generator loop finished. Decoded total frames: ${totalFrames}`);
|
|
668
|
+
if (totalFrames === 0 || decodedPlanes.length === 0) return null;
|
|
669
|
+
return {
|
|
670
|
+
channels: decodedPlanes.map((planes) => {
|
|
671
|
+
const merged = new Float32Array(totalFrames);
|
|
672
|
+
let offset = 0;
|
|
673
|
+
for (const plane of planes) {
|
|
674
|
+
merged.set(plane, offset);
|
|
675
|
+
offset += plane.length;
|
|
676
|
+
}
|
|
677
|
+
return merged;
|
|
678
|
+
}),
|
|
679
|
+
sampleRate
|
|
680
|
+
};
|
|
681
|
+
} catch (err) {
|
|
682
|
+
if (typeof process !== "undefined" && process.versions?.node) try {
|
|
683
|
+
const { execSync } = await import("node:child_process");
|
|
684
|
+
const stdout = execSync(`ffmpeg -v error -i "${url}" -f f32le -ar 48000 -ac 2 pipe:1`, { maxBuffer: 100 * 1024 * 1024 });
|
|
685
|
+
const totalSamples = stdout.byteLength / 4;
|
|
686
|
+
const numFrames = totalSamples / 2;
|
|
687
|
+
const interleaved = new Float32Array(stdout.buffer, stdout.byteOffset, totalSamples);
|
|
688
|
+
const left = new Float32Array(numFrames);
|
|
689
|
+
const right = new Float32Array(numFrames);
|
|
690
|
+
for (let i = 0; i < numFrames; i++) {
|
|
691
|
+
left[i] = interleaved[i * 2];
|
|
692
|
+
right[i] = interleaved[i * 2 + 1];
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
channels: [left, right],
|
|
696
|
+
sampleRate: 48e3
|
|
697
|
+
};
|
|
698
|
+
} catch (ffmpegErr) {
|
|
699
|
+
mediaLogger.warn({ ffmpegErr }, `[AudioExtractor] ffmpeg fallback decode failed for ${url}`);
|
|
700
|
+
}
|
|
701
|
+
mediaLogger.error({ err }, `[AudioExtractor] Failed to decode source ${url}`);
|
|
702
|
+
return null;
|
|
703
|
+
} finally {
|
|
704
|
+
if (inputAcquired) inputStore.release(url);
|
|
705
|
+
}
|
|
706
|
+
} catch (err) {
|
|
707
|
+
mediaLogger.error({ err }, `[AudioExtractor] Failed to decode source ${url}`);
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Resamples a PCM buffer using linear interpolation to target sample rate & speed.
|
|
713
|
+
*/
|
|
714
|
+
async function resampleAndStretchPCM(inputChannel, sourceSampleRate, targetSampleRate, speed, outputLength, trimOffsetSamples) {
|
|
715
|
+
const output = new Float32Array(outputLength);
|
|
716
|
+
const factor = sourceSampleRate / targetSampleRate * speed;
|
|
717
|
+
const YIELD_INTERVAL = 1e5;
|
|
718
|
+
for (let i = 0; i < outputLength; i++) {
|
|
719
|
+
if (i > 0 && i % YIELD_INTERVAL === 0) await yieldToMain();
|
|
720
|
+
const srcIndex = trimOffsetSamples + i * factor;
|
|
721
|
+
const indexFloor = Math.floor(srcIndex);
|
|
722
|
+
const indexCeil = Math.ceil(srcIndex);
|
|
723
|
+
if (indexFloor < 0) {
|
|
724
|
+
output[i] = 0;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (indexFloor >= inputChannel.length) break;
|
|
728
|
+
const weight = srcIndex - indexFloor;
|
|
729
|
+
const sampleFloor = inputChannel[indexFloor] ?? 0;
|
|
730
|
+
const val = sampleFloor + weight * ((indexCeil >= 0 && indexCeil < inputChannel.length ? inputChannel[indexCeil] ?? sampleFloor : sampleFloor) - sampleFloor);
|
|
731
|
+
output[i] = Number.isFinite(val) ? val : 0;
|
|
732
|
+
}
|
|
733
|
+
return output;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Recursively parses the VirtualMediaData tree and mixes all active audio tracks
|
|
737
|
+
* into a single unified stereo PCM stream (Float32Array channels).
|
|
738
|
+
*/
|
|
739
|
+
async function mixAudioTracks(virtualMedia, fps, targetSampleRate = 48e3, device, renderId) {
|
|
740
|
+
mediaLogger.debug(`[AudioExtractor] mixAudioTracks started. FPS: ${fps}, targetSampleRate: ${targetSampleRate}`);
|
|
741
|
+
await waitForDelayRender();
|
|
742
|
+
shaderStore.registerSignalsFromComposition(renderId, virtualMedia);
|
|
743
|
+
const durationMs = getActiveMediaMetadata(virtualMedia)?.durationMs || 1e3;
|
|
744
|
+
const totalDurationSec = durationMs / 1e3;
|
|
745
|
+
const totalTargetSamples = Math.ceil(totalDurationSec * targetSampleRate);
|
|
746
|
+
mediaLogger.debug(`[AudioExtractor] Composition total duration: ${durationMs}ms (${totalDurationSec}s). Total mixed samples to allocate: ${totalTargetSamples}`);
|
|
747
|
+
const outputLeft = new Float32Array(totalTargetSamples);
|
|
748
|
+
const outputRight = new Float32Array(totalTargetSamples);
|
|
749
|
+
const mixedChannels = [outputLeft, outputRight];
|
|
750
|
+
const decodedCache = /* @__PURE__ */ new Map();
|
|
751
|
+
async function traverse(node, inheritedSeekSec, inheritedClockFrames, inheritedVolume, inheritedSpeed, inheritedDurationFrames) {
|
|
752
|
+
if (!node) return null;
|
|
753
|
+
const op = node.operation;
|
|
754
|
+
if (!op) {
|
|
755
|
+
const durationMs$1 = getActiveMediaMetadata(node)?.durationMs ?? 0;
|
|
756
|
+
const durationFrames = Math.round(durationMs$1 / 1e3 * fps);
|
|
757
|
+
const actualDurationFrames = inheritedDurationFrames !== null ? durationFrames > 0 ? Math.min(inheritedDurationFrames, durationFrames) : inheritedDurationFrames : durationFrames;
|
|
758
|
+
const localSamplesCount$1 = Math.ceil(actualDurationFrames / fps * targetSampleRate);
|
|
759
|
+
if (localSamplesCount$1 <= 0) return null;
|
|
760
|
+
const localChannels$1 = [new Float32Array(localSamplesCount$1), new Float32Array(localSamplesCount$1)];
|
|
761
|
+
for (const child of node.children) {
|
|
762
|
+
const childChannels = await traverse(child, inheritedSeekSec, inheritedClockFrames, inheritedVolume, inheritedSpeed, actualDurationFrames);
|
|
763
|
+
if (childChannels) {
|
|
764
|
+
const childOp = child.operation;
|
|
765
|
+
const childStartFrame = childOp?.startFrame ?? childOp?.timeline?.startFrame ?? 0;
|
|
766
|
+
const childStartSample = Math.round(childStartFrame / fps * targetSampleRate);
|
|
767
|
+
for (let c = 0; c < 2; c++) {
|
|
768
|
+
const dest = localChannels$1[c];
|
|
769
|
+
const src = childChannels[c];
|
|
770
|
+
const len = Math.min(src.length, dest.length - childStartSample);
|
|
771
|
+
for (let i = 0; i < len; i++) if (childStartSample + i >= 0) dest[childStartSample + i] += src[i];
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return localChannels$1;
|
|
776
|
+
}
|
|
777
|
+
const volume = inheritedVolume * (op.volume ?? 1) * (op.opacity === 0 ? 0 : 1) * (op.muted ? 0 : 1);
|
|
778
|
+
const speedMultiplier = inheritedSpeed;
|
|
779
|
+
const localClockFrames = inheritedClockFrames + (op.startFrame ?? op.timeline?.startFrame ?? 0);
|
|
780
|
+
const segments = op.timeline?.segments || [];
|
|
781
|
+
const nodeDurationMs = getActiveMediaMetadata(node)?.durationMs ?? 0;
|
|
782
|
+
const nodeDurationFrames = Math.round(nodeDurationMs / 1e3 * fps);
|
|
783
|
+
const nodeActualDurationFrames = inheritedDurationFrames !== null ? nodeDurationFrames > 0 ? Math.min(inheritedDurationFrames, nodeDurationFrames) : inheritedDurationFrames : nodeDurationFrames;
|
|
784
|
+
const localSamplesCount = Math.ceil(nodeActualDurationFrames / fps * targetSampleRate);
|
|
785
|
+
if (localSamplesCount <= 0) return null;
|
|
786
|
+
const localLeft = new Float32Array(localSamplesCount);
|
|
787
|
+
const localRight = new Float32Array(localSamplesCount);
|
|
788
|
+
const localChannels = [localLeft, localRight];
|
|
789
|
+
const processSegment = async (seg, windowStartFrameOffset) => {
|
|
790
|
+
const totalSeekSec = inheritedSeekSec + (seg ? seg.startSec : 0);
|
|
791
|
+
const totalClockFrames = localClockFrames + windowStartFrameOffset;
|
|
792
|
+
let segDurationFrames = 0;
|
|
793
|
+
if (seg) segDurationFrames = Math.max(1, Math.round(((seg.endSec ?? 0) - seg.startSec) * fps));
|
|
794
|
+
else segDurationFrames = nodeDurationFrames;
|
|
795
|
+
const actualDurationFrames = inheritedDurationFrames !== null ? Math.min(inheritedDurationFrames, segDurationFrames) : segDurationFrames;
|
|
796
|
+
const segmentStartSample = Math.round(windowStartFrameOffset / fps * targetSampleRate);
|
|
797
|
+
if (op.op === "source") {
|
|
798
|
+
const mediaType = getMediaType(node);
|
|
799
|
+
const params = computeRenderParams(node);
|
|
800
|
+
if (params.sourceUrl && (mediaType === "Audio" || mediaType === "Video") && volume > 0) {
|
|
801
|
+
const finalSpeed = Math.max(.5, speedMultiplier * (Number(params.speed) || 1));
|
|
802
|
+
const effectiveTrimSec = Math.max(0, (Number(params.trimStartSec) || 0) + totalSeekSec);
|
|
803
|
+
let decoded = decodedCache.get(params.sourceUrl);
|
|
804
|
+
if (decoded === void 0) {
|
|
805
|
+
decoded = await decodeAudioSource(params.sourceUrl);
|
|
806
|
+
decodedCache.set(params.sourceUrl, decoded);
|
|
807
|
+
}
|
|
808
|
+
if (decoded) {
|
|
809
|
+
const clipDurationSec = actualDurationFrames / fps;
|
|
810
|
+
const clipSamplesCount = Math.min(localSamplesCount - segmentStartSample, Math.ceil(clipDurationSec * targetSampleRate));
|
|
811
|
+
if (clipSamplesCount > 0 && segmentStartSample < localSamplesCount) {
|
|
812
|
+
const trimOffsetSamples = Math.round(effectiveTrimSec * decoded.sampleRate);
|
|
813
|
+
const srcLeft = decoded.channels[0];
|
|
814
|
+
const srcRight = decoded.channels.length > 1 ? decoded.channels[1] : decoded.channels[0];
|
|
815
|
+
const stretchedLeft = await resampleAndStretchPCM(srcLeft, decoded.sampleRate, targetSampleRate, finalSpeed, clipSamplesCount, trimOffsetSamples);
|
|
816
|
+
const stretchedRight = await resampleAndStretchPCM(srcRight, decoded.sampleRate, targetSampleRate, finalSpeed, clipSamplesCount, trimOffsetSamples);
|
|
817
|
+
for (let i = 0; i < clipSamplesCount; i++) {
|
|
818
|
+
const outIndex = segmentStartSample + i;
|
|
819
|
+
localLeft[outIndex] += stretchedLeft[i] * volume;
|
|
820
|
+
localRight[outIndex] += stretchedRight[i] * volume;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
await yieldToMain();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (node.children) for (const child of node.children) {
|
|
828
|
+
const childChannels = await traverse(child, totalSeekSec, totalClockFrames, volume, speedMultiplier, actualDurationFrames);
|
|
829
|
+
if (childChannels) {
|
|
830
|
+
const childOp = child.operation;
|
|
831
|
+
const childStartFrame = childOp?.startFrame ?? childOp?.timeline?.startFrame ?? 0;
|
|
832
|
+
const destStartSample = segmentStartSample + Math.round(childStartFrame / fps * targetSampleRate);
|
|
833
|
+
for (let c = 0; c < 2; c++) {
|
|
834
|
+
const dest = localChannels[c];
|
|
835
|
+
const src = childChannels[c];
|
|
836
|
+
const len = Math.min(src.length, dest.length - destStartSample);
|
|
837
|
+
for (let i = 0; i < len; i++) if (destStartSample + i >= 0) dest[destStartSample + i] += src[i];
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const pluginProcessor = audioRegistry.get(op.op);
|
|
842
|
+
if (pluginProcessor) try {
|
|
843
|
+
const clipDurationSec = actualDurationFrames / fps;
|
|
844
|
+
const clipSamplesCount = Math.min(localSamplesCount - segmentStartSample, Math.ceil(clipDurationSec * targetSampleRate));
|
|
845
|
+
if (clipSamplesCount > 0 && segmentStartSample < localSamplesCount && segmentStartSample >= 0) await pluginProcessor(localChannels.map((ch) => ch.subarray(segmentStartSample, segmentStartSample + clipSamplesCount)), targetSampleRate, node, {
|
|
846
|
+
device,
|
|
847
|
+
frame: totalClockFrames,
|
|
848
|
+
fps,
|
|
849
|
+
renderId,
|
|
850
|
+
elapsedMs: (totalClockFrames / fps + totalSeekSec) * 1e3,
|
|
851
|
+
durationMs: getActiveMediaMetadata(node)?.durationMs ?? void 0
|
|
852
|
+
});
|
|
853
|
+
} catch (pluginErr) {
|
|
854
|
+
mediaLogger.error({ err: pluginErr }, `[AudioExtractor] Custom plugin for operation '${op.op}' failed`);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
if (segments.length > 0) {
|
|
858
|
+
let currentStart = 0;
|
|
859
|
+
for (const seg of segments) {
|
|
860
|
+
const durSec = (seg.endSec ?? 0) - seg.startSec;
|
|
861
|
+
const durFrames = Math.max(1, Math.round(durSec * fps));
|
|
862
|
+
await processSegment(seg, currentStart);
|
|
863
|
+
currentStart += durFrames;
|
|
864
|
+
}
|
|
865
|
+
} else await processSegment(null, 0);
|
|
866
|
+
return localChannels;
|
|
867
|
+
}
|
|
868
|
+
const rootChannels = await traverse(normalizeTimeline(virtualMedia, fps), 0, 0, 1, 1, null);
|
|
869
|
+
if (rootChannels) for (let c = 0; c < 2; c++) {
|
|
870
|
+
const dest = mixedChannels[c];
|
|
871
|
+
const src = rootChannels[c];
|
|
872
|
+
const len = Math.min(src.length, dest.length);
|
|
873
|
+
for (let i = 0; i < len; i++) dest[i] = src[i];
|
|
874
|
+
}
|
|
875
|
+
decodedCache.clear();
|
|
876
|
+
let mixPeak = 0;
|
|
877
|
+
for (let i = 0; i < totalTargetSamples; i++) {
|
|
878
|
+
const l = Math.abs(outputLeft[i] ?? 0);
|
|
879
|
+
const r = Math.abs(outputRight[i] ?? 0);
|
|
880
|
+
if (l > mixPeak) mixPeak = l;
|
|
881
|
+
if (r > mixPeak) mixPeak = r;
|
|
882
|
+
}
|
|
883
|
+
const autoGain = mixPeak > .95 ? .92 / mixPeak : 1;
|
|
884
|
+
for (let i = 0; i < totalTargetSamples; i++) {
|
|
885
|
+
const l = (outputLeft[i] ?? 0) * autoGain;
|
|
886
|
+
const r = (outputRight[i] ?? 0) * autoGain;
|
|
887
|
+
outputLeft[i] = Number.isFinite(l) ? Math.tanh(l) : 0;
|
|
888
|
+
outputRight[i] = Number.isFinite(r) ? Math.tanh(r) : 0;
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
channels: mixedChannels,
|
|
892
|
+
sampleRate: targetSampleRate
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
//#endregion
|
|
897
|
+
export { calculateLayerTransform, compositionStateStore, computeRenderParams, decodeAudioSource, drawCompositionTree, formatTimecode, framesToSeconds, getFontUrl, isStaticVisualMedia, mixAudioTracks, normalizeTimeline, resolveLayerDuration, secondsToFrames };
|
|
898
|
+
//# sourceMappingURL=index.mjs.map
|