@bendyline/squisq-editor-react 2.4.5 → 2.4.6

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.
@@ -0,0 +1,560 @@
1
+ import { SyntheticEvent } from 'react';
2
+ import { Doc, MediaProvider } from '@bendyline/squisq/schemas';
3
+ import { ContentContainer } from '@bendyline/squisq/storage';
4
+ import { NarrationScript, VadConfig, NarrationTrace, NarrationAlignment } from '@bendyline/squisq/narration';
5
+
6
+ /**
7
+ * Format probing for MediaRecorder.
8
+ *
9
+ * Different browsers expose different container/codec combinations. Chrome
10
+ * and Firefox produce WebM (VP8/VP9 + Opus); Safari produces MP4 (H.264 +
11
+ * AAC). We probe at runtime via `MediaRecorder.isTypeSupported()` and pick
12
+ * the best supported option, falling back to whatever the browser hands
13
+ * back when no probe succeeds.
14
+ */
15
+ /** What the recorded stream is intended to capture. */
16
+ type CaptureKind = 'audio' | 'video';
17
+ /** A probed format choice — what to pass to `MediaRecorder` and where to write it. */
18
+ interface ResolvedFormat {
19
+ /** MIME type to pass to `new MediaRecorder(stream, { mimeType })`. Empty string means "let the browser pick". */
20
+ mimeType: string;
21
+ /** File extension to use when writing to the container, including the leading dot. */
22
+ extension: string;
23
+ /** Container directory inside the `ContentContainer` (no trailing slash). */
24
+ directory: 'audio' | 'video';
25
+ }
26
+ /**
27
+ * Resolve the format the recorder will use for a given capture kind. If a
28
+ * `preferred` MIME type is supported, it wins; otherwise we fall through
29
+ * the priority list. When nothing matches (extremely old browser), we
30
+ * return an empty `mimeType` — `MediaRecorder` will pick a default and we
31
+ * tag the file with `.webm` as a best guess.
32
+ */
33
+ declare function resolveFormat(kind: CaptureKind, preferred?: string): ResolvedFormat;
34
+ /**
35
+ * `MediaRecorder` support probe. Returns false when running in a
36
+ * non-browser environment (e.g. SSR) or on a browser that doesn't
37
+ * implement the API at all.
38
+ */
39
+ declare function supportsMediaRecorder(): boolean;
40
+ /**
41
+ * `getUserMedia` support probe (for mic / camera capture).
42
+ */
43
+ declare function supportsUserMedia(): boolean;
44
+ /**
45
+ * `getDisplayMedia` support probe (for screen capture). Browsers may
46
+ * implement `mediaDevices` without `getDisplayMedia` (Firefox on Android
47
+ * being the long-standing example), so this is its own probe.
48
+ */
49
+ declare function supportsDisplayMedia(): boolean;
50
+ /**
51
+ * Build a default filename for a recording. `basename` is a hint
52
+ * (e.g. user-typed name); when omitted, a sortable timestamp is used so
53
+ * concurrent recordings don't collide. General recorder callers can supply a
54
+ * source-aware `seed`; dedicated narration callers omit it and retain the
55
+ * historical `narration-*` default.
56
+ */
57
+ type RecordingFilenameSeed = 'audio' | 'camera' | 'camera-audio' | 'screen' | 'screen-audio';
58
+ declare function buildFilename(kind: CaptureKind, extension: string, basename?: string, seed?: RecordingFilenameSeed): string;
59
+
60
+ /**
61
+ * useMediaRecorder
62
+ *
63
+ * React wrapper around `MediaRecorder` that handles stream acquisition,
64
+ * the recorder lifecycle, and produces a single `Blob` on stop. Selects
65
+ * a browser-supported MIME type via {@link resolveFormat}.
66
+ *
67
+ * Mirrors the shape of `useVideoExport` in `@bendyline/squisq-video-react`
68
+ * (request → start → stop → blob), inverted for capture rather than
69
+ * export.
70
+ *
71
+ * The `'screen+camera'` source is the one exception to "one stream, one
72
+ * blob": it drives TWO `MediaRecorder`s in lockstep (screen + system audio
73
+ * on the primary lane, camera + mic on a secondary lane), because a single
74
+ * recorder can only hold one video track. The secondary lane surfaces as
75
+ * {@link UseMediaRecorderResult.camera}; every other source leaves it null.
76
+ * The two lanes' start skew is measured (`cameraOffsetSec`) so the composed
77
+ * playback can line the presenter bubble up with the screen.
78
+ */
79
+
80
+ /**
81
+ * Which capture source to use. `screen+mic` mixes the microphone into the
82
+ * screen stream (one file); `screen+camera` records screen and camera as two
83
+ * separate files in lockstep.
84
+ */
85
+ type RecorderSource = 'mic' | 'camera' | 'screen' | 'screen+mic' | 'screen+camera';
86
+ type RecorderAudioBitrateMode = 'constant' | 'variable';
87
+ /**
88
+ * MediaRecorder options standardized after the DOM library fields currently
89
+ * shipped by TypeScript. They are passed through only when explicitly set.
90
+ */
91
+ interface RecorderExtendedMediaOptions {
92
+ audioBitrateMode?: RecorderAudioBitrateMode;
93
+ videoKeyFrameIntervalDuration?: number;
94
+ videoKeyFrameIntervalCount?: number;
95
+ }
96
+ /** Discriminated state describing what the recorder is currently doing. */
97
+ type RecorderState = 'idle' | 'requesting' | 'ready' | 'recording' | 'stopping' | 'stopped' | 'error';
98
+ /**
99
+ * The camera companion lane, present only for `source === 'screen+camera'`
100
+ * (null for every other source). Its `blob` is null until the take stops.
101
+ */
102
+ interface RecorderCameraLane {
103
+ /** Camera stream (video + mic when requested); inactive after stop, null after teardown. */
104
+ stream: MediaStream | null;
105
+ /** Final camera `Blob` after `stop()` resolves, or null while recording. */
106
+ blob: Blob | null;
107
+ /** MIME type of the camera lane (shares the video format with the screen lane). */
108
+ mimeType: string | null;
109
+ /** File extension matching `mimeType` (e.g. `.webm`). */
110
+ extension: string | null;
111
+ }
112
+ interface UseMediaRecorderOptions {
113
+ /** Which capture pipeline to use (default: `'mic'`). */
114
+ source?: RecorderSource;
115
+ /**
116
+ * Preferred MIME type override. When the browser supports it, this
117
+ * wins over the default candidate list. When unset (or unsupported),
118
+ * the hook probes a built-in priority list.
119
+ */
120
+ mimeType?: string;
121
+ /**
122
+ * Video track constraints for camera sources. Also used for screen capture
123
+ * when `screenVideoConstraints` is omitted, preserving the original API.
124
+ */
125
+ videoConstraints?: MediaTrackConstraints | boolean;
126
+ /**
127
+ * Display-video constraints for screen sources. Keeping this independent
128
+ * lets a dual screen+camera take request different dimensions/aspect ratios
129
+ * for each lane.
130
+ */
131
+ screenVideoConstraints?: MediaTrackConstraints | boolean;
132
+ /** Audio-track constraints specific to getDisplayMedia system audio. */
133
+ screenAudioConstraints?: MediaTrackConstraints;
134
+ /** Audio track constraints for mic / camera / screen+mic sources. */
135
+ audioConstraints?: MediaTrackConstraints | boolean;
136
+ /**
137
+ * Bits-per-second hint passed to `MediaRecorder`. Most browsers cap to
138
+ * reasonable defaults internally; leaving this undefined is usually
139
+ * fine.
140
+ */
141
+ bitsPerSecond?: number;
142
+ /** Audio bitrate hint passed to `MediaRecorder`. */
143
+ audioBitsPerSecond?: number;
144
+ /** Video bitrate hint passed to `MediaRecorder`. */
145
+ videoBitsPerSecond?: number;
146
+ /** Constant/variable audio encoder preference, when supported. */
147
+ audioBitrateMode?: RecorderAudioBitrateMode;
148
+ /** Requested milliseconds between video keyframes. Mutually exclusive with count. */
149
+ videoKeyFrameIntervalDuration?: number;
150
+ /** Requested frames between video keyframes. Mutually exclusive with duration. */
151
+ videoKeyFrameIntervalCount?: number;
152
+ /**
153
+ * Whether to capture system (tab/monitor) audio. Browser support is limited
154
+ * (desktop Chromium only); when unsupported the resulting stream simply omits
155
+ * it. How it is obtained depends on the source:
156
+ * - `'screen'` / `'screen+mic'` / `'screen+camera'` — folded into the screen
157
+ * lane's `getDisplayMedia` (rides the SCREEN file for the dual source).
158
+ * - `'mic'` / `'camera'` — captured via a SEPARATE `getDisplayMedia` whose
159
+ * video is discarded, then mixed into the mic/camera file. The browser still
160
+ * shows the screen/tab picker (audio-only display capture isn't allowed).
161
+ */
162
+ systemAudio?: boolean;
163
+ /**
164
+ * For `source === 'camera'`, whether to include the microphone track.
165
+ * Defaults to `true` (camera + mic). Set `false` to capture silent
166
+ * video. Ignored for `'mic'`/`'screen'`/`'screen+mic'`, whose mic
167
+ * handling is encoded in the source itself. For `'screen+camera'` this
168
+ * gates the microphone on the CAMERA lane (the screen lane never carries
169
+ * the mic — system audio rides it instead).
170
+ */
171
+ includeMicrophone?: boolean;
172
+ }
173
+ interface UseMediaRecorderResult {
174
+ /** Current recorder state. */
175
+ state: RecorderState;
176
+ /** `MediaStream` acquired by `request()`; live during preview/recording and
177
+ * inactive after stop. For `'screen+camera'` this is the SCREEN stream (see
178
+ * `camera` for the other). */
179
+ stream: MediaStream | null;
180
+ /** Final `Blob` after `stop()` resolves, or `null` while recording. For
181
+ * `'screen+camera'` this is the SCREEN file. */
182
+ blob: Blob | null;
183
+ /** MIME type the recorder actually used (after `request()`). */
184
+ mimeType: string | null;
185
+ /** File extension matching `mimeType` (e.g. `.webm`). */
186
+ extension: string | null;
187
+ /** Suggested container directory (`'audio'` for mic, `'video'` for camera/screen). */
188
+ directory: 'audio' | 'video' | null;
189
+ /** Milliseconds elapsed since `start()` was called. Updates ~10× per second while recording. */
190
+ durationMs: number;
191
+ /** Most recent error, if any. */
192
+ error: Error | null;
193
+ /**
194
+ * The camera companion lane for `source === 'screen+camera'`, else null.
195
+ * Its `blob` lands together with the primary `blob` when `stop()` resolves.
196
+ */
197
+ camera: RecorderCameraLane | null;
198
+ /**
199
+ * Camera onstart minus screen onstart, in seconds (positive = camera
200
+ * started later). Null until both lanes have reported `onstart`, and for
201
+ * every non-dual source.
202
+ */
203
+ cameraOffsetSec: number | null;
204
+ /**
205
+ * Acquire the stream and prepare a `MediaRecorder`. After this resolves
206
+ * the hook is in `'ready'` state and a `<video>`/`<audio>` element can
207
+ * preview `stream`. Call `start()` to begin recording.
208
+ */
209
+ request: () => Promise<void>;
210
+ /** Start recording. Must be called from `'ready'`. */
211
+ start: () => void;
212
+ /**
213
+ * Stop recording and resolve with the resulting `Blob`. Safe to call
214
+ * from `'recording'`; a no-op from any other state (resolves with the
215
+ * existing `blob`, or `null`). Once the take has flushed, all capture
216
+ * tracks are stopped so browser sharing / camera / microphone indicators
217
+ * do not remain active during review.
218
+ */
219
+ stop: () => Promise<Blob | null>;
220
+ /**
221
+ * Tear everything down — stops the recorder if running, releases all
222
+ * tracks, disposes the AudioContext mixer (if any), and returns to
223
+ * `'idle'`. Always safe to call.
224
+ */
225
+ cancel: () => void;
226
+ /** Clear the current take. A new permission request may be needed before re-recording. */
227
+ reset: () => void;
228
+ }
229
+ /**
230
+ * Returns the kind of capture that the given source produces. Exposed
231
+ * separately from {@link useMediaRecorder} so non-React callers
232
+ * (e.g. headless tests) can resolve a format up front.
233
+ */
234
+ declare function getCaptureKind(source: RecorderSource): CaptureKind;
235
+ declare function useMediaRecorder(options?: UseMediaRecorderOptions): UseMediaRecorderResult;
236
+
237
+ /**
238
+ * Mic capture + PCM transport for the teleprompter.
239
+ *
240
+ * getUserMedia → AudioContext → AudioWorklet tap → subscriber callbacks
241
+ * with 1024-sample Float32Array hops on the main thread. The DSP itself
242
+ * lives in core (`@bendyline/squisq/narration`) — this hook only moves
243
+ * samples. Falls back to a ScriptProcessorNode when `audioWorklet` is
244
+ * unavailable (same push model, so pacing still isn't tied to rAF).
245
+ */
246
+ type MicAnalysisStatus = 'idle' | 'starting' | 'live' | 'error';
247
+ type PcmHopListener = (pcm: Float32Array, audioTimeSec: number) => void;
248
+ interface MicAnalysisHandle {
249
+ status: MicAnalysisStatus;
250
+ error: Error | null;
251
+ /** The live mic stream (feeds the narration recorder too), or null. */
252
+ stream: MediaStream | null;
253
+ /** AudioContext sample rate once live. */
254
+ sampleRate: number | null;
255
+ /** Known audio inputs (labels appear after the first grant). */
256
+ devices: MediaDeviceInfo[];
257
+ /**
258
+ * Start (or restart) capture. Resolves with the live stream — callers
259
+ * that need it immediately must use the return value, not the `stream`
260
+ * state field, which only updates on the NEXT render (stale-closure
261
+ * hazard right after an await). Null on failure/supersession.
262
+ */
263
+ start: (deviceId: string | null) => Promise<MediaStream | null>;
264
+ stop: () => void;
265
+ /** Subscribe to PCM hops; returns an unsubscribe. */
266
+ subscribeHop: (listener: PcmHopListener) => () => void;
267
+ }
268
+ declare function useMicAnalysis(constraints?: MediaTrackConstraints): MicAnalysisHandle;
269
+
270
+ /**
271
+ * Shared types for the Narrate (teleprompter) display mode.
272
+ */
273
+ interface TeleprompterPrefs {
274
+ /** Prompter type size in px (28–96). */
275
+ fontSizePx: number;
276
+ /** Beam-splitter mirror flip. */
277
+ mirrored: boolean;
278
+ /** Base speaking rate in words per minute (80–260). */
279
+ baseWpm: number;
280
+ /** Voice-adaptive pacing on/off; off = constant-rate manual mode. */
281
+ voiceTracking: boolean;
282
+ /** VAD sensitivity 0–1 (0.5 = engine defaults). */
283
+ vadSensitivity: number;
284
+ /** Countdown before the prompter starts rolling. */
285
+ countdownSec: 0 | 3 | 5 | 10;
286
+ /** Eye-line chevrons + focus band. */
287
+ lineGuide: boolean;
288
+ /** Preferred mic device id (null = system default). */
289
+ micDeviceId: string | null;
290
+ }
291
+ declare const DEFAULT_TELEPROMPTER_PREFS: TeleprompterPrefs;
292
+ declare const TELEPROMPTER_PREF_LIMITS: Readonly<{
293
+ fontSizePx: {
294
+ min: number;
295
+ max: number;
296
+ };
297
+ baseWpm: {
298
+ min: number;
299
+ max: number;
300
+ };
301
+ vadSensitivity: {
302
+ min: number;
303
+ max: number;
304
+ };
305
+ micDeviceIdMaxLength: 1024;
306
+ }>;
307
+ /**
308
+ * Validate persisted or externally supplied preferences and return a complete,
309
+ * bounded value. Unknown keys are ignored; invalid fields retain `fallback`.
310
+ */
311
+ declare function normalizeTeleprompterPrefs(value: unknown, fallback?: TeleprompterPrefs): TeleprompterPrefs;
312
+ type PrompterTransport = 'stopped' | 'countdown' | 'rolling' | 'paused' | 'finished';
313
+ /** Floating-surface tier, best first. */
314
+ type FloatTier = 'document-pip' | 'video-pip' | 'popup' | 'docked';
315
+
316
+ /**
317
+ * The teleprompter controller — single source of truth, always in the
318
+ * MAIN window (floats are render targets only).
319
+ *
320
+ * Loop-ownership rule: **the audio worklet owns time; the main window
321
+ * owns state; the visible surface owns pixels.** Voice-mode position
322
+ * advances on worklet PCM hops (immune to rAF/timer throttling while
323
+ * the browser is occluded by recording software); manual constant-rate
324
+ * mode uses rAF and is documented as best-effort under occlusion. React
325
+ * state publishes at ~15 Hz; per-hop subscribers (`subscribeTick`)
326
+ * exist for the video-PiP canvas pump.
327
+ */
328
+
329
+ interface TeleprompterKeyEvent {
330
+ readonly defaultPrevented: boolean;
331
+ readonly key: string;
332
+ readonly repeat: boolean;
333
+ readonly target: EventTarget | null;
334
+ preventDefault: () => void;
335
+ }
336
+ interface TeleprompterController {
337
+ script: NarrationScript | null;
338
+ transport: PrompterTransport;
339
+ countdownRemaining: number | null;
340
+ /** Fractional word position (published ~15 Hz). */
341
+ wordPos: number;
342
+ /** Smoothed mic level 0–1 for the meter. */
343
+ micLevel: number;
344
+ /** VAD flag for the meter tint. */
345
+ voiceActive: boolean;
346
+ mic: MicAnalysisHandle;
347
+ prefs: TeleprompterPrefs;
348
+ setPrefs: (patch: Partial<TeleprompterPrefs>) => void;
349
+ play: () => void;
350
+ pause: () => void;
351
+ restart: () => void;
352
+ /** Move by spoken words and re-anchor voice tracking. */
353
+ nudge: (deltaWords: number) => void;
354
+ /** Jump to an absolute token index (e.g. click a block marker). */
355
+ seekToToken: (tokenIndex: number) => void;
356
+ /** Per-analysis-tick subscription (video-PiP pump). Not throttled. */
357
+ subscribeTick: (cb: (wordPos: number) => void) => () => void;
358
+ handleKeyDown: (event: TeleprompterKeyEvent) => void;
359
+ }
360
+ /** Map the 0–1 sensitivity pref onto VAD thresholds (0.5 = engine defaults). */
361
+ declare function vadConfigForSensitivity(sensitivity: number): Partial<VadConfig>;
362
+ declare function useTeleprompter(opts: {
363
+ doc: Doc | null;
364
+ micConstraints?: MediaTrackConstraints;
365
+ }): TeleprompterController;
366
+
367
+ /**
368
+ * Floating-teleprompter window manager — framework-free.
369
+ *
370
+ * Capability ladder, best first:
371
+ * 1. `document-pip` — Document Picture-in-Picture (Chromium 116+,
372
+ * Firefox 151+): a true always-on-top window hosting live DOM; the
373
+ * React surface portals into it.
374
+ * 2. `video-pip` — canvas → `captureStream(0)` → `<video>` →
375
+ * `requestPictureInPicture()` (Safari's only always-on-top path;
376
+ * `webkitSetPresentationMode` fallback). Read-only: the main
377
+ * window draws frames and calls `requestFrame()` on analysis
378
+ * ticks — never rAF, which throttles under occlusion.
379
+ * 3. `popup` — `window.open` (positionable, not always-on-top).
380
+ * 4. `docked` — no float.
381
+ *
382
+ * Every tier is feature-detected at open time and falls through to the
383
+ * next on ANY failure. All interactive state stays in the main window;
384
+ * floats are render targets only.
385
+ */
386
+
387
+ interface FloatOpenOptions {
388
+ width: number;
389
+ height: number;
390
+ /** Try this tier first; the ladder continues below it on failure. */
391
+ preferredTier?: FloatTier;
392
+ title: string;
393
+ }
394
+ interface CanvasSink {
395
+ canvas: HTMLCanvasElement;
396
+ width: number;
397
+ height: number;
398
+ /** Push the freshly drawn canvas frame into the PiP video. */
399
+ requestFrame: () => void;
400
+ }
401
+ type FloatEvent = 'closed' | 'tierchange';
402
+ interface FloatingWindowManager {
403
+ readonly tier: FloatTier;
404
+ readonly isOpen: boolean;
405
+ /** Resolves with the tier that actually opened ('docked' if none could). */
406
+ open(opts: FloatOpenOptions): Promise<FloatTier>;
407
+ /** Idempotent; restores docked and emits 'closed'. */
408
+ close(): void;
409
+ /** Portal container for 'document-pip' | 'popup'; null otherwise. */
410
+ getPortalTarget(): HTMLElement | null;
411
+ /** Canvas sink for 'video-pip'; null otherwise. */
412
+ getCanvasSink(): CanvasSink | null;
413
+ on(event: FloatEvent, cb: (tier: FloatTier) => void): () => void;
414
+ /** Tear everything down (unmount); like close() but silent-safe. */
415
+ dispose(): void;
416
+ }
417
+ /** Feature-detect the available float tiers, best first. */
418
+ declare function detectFloatTiers(): FloatTier[];
419
+ declare function createFloatingWindowManager(deps: {
420
+ styleCss: string;
421
+ }): FloatingWindowManager;
422
+
423
+ /**
424
+ * React binding over {@link createFloatingWindowManager}: exposes the
425
+ * current tier + portal target as state and guarantees the float closes
426
+ * when the owning view unmounts (mode switch, Use-tab exit, shell
427
+ * teardown).
428
+ */
429
+
430
+ interface FloatingWindowHandle {
431
+ tier: FloatTier;
432
+ isOpen: boolean;
433
+ /** Non-docked tiers this browser supports, best first. */
434
+ supportedTiers: FloatTier[];
435
+ portalTarget: HTMLElement | null;
436
+ canvasSink: CanvasSink | null;
437
+ open: (preferredTier?: FloatTier) => Promise<void>;
438
+ close: () => void;
439
+ }
440
+ declare function useFloatingWindow(styleCss: string): FloatingWindowHandle;
441
+
442
+ /**
443
+ * Narration recorder for the teleprompter — an in-place (no modal)
444
+ * capture flow.
445
+ *
446
+ * Audio records THE MIC ANALYSIS STREAM (what paces the prompter is
447
+ * exactly what lands in the take); the optional camera is a separate
448
+ * video-only capture whose start skew vs the audio recorder is measured
449
+ * and persisted (`cameraOffsetSec`) — the audio file is the doc clock,
450
+ * so camera skew never affects narration timing. While recording, a
451
+ * sparse live trace of the prompter position is sampled; after stop,
452
+ * the take is decoded and run through core's offline aligner
453
+ * (`alignNarration`) to produce word/block timestamps for the sidecar.
454
+ * Decode/alignment failure degrades gracefully: saving still works,
455
+ * just without re-timing.
456
+ */
457
+
458
+ type NarrationRecorderState = 'idle' | 'starting' | 'recording' | 'processing' | 'review' | 'saving' | 'error';
459
+ type NarrationMediaRecorderOptions = MediaRecorderOptions & RecorderExtendedMediaOptions;
460
+ interface NarrationTake {
461
+ audioBlob: Blob;
462
+ audioMime: string;
463
+ audioExt: string;
464
+ cameraBlob: Blob | null;
465
+ cameraMime: string | null;
466
+ cameraExt: string | null;
467
+ durationSec: number;
468
+ cameraOffsetSec: number | undefined;
469
+ trace: NarrationTrace;
470
+ alignment: NarrationAlignment | null;
471
+ script: NarrationScript;
472
+ }
473
+ interface UseNarrationRecorderOptions {
474
+ mic: MicAnalysisHandle;
475
+ getScript: () => NarrationScript | null;
476
+ /** Live prompter position, sampled into the trace while recording. */
477
+ getWordPos: () => number;
478
+ getMicDeviceId: () => string | null;
479
+ /** Constraints for the optional, video-only camera companion. */
480
+ cameraConstraints?: MediaTrackConstraints | boolean;
481
+ /** MediaRecorder hints for the primary narration audio file. */
482
+ audioRecorderOptions?: NarrationMediaRecorderOptions;
483
+ /** MediaRecorder hints for the camera companion file. */
484
+ cameraRecorderOptions?: NarrationMediaRecorderOptions;
485
+ /** Fired when capture actually starts (View starts the prompter). */
486
+ onRecordingStart?: () => void;
487
+ onRecordingStop?: () => void;
488
+ }
489
+ interface NarrationRecorderController {
490
+ state: NarrationRecorderState;
491
+ error: Error | null;
492
+ withCamera: boolean;
493
+ setWithCamera: (on: boolean) => void;
494
+ /** Live camera stream for the self-view while recording. */
495
+ cameraStream: MediaStream | null;
496
+ take: NarrationTake | null;
497
+ start: () => Promise<void>;
498
+ stop: () => Promise<void>;
499
+ retake: () => void;
500
+ discard: () => void;
501
+ /** Transition into/out of 'saving'; the View owns the actual I/O. */
502
+ beginSave: () => void;
503
+ finishSave: (ok: boolean, error?: Error) => void;
504
+ }
505
+ declare function useNarrationRecorder(options: UseNarrationRecorderOptions): NarrationRecorderController;
506
+
507
+ /**
508
+ * useNarrationStage — the state/orchestration half of the narration stage,
509
+ * extracted from TeleprompterView so other hosts (the Record media dialog)
510
+ * can mount the same prompter + recorder + save pipeline.
511
+ *
512
+ * Owns the teleprompter controller (single source of truth in the main
513
+ * window), the floating-window handle, the narration recorder, and the
514
+ * retry-idempotent save flow. Pair with <NarrationStage> for the DOM.
515
+ *
516
+ * Recording is prop-gated: pass `recording` deps (media provider + markdown
517
+ * writers) to enable capture; without them this is a pure prompter with zero
518
+ * capture code paths.
519
+ */
520
+
521
+ /** Editor plumbing the recording flow needs; omit for prompter-only use. */
522
+ interface TeleprompterRecordingDeps {
523
+ mediaProvider: MediaProvider;
524
+ container: ContentContainer | null;
525
+ markdownSource: string;
526
+ setMarkdownSource: (next: string) => void;
527
+ bumpMediaRevision: () => void;
528
+ }
529
+ interface UseNarrationStageOptions {
530
+ doc: Doc | null;
531
+ /** Recording deps; null/omitted disables the Record affordance. */
532
+ recording?: TeleprompterRecordingDeps | null;
533
+ /** Optional user-chosen filename base threaded into the save plan. */
534
+ getAudioBasename?: () => string | undefined;
535
+ /** Microphone constraints shared by voice analysis and audio recording. */
536
+ micConstraints?: MediaTrackConstraints;
537
+ /** Constraints for the optional narration camera lane. */
538
+ cameraConstraints?: MediaTrackConstraints;
539
+ /** MediaRecorder hints for the narration audio file. */
540
+ audioRecorderOptions?: NarrationMediaRecorderOptions;
541
+ /** MediaRecorder hints for the optional narration camera file. */
542
+ cameraRecorderOptions?: NarrationMediaRecorderOptions;
543
+ }
544
+ interface NarrationStageHandle {
545
+ controller: TeleprompterController;
546
+ float: FloatingWindowHandle;
547
+ recorder: NarrationRecorderController;
548
+ /** Pass-through so the stage component can gate its record slot. */
549
+ recording: TeleprompterRecordingDeps | null;
550
+ saveNotice: string | null;
551
+ dismissSaveNotice: () => void;
552
+ handleSave: () => Promise<void>;
553
+ handleRetake: () => void;
554
+ handleDiscard: () => void;
555
+ reviewAudioUrl: string | null;
556
+ handleReviewTimeUpdate: (event: SyntheticEvent<HTMLAudioElement>) => void;
557
+ }
558
+ declare function useNarrationStage(opts: UseNarrationStageOptions): NarrationStageHandle;
559
+
560
+ export { useMediaRecorder as A, useMicAnalysis as B, type CanvasSink as C, DEFAULT_TELEPROMPTER_PREFS as D, useNarrationRecorder as E, type FloatOpenOptions as F, useNarrationStage as G, useTeleprompter as H, vadConfigForSensitivity as I, TELEPROMPTER_PREF_LIMITS as J, normalizeTeleprompterPrefs as K, type MicAnalysisHandle as M, type NarrationRecorderController as N, type PrompterTransport as P, type RecorderAudioBitrateMode as R, type TeleprompterController as T, type UseMediaRecorderOptions as U, type CaptureKind as a, type FloatTier as b, type FloatingWindowHandle as c, type FloatingWindowManager as d, type MicAnalysisStatus as e, type NarrationRecorderState as f, type NarrationStageHandle as g, type NarrationTake as h, type RecorderCameraLane as i, type RecorderExtendedMediaOptions as j, type RecorderSource as k, type RecorderState as l, type ResolvedFormat as m, type TeleprompterPrefs as n, type TeleprompterRecordingDeps as o, type UseMediaRecorderResult as p, type UseNarrationStageOptions as q, buildFilename as r, createFloatingWindowManager as s, detectFloatTiers as t, getCaptureKind as u, resolveFormat as v, supportsDisplayMedia as w, supportsMediaRecorder as x, supportsUserMedia as y, useFloatingWindow as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-editor-react",
3
- "version": "2.4.5",
3
+ "version": "2.4.6",
4
4
  "description": "React editor shell with raw/WYSIWYG/preview modes for Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -84,9 +84,9 @@
84
84
  "react-dom": "^18.0.0 || ^19.0.0"
85
85
  },
86
86
  "dependencies": {
87
- "@bendyline/squisq": "2.4.3",
88
- "@bendyline/squisq-formats": "2.3.7",
89
- "@bendyline/squisq-react": "2.4.5",
87
+ "@bendyline/squisq": "2.4.4",
88
+ "@bendyline/squisq-formats": "2.3.8",
89
+ "@bendyline/squisq-react": "2.4.6",
90
90
  "@fortawesome/fontawesome-free": "7.2.0",
91
91
  "@tiptap/core": "2.27.2",
92
92
  "@tiptap/extension-heading": "2.27.2",