@bendyline/squisq-video-react 2.4.11 → 2.4.13

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.js CHANGED
@@ -1,28 +1,37 @@
1
1
  import {
2
2
  VideoExportButton,
3
3
  VideoExportModal
4
- } from "./chunk-JGSJQZGU.js";
4
+ } from "./chunk-RO52S6GO.js";
5
5
  import {
6
6
  CoverImageExportModal,
7
7
  coverImageFilename,
8
8
  validateCoverImageDimensions
9
- } from "./chunk-BRE3IRIM.js";
9
+ } from "./chunk-M5AK4MMV.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-BGS2NEYC.js";
16
+ } from "./chunk-TUIVBMAK.js";
17
17
  import {
18
18
  useFrameCapture
19
- } from "./chunk-4NI2QIFE.js";
19
+ } from "./chunk-2C4BEAWT.js";
20
+ import "./chunk-4GFZ3GUE.js";
20
21
  import {
21
22
  createEncoder,
22
23
  supportsWebCodecs,
23
24
  supportsWebCodecsAac,
24
25
  supportsWebCodecsH264
25
- } from "./chunk-32QCPFXE.js";
26
+ } from "./chunk-GBIYYDMY.js";
27
+ import {
28
+ createMediaEditRenderManager,
29
+ createMediaEditRenderer,
30
+ useMediaEditRenders,
31
+ useMediaEditStatus,
32
+ useProcessedAudio
33
+ } from "./chunk-SLLH2RTX.js";
34
+ import "./chunk-TL2IMDIZ.js";
26
35
  import "./chunk-5MFQMJ5Z.js";
