@bendyline/squisq-video-react 2.5.27 → 2.5.29

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.
@@ -7,7 +7,7 @@ var inlineDenoiser = createRnnoiseDenoiserFactory(
7
7
  () => import("@shiguredo/rnnoise-wasm").then(({ Rnnoise }) => Rnnoise.load())
8
8
  );
9
9
  async function inlineEngine() {
10
- return import("./mediaEditEngine-3WUTBNJN.js");
10
+ return import("./mediaEditEngine-22T4WV4X.js");
11
11
  }
12
12
  function inlineOptions(options) {
13
13
  return { ...options, denoiser: options.denoiser ?? inlineDenoiser };
@@ -108,6 +108,20 @@ function createMediaEditRenderer(options = {}) {
108
108
  async () => (await inlineEngine()).analyzeMediaEditPauses(request, jobOptions)
109
109
  );
110
110
  },
111
+ waveform(request, jobOptions = {}) {
112
+ return dispatch(
113
+ { ...request, type: "waveform", id: nextId++ },
114
+ jobOptions,
115
+ async () => (await inlineEngine()).analyzeMediaEditWaveform(request, jobOptions)
116
+ );
117
+ },
118
+ bake(request, jobOptions = {}) {
119
+ return dispatch(
120
+ { ...request, type: "bake", id: nextId++ },
121
+ jobOptions,
122
+ async () => (await import("./mediaBakeEngine-IHLCD2GH.js")).bakeMediaClip(request, jobOptions)
123
+ );
124
+ },
111
125
  dispose() {
112
126
  failAll(abortError());
113
127
  worker?.terminate();
@@ -118,20 +132,32 @@ function createMediaEditRenderer(options = {}) {
118
132
 
119
133
  // src/mediaEdit/mediaEditRenderManager.ts
120
134
  import {
135
+ MEDIA_BAKE_MANIFEST_VERSION,
121
136
  MEDIA_FX_ORDER,
122
137
  MEDIA_RENDER_MANIFEST_VERSION,
138
+ buildMediaBakeIndex,
123
139
  buildMediaRenderIndex,
124
140
  collectDocMediaClips,
141
+ isMediaBakeStale,
142
+ mediaBakeCuts,
143
+ mediaBakeFilePath,
144
+ mediaBakeKey,
145
+ mediaBakeManifestPath,
146
+ mediaBakeSpec,
125
147
  mediaClipRenderKey,
126
148
  mediaRenderFilePath,
127
149
  mediaRenderManifestPath,
128
150
  mediaRenderStaleness,
151
+ parseMediaBakeManifest,
129
152
  parseMediaRenderManifest,
130
153
  referencedMediaRenderKeys,
131
154
  selectMediaRendersForGc,
155
+ selectSupersededMediaBakes,
156
+ serializeMediaBakeManifest,
132
157
  serializeMediaFx,
133
158
  serializeMediaRenderManifest
134
159
  } from "@bendyline/squisq/mediaEdit";
160
+ var RENDER_SHARE_OF_BAKE = 0.6;
135
161
  function sourceSizeOf(sizes, src) {
136
162
  const direct = sizes.get(src);
137
163
  if (direct != null) return direct;
@@ -158,6 +184,11 @@ function createMediaEditRenderManager(options) {
158
184
  let disposed = false;
159
185
  let latestDoc = null;
160
186
  let lastExtras = [];
187
+ let bakeIndex = /* @__PURE__ */ new Map();
188
+ const bakeManifests = /* @__PURE__ */ new Map();
189
+ const staleBakes = /* @__PURE__ */ new Set();
190
+ const bakesInFlight = /* @__PURE__ */ new Map();
191
+ const pinnedRenders = /* @__PURE__ */ new Set();
161
192
  const indexSignature = () => [...index].filter(([key]) => !staleKeys.has(key)).map(([key, entry]) => `${key}=${entry.path}`).sort().join("|");
162
193
  const notify = () => {
163
194
  version++;
@@ -179,11 +210,26 @@ function createMediaEditRenderManager(options) {
179
210
  manifests.set(key, manifest);
180
211
  return manifest;
181
212
  };
213
+ const bakeManifestFor = async (key) => {
214
+ if (bakeManifests.has(key)) return bakeManifests.get(key) ?? null;
215
+ const entry = bakeIndex.get(key);
216
+ const manifest = entry?.manifestPath ? parseMediaBakeManifest(await loadText(entry.manifestPath) ?? "") : null;
217
+ bakeManifests.set(key, manifest);
218
+ return manifest;
219
+ };
182
220
  const refreshIndex = async () => {
183
221
  const entries = await provider.listMedia();
184
222
  const before = indexSignature();
185
223
  index = buildMediaRenderIndex(entries);
186
224
  sourceSizes = new Map(entries.map((e) => [e.name, e.size]));
225
+ bakeIndex = buildMediaBakeIndex(entries);
226
+ staleBakes.clear();
227
+ for (const [key] of bakeIndex) {
228
+ const manifest = await bakeManifestFor(key);
229
+ if (manifest && isMediaBakeStale(manifest, { sourceSize: sourceSizeOf(sourceSizes, manifest.src) })) {
230
+ staleBakes.add(key);
231
+ }
232
+ }
187
233
  staleKeys.clear();
188
234
  for (const [key] of index) {
189
235
  const manifest = await manifestFor(key);
@@ -204,6 +250,7 @@ function createMediaEditRenderManager(options) {
204
250
  return indexLoad;
205
251
  };
206
252
  const isCurrent = (key) => index.has(key) && !staleKeys.has(key);
253
+ const isBakeCurrent = (key) => bakeIndex.has(key) && !staleBakes.has(key);
207
254
  const pump = async () => {
208
255
  if (running || disposed) return;
209
256
  const job = queue.shift();
@@ -296,6 +343,123 @@ function createMediaEditRenderManager(options) {
296
343
  if (!response.ok) throw new Error(`Could not read ${src} (${response.status}).`);
297
344
  return response.blob();
298
345
  }
346
+ const cancelled = () => new DOMException("The bake was cancelled.", "AbortError");
347
+ const processedAudioFor = async (clip, renderKey, options2) => {
348
+ if (!isCurrent(renderKey)) {
349
+ pinnedRenders.add(renderKey);
350
+ try {
351
+ if (statuses.get(renderKey)?.state === "failed") statuses.delete(renderKey);
352
+ enqueue(clip, false);
353
+ notify();
354
+ void pump();
355
+ await new Promise((resolve, reject) => {
356
+ const check = () => {
357
+ const status = statuses.get(renderKey);
358
+ if (options2.signal?.aborted) settle(() => reject(cancelled()));
359
+ else if (isCurrent(renderKey)) settle(resolve);
360
+ else if (status?.state === "failed") {
361
+ settle(() => reject(new Error(`Audio cleanup failed: ${status.error}`)));
362
+ } else options2.onProgress?.((status?.progress ?? 0) * RENDER_SHARE_OF_BAKE);
363
+ };
364
+ const settle = (finish) => {
365
+ unsubscribe();
366
+ options2.signal?.removeEventListener("abort", check);
367
+ finish();
368
+ };
369
+ const unsubscribe = manager.subscribe(check);
370
+ options2.signal?.addEventListener("abort", check);
371
+ check();
372
+ });
373
+ } finally {
374
+ pinnedRenders.delete(renderKey);
375
+ }
376
+ }
377
+ const path = index.get(renderKey)?.path;
378
+ if (!path) throw new Error("The processed audio for this clip is missing.");
379
+ return path;
380
+ };
381
+ const referencedBakeKeys = () => {
382
+ const clips = [...latestDoc ? collectDocMediaClips(latestDoc) : [], ...lastExtras];
383
+ return /* @__PURE__ */ new Set([...clips.map((clip) => mediaBakeKey(clip)), ...bakesInFlight.keys()]);
384
+ };
385
+ const pruneBakes = async (sources, keep) => {
386
+ const loaded = /* @__PURE__ */ new Map();
387
+ for (const [key] of bakeIndex) {
388
+ const manifest = await bakeManifestFor(key);
389
+ if (manifest) loaded.set(key, manifest);
390
+ }
391
+ const referenced = referencedBakeKeys();
392
+ if (keep) referenced.add(keep);
393
+ const paths = selectSupersededMediaBakes(bakeIndex, loaded, sources, referenced);
394
+ for (const path of paths) await provider.removeMedia(path).catch(() => {
395
+ });
396
+ for (const [key, entry] of [...bakeIndex]) {
397
+ if (!paths.includes(entry.path)) continue;
398
+ bakeIndex.delete(key);
399
+ bakeManifests.delete(key);
400
+ staleBakes.delete(key);
401
+ }
402
+ return paths;
403
+ };
404
+ const runBake = async (clip, key, options2) => {
405
+ const renderKey = mediaClipRenderKey(clip);
406
+ const audioPath = renderKey ? await processedAudioFor(clip, renderKey, options2) : null;
407
+ const from = renderKey ? RENDER_SHARE_OF_BAKE : 0;
408
+ const source = await loadSource(clip.src);
409
+ const audio = audioPath ? await loadSource(audioPath) : void 0;
410
+ let lastReported = 0;
411
+ const result = await renderer.bake(
412
+ {
413
+ source,
414
+ kind: clip.kind,
415
+ ...audio ? { audio } : {},
416
+ ...clip.clipStart != null ? { clipStart: clip.clipStart } : {},
417
+ ...clip.clipEnd != null ? { clipEnd: clip.clipEnd } : {},
418
+ cuts: mediaBakeCuts(clip),
419
+ ...clip.edits?.gain ? { gain: clip.edits.gain } : {},
420
+ ...clip.edits?.fadeIn ? { fadeIn: clip.edits.fadeIn } : {},
421
+ ...clip.edits?.fadeOut ? { fadeOut: clip.edits.fadeOut } : {},
422
+ ...clip.kind === "video" && clip.edits?.crop ? { crop: clip.edits.crop } : {}
423
+ },
424
+ {
425
+ signal: options2.signal,
426
+ onProgress: (fraction) => {
427
+ if (fraction - lastReported < 0.01 && fraction < 1) return;
428
+ lastReported = fraction;
429
+ options2.onProgress?.(from + fraction * (1 - from));
430
+ }
431
+ }
432
+ );
433
+ const size = result.bytes.byteLength;
434
+ const bakePath = await provider.addMedia(
435
+ mediaBakeFilePath(clip.src, key, result.extension),
436
+ result.bytes,
437
+ result.mimeType
438
+ );
439
+ const manifest = {
440
+ version: MEDIA_BAKE_MANIFEST_VERSION,
441
+ src: clip.src,
442
+ key,
443
+ spec: mediaBakeSpec(clip),
444
+ file: bakePath,
445
+ mimeType: result.mimeType,
446
+ sourceSize: source.size,
447
+ duration: result.durationSec,
448
+ engine: result.engine,
449
+ createdAt: now().toISOString()
450
+ };
451
+ const manifestPath = await provider.addMedia(
452
+ mediaBakeManifestPath(clip.src, key),
453
+ new TextEncoder().encode(serializeMediaBakeManifest(manifest)),
454
+ "application/json"
455
+ );
456
+ bakeIndex.set(key, { key, path: bakePath, manifestPath, size });
457
+ bakeManifests.set(key, manifest);
458
+ staleBakes.delete(key);
459
+ await pruneBakes(/* @__PURE__ */ new Set([clip.src]), key);
460
+ notify();
461
+ return bakePath;
462
+ };
299
463
  const requiredKeys = (doc) => {
300
464
  const keys = referencedMediaRenderKeys(doc);
301
465
  for (const clip of lastExtras) {
@@ -317,7 +481,7 @@ function createMediaEditRenderManager(options) {
317
481
  clips.map((clip) => mediaClipRenderKey(clip)).filter((key) => key != null)
318
482
  );
319
483
  for (let i = queue.length - 1; i >= 0; i--) {
320
- if (!wanted.has(queue[i].key)) {
484
+ if (!wanted.has(queue[i].key) && !pinnedRenders.has(queue[i].key)) {
321
485
  statuses.delete(queue[i].key);
322
486
  queue.splice(i, 1);
323
487
  }
@@ -369,6 +533,26 @@ function createMediaEditRenderManager(options) {
369
533
  void pump();
370
534
  },
371
535
  loadSource,
536
+ bakedMedia(clip) {
537
+ const key = mediaBakeKey(clip);
538
+ return isBakeCurrent(key) ? bakeIndex.get(key)?.path : void 0;
539
+ },
540
+ async bake(clip, bakeOptions = {}) {
541
+ if (bakeOptions.signal?.aborted) throw cancelled();
542
+ await ensureIndex();
543
+ const key = mediaBakeKey(clip);
544
+ const current = isBakeCurrent(key) ? bakeIndex.get(key)?.path : void 0;
545
+ if (current) {
546
+ bakeOptions.onProgress?.(1);
547
+ return current;
548
+ }
549
+ let job = bakesInFlight.get(key);
550
+ if (!job) {
551
+ job = runBake(clip, key, bakeOptions).finally(() => bakesInFlight.delete(key));
552
+ bakesInFlight.set(key, job);
553
+ }
554
+ return job;
555
+ },
372
556
  async collectGarbage(doc, at = now()) {
373
557
  await refreshIndex();
374
558
  const docSources = new Set(
@@ -385,6 +569,8 @@ function createMediaEditRenderManager(options) {
385
569
  }
386
570
  const paths = selectMediaRendersForGc(candidates, requiredKeys(doc), loaded, at);
387
571
  for (const path of paths) await provider.removeMedia(path);
572
+ const bakes = latestDoc === doc ? await pruneBakes(docSources) : [];
573
+ if (bakes.length > 0) notify();
388
574
  if (paths.length > 0) {
389
575
  for (const key of candidates.keys()) {
390
576
  const entry = index.get(key);
@@ -396,7 +582,7 @@ function createMediaEditRenderManager(options) {
396
582
  indexVersion++;
397
583
  notify();
398
584
  }
399
- return paths;
585
+ return [...paths, ...bakes];
400
586
  },
401
587
  subscribe(listener) {
402
588
  listeners.add(listener);
@@ -51,7 +51,7 @@ var DecodedVideoCursor = class _DecodedVideoCursor {
51
51
  static async open(blob, ownerDocument) {
52
52
  let codecs;
53
53
  try {
54
- codecs = await import("./captureVideoCodecs-HVCZBBNT.js");
54
+ codecs = await import("./captureVideoCodecs-3QXX66ZI.js");
55
55
  } catch {
56
56
  return null;
57
57
  }
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { Doc, MediaProvider } from '@bendyline/squisq/schemas';
4
- import { e as MediaEditRenderManager } from '../mediaEditRenderManager-W_o6RRhT.js';
4
+ import { g as MediaEditRenderManager } from '../mediaEditRenderManager-DhqIn8yo.js';
5
5
  import { b as VideoExportConfig, f as VideoOutputFormat } from '../useVideoExport-DsNDpjmO.js';
6
6
  export { CoverImageExportFormat, CoverImageExportModal, CoverImageExportModalProps, coverImageFilename, validateCoverImageDimensions } from '../cover-image/index.js';
7
7
  import '@bendyline/squisq/mediaEdit';
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  VideoExportButton,
3
3
  VideoExportModal
4
- } from "../chunk-JKRF65OD.js";
4
+ } from "../chunk-QZSP5DGO.js";
5
5
  import {
6
6
  CoverImageExportModal,
7
7
  coverImageFilename,
8
8
  validateCoverImageDimensions
9
- } from "../chunk-XPWYKETM.js";
9
+ } from "../chunk-QKTQL3NK.js";
10
10
  import "../chunk-3NJDHK62.js";
11
- import "../chunk-3IMR25SX.js";
12
- import "../chunk-ZEA6XJL7.js";
11
+ import "../chunk-MVEZDB4G.js";
12
+ import "../chunk-YO7JVV3K.js";
13
13
  import "../chunk-RIXBIX3H.js";
14
14
  import "../chunk-5MFQMJ5Z.js";
15
15
  export {
@@ -2,9 +2,9 @@ import {
2
2
  CoverImageExportModal,
3
3
  coverImageFilename,
4
4
  validateCoverImageDimensions
5
- } from "../chunk-XPWYKETM.js";
5
+ } from "../chunk-QKTQL3NK.js";
6
6
  import "../chunk-3NJDHK62.js";
7
- import "../chunk-ZEA6XJL7.js";
7
+ import "../chunk-YO7JVV3K.js";
8
8
  export {
9
9
  CoverImageExportModal,
10
10
  coverImageFilename,
@@ -7,7 +7,7 @@ import {
7
7
  } from "../chunk-3NJDHK62.js";
8
8
  import {
9
9
  useFrameCapture
10
- } from "../chunk-ZEA6XJL7.js";
10
+ } from "../chunk-YO7JVV3K.js";
11
11
 
12
12
  // src/DashboardImageExportModal.tsx
13
13
  import { useCallback, useId, useMemo, useRef, useState } from "react";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createAudioFileEncoder,
3
3
  supportedAudioFileFormats
4
- } from "../chunk-VE3OUBRG.js";
4
+ } from "../chunk-XDJ2FPQQ.js";
5
5
  import "../chunk-2W6JQSIY.js";
6
6
  import {
7
7
  createEncoder,
@@ -3,10 +3,10 @@ import {
3
3
  resolveVideoCoverFramePlan,
4
4
  resolveVideoExportCover,
5
5
  useVideoExport
6
- } from "../chunk-3IMR25SX.js";
6
+ } from "../chunk-MVEZDB4G.js";
7
7
  import {
8
8
  useFrameCapture
9
- } from "../chunk-ZEA6XJL7.js";
9
+ } from "../chunk-YO7JVV3K.js";
10
10
  import "../chunk-RIXBIX3H.js";
11
11
  import "../chunk-5MFQMJ5Z.js";
12
12
  export {
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { FrameCaptureHandle, FrameCaptureOptions, FrameCaptureRenderOptions, use
5
5
  export { E as EncoderConfig, a as EncoderFrameSource, M as MainThreadEncoder, c as createEncoder, s as supportsWebCodecs, b as supportsWebCodecsH264 } from './mainThreadEncoder-BVCLjlfj.js';
6
6
  export { AudioTimelineClip, FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
7
7
  export { AudioFileEncoder, AudioFileEncoderOptions, AudioFileFormat, RenderDocumentAudioOptions, createAudioFileEncoder, renderAudioTimeline, renderDocumentAudio, supportedAudioFileFormats, supportsWebCodecsAac } from './encoder/index.js';
8
- export { M as MediaEditJobOptions, a as MediaEditPausesRequest, b as MediaEditPausesResult, c as MediaEditPreviewRequest, d as MediaEditPreviewResult, e as MediaEditRenderManager, f as MediaEditRenderManagerOptions, g as MediaEditRenderRequest, h as MediaEditRenderResult, i as MediaEditRenderState, j as MediaEditRenderStatus, k as MediaEditRenderer, l as MediaEditRendererOptions, m as createMediaEditRenderManager, n as createMediaEditRenderer } from './mediaEditRenderManager-W_o6RRhT.js';
8
+ export { M as MediaBakeRequest, a as MediaBakeResult, b as MediaEditJobOptions, c as MediaEditPausesRequest, d as MediaEditPausesResult, e as MediaEditPreviewRequest, f as MediaEditPreviewResult, g as MediaEditRenderManager, h as MediaEditRenderManagerOptions, i as MediaEditRenderRequest, j as MediaEditRenderResult, k as MediaEditRenderState, l as MediaEditRenderStatus, m as MediaEditRenderer, n as MediaEditRendererOptions, o as MediaEditWaveformRequest, p as MediaEditWaveformResult, q as createMediaEditRenderManager, r as createMediaEditRenderer } from './mediaEditRenderManager-DhqIn8yo.js';
9
9
  export { MediaEditRendersView, useMediaEditRenders, useMediaEditStatus, useProcessedAudio } from './media-edit/index.js';
10
10
  import 'react';
11
11
  import '@bendyline/squisq/schemas';
package/dist/index.js CHANGED
@@ -1,26 +1,26 @@
1
1
  import {
2
2
  VideoExportButton,
3
3
  VideoExportModal
4
- } from "./chunk-JKRF65OD.js";
4
+ } from "./chunk-QZSP5DGO.js";
5
5
  import {
6
6
  CoverImageExportModal,
7
7
  coverImageFilename,
8
8
  validateCoverImageDimensions
9
- } from "./chunk-XPWYKETM.js";
9
+ } from "./chunk-QKTQL3NK.js";
10
10
  import "./chunk-3NJDHK62.js";
11
11
  import {
12
12
  DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
13
13
  resolveVideoCoverFramePlan,
14
14
  resolveVideoExportCover,
15
15
  useVideoExport
16
- } from "./chunk-3IMR25SX.js";
16
+ } from "./chunk-MVEZDB4G.js";
17
17
  import {
18
18
  useFrameCapture
19
- } from "./chunk-ZEA6XJL7.js";
19
+ } from "./chunk-YO7JVV3K.js";
20
20
  import {
21
21
  createAudioFileEncoder,
22
22
  supportedAudioFileFormats
23
- } from "./chunk-VE3OUBRG.js";
23
+ } from "./chunk-XDJ2FPQQ.js";
24
24
  import "./chunk-2W6JQSIY.js";
25
25
  import {
26
26
  createEncoder,
@@ -37,7 +37,7 @@ import {
37
37
  useMediaEditRenders,
38
38
  useMediaEditStatus,
39
39
  useProcessedAudio
40
- } from "./chunk-TRCTDIWS.js";
40
+ } from "./chunk-XPPQWQEO.js";
41
41
  import "./chunk-TL2IMDIZ.js";
42
42
  export {
43
43
  CoverImageExportModal,
@@ -6,7 +6,7 @@ import {
6
6
  Output,
7
7
  WebMOutputFormat,
8
8
  canEncodeAudio
9
- } from "./chunk-EJ3DVPFF.js";
9
+ } from "./chunk-MOIVICAD.js";
10
10
  import {
11
11
  assertOpen,
12
12
  blockFrames,
@@ -1,5 +1,5 @@
1
- import { j as MediaEditRenderStatus, e as MediaEditRenderManager } from '../mediaEditRenderManager-W_o6RRhT.js';
2
- export { M as MediaEditJobOptions, a as MediaEditPausesRequest, b as MediaEditPausesResult, c as MediaEditPreviewRequest, d as MediaEditPreviewResult, f as MediaEditRenderManagerOptions, g as MediaEditRenderRequest, h as MediaEditRenderResult, i as MediaEditRenderState, k as MediaEditRenderer, l as MediaEditRendererOptions, m as createMediaEditRenderManager, n as createMediaEditRenderer } from '../mediaEditRenderManager-W_o6RRhT.js';
1
+ import { l as MediaEditRenderStatus, g as MediaEditRenderManager } from '../mediaEditRenderManager-DhqIn8yo.js';
2
+ export { M as MediaBakeRequest, a as MediaBakeResult, b as MediaEditJobOptions, c as MediaEditPausesRequest, d as MediaEditPausesResult, e as MediaEditPreviewRequest, f as MediaEditPreviewResult, h as MediaEditRenderManagerOptions, i as MediaEditRenderRequest, j as MediaEditRenderResult, k as MediaEditRenderState, m as MediaEditRenderer, n as MediaEditRendererOptions, o as MediaEditWaveformRequest, p as MediaEditWaveformResult, q as createMediaEditRenderManager, r as createMediaEditRenderer } from '../mediaEditRenderManager-DhqIn8yo.js';
3
3
  import { MediaClip, Doc } from '@bendyline/squisq/schemas';
4
4
  import '@bendyline/squisq/mediaEdit';
5
5
 
@@ -4,7 +4,7 @@ import {
4
4
  useMediaEditRenders,
5
5
  useMediaEditStatus,
6
6
  useProcessedAudio
7
- } from "../chunk-TRCTDIWS.js";
7
+ } from "../chunk-XPPQWQEO.js";
8
8
  import "../chunk-TL2IMDIZ.js";
9
9
  export {
10
10
  createMediaEditRenderManager,
@@ -0,0 +1,12 @@
1
+ import {
2
+ bakeCropRectangle,
3
+ bakeDecodeRuns,
4
+ bakeMediaClip
5
+ } from "./chunk-VKGKDTWK.js";
6
+ import "./chunk-AARDBHRJ.js";
7
+ import "./chunk-MOIVICAD.js";
8
+ export {
9
+ bakeCropRectangle,
10
+ bakeDecodeRuns,
11
+ bakeMediaClip
12
+ };
@@ -1,13 +1,16 @@
1
1
  import {
2
2
  analyzeMediaEditAudio,
3
3
  analyzeMediaEditPauses,
4
+ analyzeMediaEditWaveform,
4
5
  previewMediaEditAudio,
5
6
  renderMediaEditAudio
6
- } from "./chunk-J6MJYH2I.js";
7
- import "./chunk-EJ3DVPFF.js";
7
+ } from "./chunk-SGLTT5CL.js";
8
+ import "./chunk-AARDBHRJ.js";
9
+ import "./chunk-MOIVICAD.js";
8
10
  export {
9
11
  analyzeMediaEditAudio,
10
12
  analyzeMediaEditPauses,
13
+ analyzeMediaEditWaveform,
11
14
  previewMediaEditAudio,
12
15
  renderMediaEditAudio
13
16
  };
@@ -1,5 +1,5 @@
1
1
  import { Doc, MediaClip, MediaProvider } from '@bendyline/squisq/schemas';
2
- import { DenoiserFactory, PauseTightenOptions, MediaCut, MediaFxAnalysis } from '@bendyline/squisq/mediaEdit';
2
+ import { DenoiserFactory, PauseTightenOptions, MediaCut, MediaFxAnalysis, MediaCrop } from '@bendyline/squisq/mediaEdit';
3
3
 
4
4
  /**
5
5
  * Encode the chain's output into the render file: Opus in WebM when
@@ -62,6 +62,66 @@ interface MediaEditPreviewResult {
62
62
  original: Float32Array[];
63
63
  processed: Float32Array[];
64
64
  }
65
+ interface MediaEditWaveformRequest {
66
+ source: Blob;
67
+ }
68
+ interface MediaEditWaveformResult {
69
+ /**
70
+ * The loudest sample (0…1) in each slice of the audio track, from media
71
+ * time 0. Empty when the media has no audio track.
72
+ */
73
+ peaks: Float32Array;
74
+ /** Slices per second. */
75
+ peaksPerSec: number;
76
+ /** Length of the whole file — its longest track — in seconds. */
77
+ durationSec: number;
78
+ }
79
+
80
+ /**
81
+ * Media-bake engine (browser): a clip with its whole recipe applied, encoded
82
+ * as one ordinary media file — for players that cannot apply a recipe.
83
+ *
84
+ * Audio clips: decode the trim window once, drop the cut ranges, apply gain
85
+ * and fades, encode (M4A where the browser can encode AAC, else Opus in WebM,
86
+ * else WAV). Video clips: decode the kept ranges, crop, re-encode (H.264 in
87
+ * MP4 where available, else VP9/VP8 in WebM) and mux with the same baked
88
+ * audio. The sound comes from the clip's processed-audio render when the
89
+ * caller passes one, so the signal chain is never run twice.
90
+ *
91
+ * Realm-agnostic, like the render engine: runs in the media-edit worker, or
92
+ * on the main thread as a fallback.
93
+ */
94
+
95
+ interface MediaBakeRequest {
96
+ /** The clip's source file. */
97
+ source: Blob;
98
+ kind: 'audio' | 'video';
99
+ /**
100
+ * Processed audio to use for the sound instead of the source's own: an `fx`
101
+ * render, which is sample-aligned with the source's audio track.
102
+ */
103
+ audio?: Blob;
104
+ /** Source in-point in seconds. Default 0. */
105
+ clipStart?: number;
106
+ /** Source out-point in seconds. Default: the source's end. */
107
+ clipEnd?: number;
108
+ /** Removed source ranges. */
109
+ cuts?: MediaCut[];
110
+ /** Clip gain in dB. */
111
+ gain?: number;
112
+ fadeIn?: number;
113
+ fadeOut?: number;
114
+ /** Video only: the part of the frame to keep. */
115
+ crop?: MediaCrop;
116
+ }
117
+ interface MediaBakeResult {
118
+ bytes: ArrayBuffer;
119
+ mimeType: string;
120
+ extension: 'mp4' | 'webm' | 'm4a' | 'wav';
121
+ /** Length of the baked clip in seconds. */
122
+ durationSec: number;
123
+ engine: string;
124
+ }
65
125
 
66
126
  /**
67
127
  * Main-thread client for the media-edit engine.
@@ -78,6 +138,16 @@ interface MediaEditRenderer {
78
138
  preview(request: MediaEditPreviewRequest, options?: MediaEditJobOptions): Promise<MediaEditPreviewResult>;
79
139
  /** Propose cuts that shorten long pauses. */
80
140
  pauses(request: MediaEditPausesRequest, options?: MediaEditJobOptions): Promise<MediaEditPausesResult>;
141
+ /**
142
+ * The take's waveform peaks and its length, for an editing timeline. Media
143
+ * with no audio track has no peaks, but still its length.
144
+ */
145
+ waveform(request: MediaEditWaveformRequest, options?: MediaEditJobOptions): Promise<MediaEditWaveformResult>;
146
+ /**
147
+ * Bake a clip: its source with trim, cuts, gain, fades and crop applied,
148
+ * encoded as one ordinary media file.
149
+ */
150
+ bake(request: MediaBakeRequest, options?: MediaEditJobOptions): Promise<MediaBakeResult>;
81
151
  dispose(): void;
82
152
  }
83
153
  interface MediaEditRendererOptions {
@@ -104,6 +174,12 @@ declare function createMediaEditRenderer(options?: MediaEditRendererOptions): Me
104
174
  * media, so they travel with it. A render is stale when its source changed
105
175
  * size (a re-recorded take under the same name) or when it skipped ops this
106
176
  * engine now knows; stale renders are replaced.
177
+ *
178
+ * The manager also makes BAKED clips — a clip's whole recipe (trim, cuts,
179
+ * processed audio, gain, fades, crop) in one ordinary file, for players that
180
+ * cannot apply a recipe. Those are made only when asked for (`bake`): an
181
+ * export that needs them, or "Save edited copy". They share the folder and
182
+ * are replaced as the recipe changes.
107
183
  */
108
184
 
109
185
  type MediaEditRenderState = 'queued' | 'rendering' | 'ready' | 'failed';
@@ -114,6 +190,11 @@ interface MediaEditRenderStatus {
114
190
  progress?: number;
115
191
  error?: string;
116
192
  }
193
+ interface MediaBakeOptions {
194
+ signal?: AbortSignal;
195
+ /** 0…1 across the whole job, including a processed-audio render it has to wait for. */
196
+ onProgress?: (fraction: number) => void;
197
+ }
117
198
  interface MediaEditRenderManager {
118
199
  /** The engine client, shared for previews and analysis. */
119
200
  readonly renderer: MediaEditRenderer;
@@ -134,11 +215,22 @@ interface MediaEditRenderManager {
134
215
  retry(clip: MediaClip): void;
135
216
  /** Load a media reference as a Blob (sources for previews). */
136
217
  loadSource(src: string): Promise<Blob>;
218
+ /** The baked copy of a clip — its whole recipe in one file — when a current one exists. */
219
+ bakedMedia(clip: MediaClip): string | undefined;
220
+ /**
221
+ * Make the baked copy of a clip, or reuse a current one, and resolve with
222
+ * its path. A recipe with a signal chain first gets its processed-audio
223
+ * render (queued here if it is not already), so the baked sound is the
224
+ * cleaned-up sound. Rejects when the clip cannot be baked (a browser that
225
+ * cannot encode video, a failed render) or the job is cancelled.
226
+ */
227
+ bake(clip: MediaClip, options?: MediaBakeOptions): Promise<string>;
137
228
  /**
138
229
  * Delete renders of this document's sources that no current recipe
139
- * references and that are older than the GC age. Renders of other sources
140
- * are never touched — a media folder can be shared by several documents.
141
- * Returns the removed paths.
230
+ * references and that are older than the GC age, and its baked clips that
231
+ * no current recipe asks for (at once: they are made again on demand).
232
+ * Files of other sources are never touched — a media folder can be shared
233
+ * by several documents. Returns the removed paths.
142
234
  */
143
235
  collectGarbage(doc: Doc, now?: Date): Promise<string[]>;
144
236
  /** Change notifications (index, status, progress). Returns an unsubscribe. */
@@ -164,4 +256,4 @@ interface MediaEditRenderManagerOptions {
164
256
  }
165
257
  declare function createMediaEditRenderManager(options: MediaEditRenderManagerOptions): MediaEditRenderManager;
166
258
 
167
- export { type MediaEditJobOptions as M, type MediaEditPausesRequest as a, type MediaEditPausesResult as b, type MediaEditPreviewRequest as c, type MediaEditPreviewResult as d, type MediaEditRenderManager as e, type MediaEditRenderManagerOptions as f, type MediaEditRenderRequest as g, type MediaEditRenderResult as h, type MediaEditRenderState as i, type MediaEditRenderStatus as j, type MediaEditRenderer as k, type MediaEditRendererOptions as l, createMediaEditRenderManager as m, createMediaEditRenderer as n };
259
+ export { type MediaBakeRequest as M, type MediaBakeResult as a, type MediaEditJobOptions as b, type MediaEditPausesRequest as c, type MediaEditPausesResult as d, type MediaEditPreviewRequest as e, type MediaEditPreviewResult as f, type MediaEditRenderManager as g, type MediaEditRenderManagerOptions as h, type MediaEditRenderRequest as i, type MediaEditRenderResult as j, type MediaEditRenderState as k, type MediaEditRenderStatus as l, type MediaEditRenderer as m, type MediaEditRendererOptions as n, type MediaEditWaveformRequest as o, type MediaEditWaveformResult as p, createMediaEditRenderManager as q, createMediaEditRenderer as r };
@@ -1,10 +1,15 @@
1
1
  import {
2
2
  analyzeMediaEditAudio,
3
3
  analyzeMediaEditPauses,
4
+ analyzeMediaEditWaveform,
4
5
  previewMediaEditAudio,
5
6
  renderMediaEditAudio
6
- } from "../chunk-J6MJYH2I.js";
7
- import "../chunk-EJ3DVPFF.js";
7
+ } from "../chunk-SGLTT5CL.js";
8
+ import {
9
+ bakeMediaClip
10
+ } from "../chunk-VKGKDTWK.js";
11
+ import "../chunk-AARDBHRJ.js";
12
+ import "../chunk-MOIVICAD.js";
8
13
  import {
9
14
  createRnnoiseDenoiserFactory
10
15
  } from "../chunk-TL2IMDIZ.js";
@@ -32,6 +37,14 @@ async function run(request) {
32
37
  } else if (request.type === "pauses") {
33
38
  const result = await analyzeMediaEditPauses(request, options);
34
39
  scope.postMessage({ type: "paused", id: request.id, result });
40
+ } else if (request.type === "bake") {
41
+ const result = await bakeMediaClip(request, options);
42
+ scope.postMessage({ type: "baked", id: request.id, result }, [result.bytes]);
43
+ } else if (request.type === "waveform") {
44
+ const result = await analyzeMediaEditWaveform(request, options);
45
+ scope.postMessage({ type: "waveformed", id: request.id, result }, [
46
+ result.peaks.buffer
47
+ ]);
35
48
  } else if (request.type === "analyze") {
36
49
  const result = await analyzeMediaEditAudio(request, options);
37
50
  scope.postMessage({ type: "analyzed", id: request.id, result });