@framefields/compositions 0.0.0-stage → 2.0.2

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/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