27
36
  export {
28
37
  CoverImageExportModal,
@@ -31,12 +40,17 @@ export {
31
40
  VideoExportModal,
32
41
  coverImageFilename,
33
42
  createEncoder,
43
+ createMediaEditRenderManager,
44
+ createMediaEditRenderer,
34
45
  resolveVideoCoverFramePlan,
35
46
  resolveVideoExportCover,
36
47
  supportsWebCodecs,
37
48
  supportsWebCodecsAac,
38
49
  supportsWebCodecsH264,
39
50
  useFrameCapture,
51
+ useMediaEditRenders,
52
+ useMediaEditStatus,
53
+ useProcessedAudio,
40
54
  useVideoExport,
41
55
  validateCoverImageDimensions
42
56
  };
@@ -0,0 +1,29 @@
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';
3
+ import { MediaClip, Doc } from '@bendyline/squisq/schemas';
4
+ import '@bendyline/squisq/mediaEdit';
5
+
6
+ /**
7
+ * React bindings for a {@link MediaEditRenderManager}.
8
+ *
9
+ * - {@link useProcessedAudio} re-renders only when the set of available
10
+ * renders changes, so a `DocPlayer` or schedule memoized on it switches an
11
+ * edited clip over the moment its render lands — and never churns on
12
+ * progress ticks.
13
+ * - {@link useMediaEditStatus} tracks every change, for progress displays.
14
+ * - {@link useMediaEditRenders} combines both and, given the document, keeps
15
+ * the manager synced — the one-call binding for hosts.
16
+ */
17
+
18
+ interface MediaEditRendersView {
19
+ processedAudio: (clip: MediaClip) => string | undefined;
20
+ status: (clip: MediaClip) => MediaEditRenderStatus | null;
21
+ }
22
+ /** The processed-audio lookup; a new identity only when renders appear or vanish. */
23
+ declare function useProcessedAudio(manager: MediaEditRenderManager | null | undefined): (clip: MediaClip) => string | undefined;
24
+ /** Render status per clip; updates with progress. */
25
+ declare function useMediaEditStatus(manager: MediaEditRenderManager | null | undefined): (clip: MediaClip) => MediaEditRenderStatus | null;
26
+ /** Both views; with `doc`, also keeps the manager synced to it. */
27
+ declare function useMediaEditRenders(manager: MediaEditRenderManager | null | undefined, doc?: Doc | null): MediaEditRendersView;
28
+
29
+ export { MediaEditRenderManager, MediaEditRenderStatus, type MediaEditRendersView, useMediaEditRenders, useMediaEditStatus, useProcessedAudio };
@@ -0,0 +1,15 @@
1
+ import {
2
+ createMediaEditRenderManager,
3
+ createMediaEditRenderer,
4
+ useMediaEditRenders,
5
+ useMediaEditStatus,
6
+ useProcessedAudio
7
+ } from "../chunk-SLLH2RTX.js";
8
+ import "../chunk-TL2IMDIZ.js";
9
+ export {
10
+ createMediaEditRenderManager,
11
+ createMediaEditRenderer,
12
+ useMediaEditRenders,
13
+ useMediaEditStatus,
14
+ useProcessedAudio
15
+ };
@@ -0,0 +1,13 @@
1
+ import {
2
+ analyzeMediaEditAudio,
3
+ analyzeMediaEditPauses,
4
+ previewMediaEditAudio,
5
+ renderMediaEditAudio
6
+ } from "./chunk-IFQJAW4V.js";
7
+ import "./chunk-4GFZ3GUE.js";
8
+ export {
9
+ analyzeMediaEditAudio,
10
+ analyzeMediaEditPauses,
11
+ previewMediaEditAudio,
12
+ renderMediaEditAudio
13
+ };
@@ -0,0 +1,167 @@
1
+ import { Doc, MediaClip, MediaProvider } from '@bendyline/squisq/schemas';
2
+ import { DenoiserFactory, PauseTightenOptions, MediaCut, MediaFxAnalysis } from '@bendyline/squisq/mediaEdit';
3
+
4
+ /**
5
+ * Encode the chain's output into the render file: Opus in WebM when
6
+ * WebCodecs can encode Opus, 16-bit WAV otherwise. Opus keeps renders small
7
+ * (~7 MB per 10 minutes at 96 kb/s mono) so they fit every surface's media
8
+ * transport; WAV is the always-available fallback.
9
+ */
10
+ interface EncodedAudio {
11
+ bytes: ArrayBuffer;
12
+ mimeType: 'audio/webm' | 'audio/wav';
13
+ extension: 'webm' | 'wav';
14
+ }
15
+
16
+ /**
17
+ * Media-edit render engine (browser): decode → signal chain → encode.
18
+ *
19
+ * Realm-agnostic — runs in the media-edit worker, or on the main thread as a
20
+ * fallback. Every input is a Blob plus a canonical `fx` string, so requests
21
+ * cross a worker boundary without copying media bytes.
22
+ */
23
+
24
+ interface MediaEditJobOptions {
25
+ onProgress?: (fraction: number) => void;
26
+ signal?: AbortSignal;
27
+ /** The denoiser the chain uses for `denoise`; without one the op is skipped. */
28
+ denoiser?: DenoiserFactory;
29
+ }
30
+ interface MediaEditRenderRequest {
31
+ source: Blob;
32
+ /** Canonical `fx` value. */
33
+ fx: string;
34
+ }
35
+ interface MediaEditRenderResult extends EncodedAudio {
36
+ analysis: MediaFxAnalysis;
37
+ ignoredOps: string[];
38
+ /** Duration of the source's audio track, seconds. */
39
+ sourceDuration: number;
40
+ engine: string;
41
+ }
42
+ interface MediaEditPreviewRequest extends MediaEditRenderRequest {
43
+ /** Media-time window to preview. */
44
+ startSec: number;
45
+ endSec: number;
46
+ /** Reusable whole-take analysis (breaths, loudness gain); computed locally when absent. */
47
+ analysis?: MediaFxAnalysis;
48
+ }
49
+ interface MediaEditPausesRequest {
50
+ source: Blob;
51
+ options?: PauseTightenOptions;
52
+ }
53
+ interface MediaEditPausesResult {
54
+ /** Proposed cuts in source seconds. */
55
+ cuts: MediaCut[];
56
+ /** Length of the audio track, seconds. */
57
+ durationSec: number;
58
+ }
59
+ interface MediaEditPreviewResult {
60
+ sampleRate: number;
61
+ startSec: number;
62
+ original: Float32Array[];
63
+ processed: Float32Array[];
64
+ }
65
+
66
+ /**
67
+ * Main-thread client for the media-edit engine.
68
+ *
69
+ * Jobs run in `workers/mediaEdit.worker.js` so a render never blocks the
70
+ * editor. If the worker cannot start (a CSP without `worker-src`, a bundler
71
+ * that dropped the asset), jobs fall back to the main thread with the same
72
+ * results — slower to the UI, never broken.
73
+ */
74
+
75
+ interface MediaEditRenderer {
76
+ render(request: MediaEditRenderRequest, options?: MediaEditJobOptions): Promise<MediaEditRenderResult>;
77
+ analyze(request: MediaEditRenderRequest, options?: MediaEditJobOptions): Promise<MediaFxAnalysis | null>;
78
+ preview(request: MediaEditPreviewRequest, options?: MediaEditJobOptions): Promise<MediaEditPreviewResult>;
79
+ /** Propose cuts that shorten long pauses. */
80
+ pauses(request: MediaEditPausesRequest, options?: MediaEditJobOptions): Promise<MediaEditPausesResult>;
81
+ dispose(): void;
82
+ }
83
+ interface MediaEditRendererOptions {
84
+ /**
85
+ * Create the worker. Defaults to the packaged `workers/mediaEdit.worker.js`
86
+ * resolved beside this module; hosts whose bundler needs an explicit worker
87
+ * import pass their own. Return null to run on the main thread.
88
+ */
89
+ createWorker?: () => Worker | null;
90
+ }
91
+ declare function createMediaEditRenderer(options?: MediaEditRendererOptions): MediaEditRenderer;
92
+
93
+ /**
94
+ * Keeps a document's processed-audio renders in step with its recipes.
95
+ *
96
+ * One manager per media scope (a document's `MediaProvider`). `sync(doc)`
97
+ * indexes the `.mediaEdits/` folder, finds every clip whose `fx` recipe has no
98
+ * current render, and renders them one at a time in the background through a
99
+ * {@link MediaEditRenderer}. Playback and export read `processedAudio(clip)`,
100
+ * which answers only with renders that exist — an edited clip plays its
101
+ * original audio until its render lands, never silence.
102
+ *
103
+ * Renders are written through the provider next to the document's other
104
+ * media, so they travel with it. A render is stale when its source changed
105
+ * size (a re-recorded take under the same name) or when it skipped ops this
106
+ * engine now knows; stale renders are replaced.
107
+ */
108
+
109
+ type MediaEditRenderState = 'queued' | 'rendering' | 'ready' | 'failed';
110
+ interface MediaEditRenderStatus {
111
+ key: string;
112
+ state: MediaEditRenderState;
113
+ /** 0…1 while rendering. */
114
+ progress?: number;
115
+ error?: string;
116
+ }
117
+ interface MediaEditRenderManager {
118
+ /** The engine client, shared for previews and analysis. */
119
+ readonly renderer: MediaEditRenderer;
120
+ /**
121
+ * Reconcile with a document: index renders and queue missing or stale ones.
122
+ * `extraClips` adds media the document model does not schedule itself —
123
+ * an editor passes body-embedded `<audio>`/`<video>` tags here. Omitted,
124
+ * the extras from the previous call carry over.
125
+ */
126
+ sync(doc: Doc, extraClips?: readonly MediaClip[]): void;
127
+ /** The render an edited clip should play, when one exists. */
128
+ processedAudio(clip: MediaClip): string | undefined;
129
+ /** Render status for a clip, or null when its recipe renders nothing. */
130
+ status(clip: MediaClip): MediaEditRenderStatus | null;
131
+ /** Resolves once every render the document needs is ready or has failed. */
132
+ whenReady(doc: Doc, signal?: AbortSignal): Promise<void>;
133
+ /** Re-render a clip even if a render exists (e.g. after a failure). */
134
+ retry(clip: MediaClip): void;
135
+ /** Load a media reference as a Blob (sources for previews). */
136
+ loadSource(src: string): Promise<Blob>;
137
+ /**
138
+ * 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.
142
+ */
143
+ collectGarbage(doc: Doc, now?: Date): Promise<string[]>;
144
+ /** Change notifications (index, status, progress). Returns an unsubscribe. */
145
+ subscribe(listener: () => void): () => void;
146
+ /** Bumps on every change (status, progress, renders), for `useSyncExternalStore`. */
147
+ getVersion(): number;
148
+ /**
149
+ * Bumps only when the set of available renders changes — what
150
+ * {@link MediaEditRenderManager.processedAudio} answers. Progress ticks leave it alone,
151
+ * so playback schedules keyed on it do not churn while a render runs.
152
+ */
153
+ getIndexVersion(): number;
154
+ dispose(): void;
155
+ }
156
+ interface MediaEditRenderManagerOptions {
157
+ mediaProvider: MediaProvider;
158
+ /** Shared engine client; the manager creates (and disposes) its own when omitted. */
159
+ renderer?: MediaEditRenderer;
160
+ /** For tests; defaults to the global `fetch`. */
161
+ fetch?: typeof fetch;
162
+ /** Clock for manifests and GC; defaults to `new Date()`. */
163
+ now?: () => Date;
164
+ }
165
+ declare function createMediaEditRenderManager(options: MediaEditRenderManagerOptions): MediaEditRenderManager;
166
+
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 };
@@ -1,4 +1,4 @@
1
- import { MediaProvider, Theme, VideoPresentation, VideoPipSize, VideoPipShape, VideoPipPosition, Doc } from '@bendyline/squisq/schemas';
1
+ import { MediaScheduleOptions, MediaProvider, Theme, VideoPresentation, VideoPipSize, VideoPipShape, VideoPipPosition, Doc } from '@bendyline/squisq/schemas';
2
2
  import { ResourcePolicy } from '@bendyline/squisq/markdown';
