@bendyline/squisq-editor-react 2.13.22 → 2.13.23

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.
@@ -582,12 +582,17 @@ import {
582
582
  useRef as useRef3,
583
583
  useEffect as useEffect3
584
584
  } from "react";
585
+ import { useVideoDurations } from "@bendyline/squisq-react";
585
586
  import {
586
587
  parseMarkdown as parseMarkdown4,
587
588
  stringifyMarkdown,
588
589
  resolveWikiLinks
589
590
  } from "@bendyline/squisq/markdown";
590
- import { markdownToDoc } from "@bendyline/squisq/doc";
591
+ import {
592
+ applyVideoDurations,
593
+ collectVideoDurationSources,
594
+ markdownToDoc
595
+ } from "@bendyline/squisq/doc";
591
596
 
592
597
  // src/speech/useDictationController.ts
593
598
  import {
@@ -1022,7 +1027,7 @@ function useDictationController({
1022
1027
  }
1023
1028
  return;
1024
1029
  }
1025
- const { startDictationSession } = await import("./dictationSession-AVCXB3JR.js");
1030
+ const { startDictationSession } = await import("./dictationSession-WEXEY5NS.js");
1026
1031
  if (!isCurrent()) return;
1027
1032
  const handle = await startDictationSession({
1028
1033
  provider: active,
@@ -1659,6 +1664,7 @@ function EditorProvider({
1659
1664
  versioningAutoSaveIdleMs = DEFAULT_AUTOSAVE_IDLE_MS,
1660
1665
  onSaveVersion,
1661
1666
  mediaProvider = null,
1667
+ mediaBasePath = "/",
1662
1668
  mediaEditRenders = null,
1663
1669
  imageDisplayMode = "inline",
1664
1670
  mentionProvider = null,
@@ -1679,6 +1685,7 @@ function EditorProvider({
1679
1685
  fileName,
1680
1686
  saveCoverImageOutput,
1681
1687
  saveDashboardImageOutput,
1688
+ saveMediaEditOutput,
1682
1689
  language,
1683
1690
  findMode: controlledFindMode,
1684
1691
  onFindModeChange,
@@ -2037,6 +2044,15 @@ function EditorProvider({
2037
2044
  cancelled = true;
2038
2045
  };
2039
2046
  }, [markdownDoc, wikiLinks]);
2047
+ const videoDurationSources = useMemo4(
2048
+ () => presentationDoc ? collectVideoDurationSources(presentationDoc) : [],
2049
+ [presentationDoc]
2050
+ );
2051
+ const videoDurations = useVideoDurations(videoDurationSources, mediaBasePath, mediaProvider);
2052
+ const timedDoc = useMemo4(
2053
+ () => presentationDoc && videoDurations.size > 0 ? applyVideoDurations(presentationDoc, (src) => videoDurations.get(src)) : presentationDoc,
2054
+ [presentationDoc, videoDurations]
2055
+ );
2040
2056
  useEffect3(() => {
2041
2057
  if (editorMode !== "markdown") return;
2042
2058
  if (skipInitialDebouncedParseRef.current) {
@@ -2396,7 +2412,7 @@ function EditorProvider({
2396
2412
  () => ({
2397
2413
  markdownSource,
2398
2414
  markdownDoc,
2399
- doc: presentationDoc,
2415
+ doc: timedDoc,
2400
2416
  activeView,
2401
2417
  parseError,
2402
2418
  isParsing,
@@ -2451,6 +2467,7 @@ function EditorProvider({
2451
2467
  fileName,
2452
2468
  saveCoverImageOutput,
2453
2469
  saveDashboardImageOutput,
2470
+ saveMediaEditOutput,
2454
2471
  setMarkdownSource,
2455
2472
  setEditorSource,
2456
2473
  setLayoutMode,
@@ -2487,7 +2504,7 @@ function EditorProvider({
2487
2504
  [
2488
2505
  markdownSource,
2489
2506
  markdownDoc,
2490
- presentationDoc,
2507
+ timedDoc,
2491
2508
  activeView,
2492
2509
  parseError,
2493
2510
  isParsing,
@@ -2536,6 +2553,7 @@ function EditorProvider({
2536
2553
  fileName,
2537
2554
  saveCoverImageOutput,
2538
2555
  saveDashboardImageOutput,
2556
+ saveMediaEditOutput,
2539
2557
  setMarkdownSource,
2540
2558
  setEditorSource,
2541
2559
  setLayoutMode,
@@ -3174,6 +3192,7 @@ export {
3174
3192
  resolveEmbeddedMediaTiming,
3175
3193
  collectEmbeddedVideoSchedule,
3176
3194
  collectTimelinePlaybackSchedule,
3195
+ listEditableMedia,
3177
3196
  findEditableMedia,
3178
3197
  groupCompanions,
3179
3198
  mediaTargetFromElement,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  NarrationStage,
3
3
  useNarrationStage
4
- } from "./chunk-JCIK2QU5.js";
4
+ } from "./chunk-2KTMXJWB.js";
5
5
 
6
6
  // src/teleprompter/TeleprompterView.tsx
7
7
  import { jsx } from "react/jsx-runtime";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RecorderModal
3
- } from "./chunk-APTVXGK6.js";
3
+ } from "./chunk-QI5SHUVT.js";
4
4
 
5
5
  // src/recorder/RecorderButton.tsx
6
6
  import { useCallback, useState } from "react";
@@ -16,16 +16,18 @@ import {
16
16
  resolveMaxRecordingBytes,
17
17
  useNarrationStage,
18
18
  useStreamPreview
19
- } from "./chunk-JCIK2QU5.js";
19
+ } from "./chunk-2KTMXJWB.js";
20
20
  import {
21
21
  buildFilename,
22
+ dualFilenameSeeds,
23
+ recordingFilenameSeed,
22
24
  requestMicStream,
23
25
  resolveFormat,
24
26
  supportsDisplayMedia,
25
27
  supportsMediaRecorder,
26
28
  supportsSystemAudioCapture,
27
29
  supportsUserMedia
28
- } from "./chunk-DNJ6HOES.js";
30
+ } from "./chunk-QXHX7N4V.js";
29
31
  import {
30
32
  buildTimingJson,
31
33
  encodeNarrationTimingJson,
@@ -33,6 +35,113 @@ import {
33
35
  timingPathFor
34
36
  } from "./chunk-GG35EBRB.js";
35
37
 
38
+ // src/recorder/captureErrors.ts
39
+ var displayCaptureErrors = /* @__PURE__ */ new WeakSet();
40
+ function markDisplayCaptureError(error) {
41
+ if (typeof error === "object" && error !== null) displayCaptureErrors.add(error);
42
+ return error;
43
+ }
44
+ var PRIVACY_HINT = "your system's privacy settings allow apps to use it";
45
+ function errorName(error) {
46
+ return typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
47
+ }
48
+ function errorMessage(error) {
49
+ if (error instanceof Error) return error.message;
50
+ if (typeof error === "object" && error !== null && "message" in error) {
51
+ return String(error.message);
52
+ }
53
+ return typeof error === "string" ? error : "";
54
+ }
55
+ function failedConstraint(error) {
56
+ return typeof error === "object" && error !== null && "constraint" in error ? String(error.constraint ?? "") : "";
57
+ }
58
+ function countDevices(devices, kind) {
59
+ if (!devices || devices.length === 0) return null;
60
+ return devices.filter((device) => device.kind === kind).length;
61
+ }
62
+ function requestedNoun(context) {
63
+ if (context.camera && context.microphone) return "camera or microphone";
64
+ if (context.camera) return "camera";
65
+ if (context.microphone) return "microphone";
66
+ return "capture device";
67
+ }
68
+ function describeDisplayError(name, message) {
69
+ if (name === "NotAllowedError" || name === "AbortError") {
70
+ return /system/i.test(message) ? "Your system is blocking screen recording. Allow it in your privacy settings, then try again." : "Screen sharing was cancelled. Click Start preview to choose a screen, window, or tab.";
71
+ }
72
+ if (name === "NotFoundError" || name === "NotReadableError") {
73
+ return "The screen could not be captured. Try choosing a different screen, window, or tab.";
74
+ }
75
+ return null;
76
+ }
77
+ function describeNotFound(context) {
78
+ const cameras = countDevices(context.devices, "videoinput");
79
+ const microphones = countDevices(context.devices, "audioinput");
80
+ const noCamera = context.camera && cameras === 0;
81
+ const noMicrophone = context.microphone && microphones === 0;
82
+ if (noCamera && noMicrophone) {
83
+ return `No camera or microphone was found. Connect them, then try again. If they are connected, check that they are switched on and that ${PRIVACY_HINT}.`;
84
+ }
85
+ if (noCamera) {
86
+ const fallback = context.microphone ? "Connect a camera, or turn off Camera to record audio only." : "Connect a camera, then try again.";
87
+ return `No camera was found. ${fallback} If a camera is connected, check that it is switched on and that ${PRIVACY_HINT}.`;
88
+ }
89
+ if (noMicrophone) {
90
+ return `No microphone was found. Connect a microphone, then try again. If one is connected, check that it is switched on and that ${PRIVACY_HINT}.`;
91
+ }
92
+ if (context.camera && context.cameraDeviceId) {
93
+ return "The selected camera is not connected. Choose another camera, or System default.";
94
+ }
95
+ if (context.microphone && context.microphoneDeviceId) {
96
+ return "The selected microphone is not connected. Choose another microphone, or System default.";
97
+ }
98
+ const noun = requestedNoun(context);
99
+ return `The ${noun} could not be found. Check that it is connected and switched on, and that ${PRIVACY_HINT}.`;
100
+ }
101
+ function describeOverconstrained(error, context) {
102
+ const constraint = failedConstraint(error);
103
+ if (constraint === "deviceId" || constraint === "groupId") {
104
+ if (context.camera && context.cameraDeviceId) {
105
+ return "The selected camera is not connected. Choose another camera, or System default.";
106
+ }
107
+ if (context.microphone && context.microphoneDeviceId) {
108
+ return "The selected microphone is not connected. Choose another microphone, or System default.";
109
+ }
110
+ }
111
+ const noun = requestedNoun(context);
112
+ const setting = constraint ? ` (${constraint})` : "";
113
+ return `Your ${noun} cannot provide the requested settings${setting}. Clear that value in Advanced device settings, or set Constraint strength to Preferred.`;
114
+ }
115
+ function describeCaptureError(error, context) {
116
+ const name = errorName(error);
117
+ const message = errorMessage(error);
118
+ if (typeof error === "object" && error !== null && displayCaptureErrors.has(error)) {
119
+ const display = describeDisplayError(name, message);
120
+ if (display) return display;
121
+ }
122
+ if (name === "NotFoundError" || /requested device not found/i.test(message)) {
123
+ return describeNotFound(context);
124
+ }
125
+ if (name === "OverconstrainedError") {
126
+ return describeOverconstrained(error, context);
127
+ }
128
+ if (name === "NotAllowedError" || name === "SecurityError") {
129
+ const noun = requestedNoun(context);
130
+ return /system/i.test(message) ? `Your system's privacy settings are blocking the ${noun}. Allow access for this app, then try again.` : `Access to the ${noun} was blocked. Allow it when prompted, or in your browser or system settings, then try again.`;
131
+ }
132
+ if (name === "NotReadableError" || /could not start (video|audio) source/i.test(message)) {
133
+ const noun = /video source/i.test(message) ? "camera" : /audio source/i.test(message) ? "microphone" : requestedNoun(context);
134
+ return `The ${noun} could not start. Another app may be using it: close apps that use the ${noun}, then try again.`;
135
+ }
136
+ if (/getUserMedia is not available/i.test(message)) {
137
+ return "Cameras and microphones are not available here. Recording needs a browser or app that can access them.";
138
+ }
139
+ if (/getDisplayMedia is not available/i.test(message)) {
140
+ return "Screen capture is not available here. Recording the screen needs a browser or app that supports it.";
141
+ }
142
+ return message || "Recording could not start.";
143
+ }
144
+
36
145
  // src/recorder/sources/screenRegion.ts
37
146
  function validateScreenCaptureRegion(region, width, height) {
38
147
  const { x, y, width: w, height: h } = region;
@@ -186,6 +295,8 @@ async function requestFullScreenStream(options) {
186
295
  const displayStream = await navigator.mediaDevices.getDisplayMedia({
187
296
  video,
188
297
  audio: systemAudio ? options?.systemAudioConstraints ?? true : false
298
+ }).catch((err) => {
299
+ throw markDisplayCaptureError(err);
189
300
  });
190
301
  if (!includeMic) {
191
302
  return {
@@ -249,6 +360,8 @@ async function requestSystemAudioStream(audioConstraints) {
249
360
  const display = await navigator.mediaDevices.getDisplayMedia({
250
361
  video: true,
251
362
  audio: audioConstraints ?? true
363
+ }).catch((err) => {
364
+ throw markDisplayCaptureError(err);
252
365
  });
253
366
  display.getVideoTracks().forEach((t) => t.stop());
254
367
  const audio = display.getAudioTracks();
@@ -2672,20 +2785,6 @@ function captureSummary(micOn, cameraOn, screenOn, systemAudioOn) {
2672
2785
  }
2673
2786
  return "Pick at least one source to record.";
2674
2787
  }
2675
- function recordingFilenameSeed(source, stream, audioRequested) {
2676
- if (source === "mic") return "audio";
2677
- const hasAudio = stream ? stream.getAudioTracks().length > 0 : audioRequested;
2678
- if (source === "camera") return hasAudio ? "camera-audio" : "camera";
2679
- return hasAudio ? "screen-audio" : "screen";
2680
- }
2681
- function dualFilenameSeeds(screenStream, cameraStream, systemAudioRequested, micRequested) {
2682
- const screenHasAudio = screenStream ? screenStream.getAudioTracks().length > 0 : systemAudioRequested;
2683
- const cameraHasAudio = cameraStream ? cameraStream.getAudioTracks().length > 0 : micRequested;
2684
- return {
2685
- screen: screenHasAudio ? "screen-audio" : "screen",
2686
- camera: cameraHasAudio ? "camera-audio" : "camera"
2687
- };
2688
- }
2689
2788
  function RecorderModal({
2690
2789
  mediaProvider,
2691
2790
  container = null,
@@ -3370,6 +3469,14 @@ function RecorderModal({
3370
3469
  const deviceSettingsLocked = narrationRecording ? !narrationRecorderIdle : togglesLocked;
3371
3470
  const microphoneEnabled = narrationRecording ? true : micOn;
3372
3471
  const cameraEnabled = narrationRecording ? stage.recorder.withCamera : cameraOn;
3472
+ const captureErrorContext = {
3473
+ camera: cameraEnabled,
3474
+ microphone: microphoneEnabled,
3475
+ cameraDeviceId: deviceSettings.camera.deviceId,
3476
+ microphoneDeviceId: deviceSettings.audio.deviceId,
3477
+ devices: mediaDevices
3478
+ };
3479
+ const narrationError = stage.recorder.error ?? stage.controller.mic.error;
3373
3480
  const narrationToggleFor = (key) => {
3374
3481
  switch (key) {
3375
3482
  case "mic":
@@ -3539,7 +3646,7 @@ function RecorderModal({
3539
3646
  cameraStream: activeCameraStream
3540
3647
  }
3541
3648
  ),
3542
- !narrationRecording && recorder.error && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: recorder.error.message }),
3649
+ !narrationRecording && recorder.error && /* @__PURE__ */ jsx5("div", { role: "alert", style: errorStyle, "data-testid": "recorder-error", children: describeCaptureError(recorder.error, captureErrorContext) }),
3543
3650
  !narrationRecording && saveError && /* @__PURE__ */ jsxs5("div", { role: "alert", style: errorStyle, children: [
3544
3651
  saveError,
3545
3652
  /* @__PURE__ */ jsx5("p", { style: { marginBottom: 0 }, children: "Your recording is still available below. Download a copy before closing, or try saving again." })
@@ -3559,7 +3666,7 @@ function RecorderModal({
3559
3666
  "total. Final encoding may add more data."
3560
3667
  ] }),
3561
3668
  !narrationRecording && recorder.limitReached && /* @__PURE__ */ jsx5("p", { role: "status", style: recordingStatusStyle, children: "Recording stopped at the size limit. Your complete recording is retained; download a copy or save it to the document." }),
3562
- narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
3669
+ narrationOn && narrationError && /* @__PURE__ */ jsx5("div", { role: "alert", style: errorStyle, "data-testid": "recorder-narration-error", children: describeCaptureError(narrationError, captureErrorContext) }),
3563
3670
  narrationRecording && narrationCameraStream && /* @__PURE__ */ jsx5("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx5(
3564
3671
  "video",
3565
3672
  {
@@ -70,6 +70,20 @@ function buildFilename(kind, extension, basename, seed) {
70
70
  const prefix = seed ?? (kind === "audio" ? "narration" : "recording");
71
71
  return `${prefix}-${stamp}${extension}`;
72
72
  }
73
+ function recordingFilenameSeed(source, stream, audioRequested) {
74
+ if (source === "mic") return "audio";
75
+ const hasAudio = stream ? stream.getAudioTracks().length > 0 : audioRequested;
76
+ if (source === "camera") return hasAudio ? "camera-audio" : "camera";
77
+ return hasAudio ? "screen-audio" : "screen";
78
+ }
79
+ function dualFilenameSeeds(screenStream, cameraStream, systemAudioRequested, micRequested) {
80
+ const screenHasAudio = screenStream ? screenStream.getAudioTracks().length > 0 : systemAudioRequested;
81
+ const cameraHasAudio = cameraStream ? cameraStream.getAudioTracks().length > 0 : micRequested;
82
+ return {
83
+ screen: screenHasAudio ? "screen-audio" : "screen",
84
+ camera: cameraHasAudio ? "camera-audio" : "camera"
85
+ };
86
+ }
73
87
 
74
88
  // src/recorder/sources/micStream.ts
75
89
  async function requestMicStream(constraints) {
@@ -89,5 +103,7 @@ export {
89
103
  supportsDisplayMedia,
90
104
  supportsSystemAudioCapture,
91
105
  buildFilename,
106
+ recordingFilenameSeed,
107
+ dualFilenameSeeds,
92
108
  requestMicStream
93
109
  };
@@ -1,6 +1,6 @@
1
1
  // src/speech/index.ts
2
2
  async function startDictationSession(options) {
3
- const session = await import("./dictationSession-AVCXB3JR.js");
3
+ const session = await import("./dictationSession-WEXEY5NS.js");
4
4
  if (options.signal.aborted) throw new DOMException("Aborted", "AbortError");
5
5
  return session.startDictationSession(options);
6
6
  }
@@ -10,7 +10,7 @@ import {
10
10
  requestMicStream,
11
11
  resolveFormat,
12
12
  supportsMediaRecorder
13
- } from "./chunk-DNJ6HOES.js";
13
+ } from "./chunk-QXHX7N4V.js";
14
14
 
15
15
  // src/speech/microphoneSpeechActivity.ts
16
16
  var SAMPLE_INTERVAL_MS = 50;