3
3
  import { CoverSlidePlayback } from '@bendyline/squisq/doc';
4
4
  import { VideoQuality, VideoOrientation, FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
@@ -37,6 +37,12 @@ interface VideoExportConfig {
37
37
  * degradation; `omit` intentionally skips audio.
38
38
  */
39
39
  audioPolicy?: VideoAudioPolicy;
40
+ /**
41
+ * Processed-audio lookup for media-edit recipes (`fx`): the mix uses an
42
+ * edited clip's render instead of its original audio. Hosts await pending
43
+ * renders before exporting; a clip without a render exports its original.
44
+ */
45
+ processedAudio?: MediaScheduleOptions['processedAudio'];
40
46
  /** Render authored animations and slide transitions (default: true for MP4, false for GIF). */
41
47
  animationsEnabled?: boolean;
42
48
  /** Encoding quality preset (default: 'normal') */
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,63 @@
1
+ import {
2
+ analyzeMediaEditAudio,
3
+ analyzeMediaEditPauses,
4
+ previewMediaEditAudio,
5
+ renderMediaEditAudio
6
+ } from "../chunk-IFQJAW4V.js";
7
+ import "../chunk-4GFZ3GUE.js";
8
+ import {
9
+ createRnnoiseDenoiserFactory
10
+ } from "../chunk-TL2IMDIZ.js";
11
+
12
+ // src/workers/mediaEdit.worker.ts
13
+ import { Rnnoise } from "@shiguredo/rnnoise-wasm";
14
+ var scope = self;
15
+ var jobs = /* @__PURE__ */ new Map();
16
+ var denoiser = createRnnoiseDenoiserFactory(() => Rnnoise.load());
17
+ function isAbort(err) {
18
+ return err instanceof DOMException && err.name === "AbortError";
19
+ }
20
+ async function run(request) {
21
+ const controller = new AbortController();
22
+ jobs.set(request.id, controller);
23
+ const options = {
24
+ denoiser,
25
+ signal: controller.signal,
26
+ onProgress: (fraction) => scope.postMessage({ type: "progress", id: request.id, fraction })
27
+ };
28
+ try {
29
+ if (request.type === "render") {
30
+ const result = await renderMediaEditAudio(request, options);
31
+ scope.postMessage({ type: "rendered", id: request.id, result }, [result.bytes]);
32
+ } else if (request.type === "pauses") {
33
+ const result = await analyzeMediaEditPauses(request, options);
34
+ scope.postMessage({ type: "paused", id: request.id, result });
35
+ } else if (request.type === "analyze") {
36
+ const result = await analyzeMediaEditAudio(request, options);
37
+ scope.postMessage({ type: "analyzed", id: request.id, result });
38
+ } else {
39
+ const result = await previewMediaEditAudio(request, options);
40
+ const buffers = [...result.original, ...result.processed].map(
41
+ (ch) => ch.buffer
42
+ );
43
+ scope.postMessage({ type: "previewed", id: request.id, result }, buffers);
44
+ }
45
+ } catch (err) {
46
+ scope.postMessage({
47
+ type: "error",
48
+ id: request.id,
49
+ message: err instanceof Error ? err.message : String(err),
50
+ aborted: isAbort(err)
51
+ });
52
+ } finally {
53
+ jobs.delete(request.id);
54
+ }
55
+ }
56
+ scope.onmessage = (event) => {
57
+ const request = event.data;
58
+ if (request.type === "cancel") {
59
+ jobs.get(request.id)?.abort();
60
+ return;
61
+ }
62
+ void run(request);
63
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-video-react",
3
- "version": "2.4.11",
3
+ "version": "2.4.13",
4
4
  "description": "React components for browser-based MP4 and animated-GIF export of Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -53,6 +53,10 @@
53
53
  "./encoder": {
54
54
  "types": "./dist/encoder/index.d.ts",
55
55
  "import": "./dist/encoder/index.js"
56
+ },
57
+ "./media-edit": {
58
+ "types": "./dist/media-edit/index.d.ts",
59
+ "import": "./dist/media-edit/index.js"
56
60
  }
57
61
  },
58
62
  "files": [
@@ -73,20 +77,22 @@
73
77
  "react-dom": "^18.0.0 || ^19.0.0"
74
78
  },
75
79
  "dependencies": {
76
- "@bendyline/squisq": "2.11.7",
77
- "@bendyline/squisq-video": "2.3.10",
78
- "@bendyline/squisq-react": "2.11.7",
80
+ "@bendyline/squisq": "2.11.8",
81
+ "@bendyline/squisq-video": "2.3.11",
82
+ "@bendyline/squisq-react": "2.11.9",
79
83
  "@ffmpeg/core": "0.12.9",
80
84
  "@ffmpeg/ffmpeg": "0.12.15",
81
85
  "@ffmpeg/util": "0.12.2",
86
+ "@shiguredo/rnnoise-wasm": "2025.1.5",
82
87
  "html2canvas": "1.4.1"
83
88
  },
84
89
  "devDependencies": {
85
90
  "@types/react": "18.3.28",
86
91
  "@types/react-dom": "18.3.7",
92
+ "mediabunny": "1.57.0",
93
+ "mp4-muxer": "5.2.2",
87
94
  "react": "18.3.1",
88
95
  "react-dom": "18.3.1",
89
- "mp4-muxer": "5.2.2",
90
96
  "tsup": "8.5.1",
91
97
  "typescript": "5.9.3"
92
98
  },