@bendyline/squisq-editor-react 2.7.0 → 2.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -718,8 +718,62 @@ function useMediaRecorder(options = {}) {
718
718
  };
719
719
  }
720
720
 
721
+ // src/recorder/mediaDeviceList.ts
722
+ var ALIAS_DEVICE_IDS = /* @__PURE__ */ new Set(["default", "communications"]);
723
+ function recorderDeviceOptions(devices, kind, noun, selectedDeviceId = "") {
724
+ const options = [];
725
+ for (const device of devices) {
726
+ if (device.kind !== kind) continue;
727
+ const isSelected = selectedDeviceId !== "" && device.deviceId === selectedDeviceId;
728
+ if (!isSelected && (!device.deviceId || ALIAS_DEVICE_IDS.has(device.deviceId))) continue;
729
+ options.push({
730
+ deviceId: device.deviceId,
731
+ label: device.label || `${noun} ${options.length + 1}`,
732
+ groupId: device.groupId || ""
733
+ });
734
+ }
735
+ return options;
736
+ }
737
+ function hasRecorderDeviceChoice(options) {
738
+ return options.length > 1;
739
+ }
740
+ function recorderDeviceGroups(options) {
741
+ const groups = /* @__PURE__ */ new Map();
742
+ for (const [index, option] of options.entries()) {
743
+ if (!option.groupId || groups.has(option.groupId)) continue;
744
+ groups.set(option.groupId, option.label || `Device group ${index + 1}`);
745
+ }
746
+ return [...groups].map(([id, label]) => ({ id, label }));
747
+ }
748
+
749
+ // src/recorder/hooks/useMediaDevices.ts
750
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
751
+ function useMediaDevices() {
752
+ const [devices, setDevices] = useState2([]);
753
+ const [supported, setSupported] = useState2(null);
754
+ const refresh = useCallback2(() => {
755
+ const mediaDevices = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
756
+ if (!mediaDevices) return;
757
+ if (typeof mediaDevices.getSupportedConstraints === "function") {
758
+ setSupported(mediaDevices.getSupportedConstraints());
759
+ }
760
+ if (typeof mediaDevices.enumerateDevices !== "function") return;
761
+ void mediaDevices.enumerateDevices().then(setDevices).catch(() => {
762
+ });
763
+ }, []);
764
+ useEffect2(() => {
765
+ refresh();
766
+ const mediaDevices = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
767
+ if (!mediaDevices?.addEventListener) return;
768
+ const handleDeviceChange = () => refresh();
769
+ mediaDevices.addEventListener("devicechange", handleDeviceChange);
770
+ return () => mediaDevices.removeEventListener("devicechange", handleDeviceChange);
771
+ }, [refresh]);
772
+ return { devices, supported, refresh };
773
+ }
774
+
721
775
  // src/recorder/RecorderDeviceSettingsPanel.tsx
722
- import { useCallback as useCallback2, useEffect as useEffect2, useId, useState as useState2 } from "react";
776
+ import { useEffect as useEffect3, useId } from "react";
723
777
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
724
778
  var detailsStyle = {
725
779
  border: "1px solid var(--squisq-recorder-border)",
@@ -809,17 +863,6 @@ function triState(value) {
809
863
  function booleanFromTriState(value) {
810
864
  return value === "default" ? void 0 : value === "true";
811
865
  }
812
- function deviceLabel(device, index, noun) {
813
- return device.label || `${noun} ${index + 1}`;
814
- }
815
- function deviceGroups(devices) {
816
- const groups = /* @__PURE__ */ new Map();
817
- for (const [index, device] of devices.entries()) {
818
- if (!device.groupId || groups.has(device.groupId)) continue;
819
- groups.set(device.groupId, device.label || `Device group ${index + 1}`);
820
- }
821
- return [...groups].map(([id, label]) => ({ id, label }));
822
- }
823
866
  function readSettings(track) {
824
867
  if (!track || typeof track.getSettings !== "function") return null;
825
868
  try {
@@ -888,41 +931,29 @@ function RecorderDeviceSettingsPanel({
888
931
  primaryStream = null,
889
932
  cameraStream = null
890
933
  }) {
891
- const [devices, setDevices] = useState2([]);
892
- const [supported, setSupported] = useState2(null);
893
934
  const audioMimeListId = useId();
894
935
  const videoMimeListId = useId();
895
- const refreshBrowserInfo = useCallback2(async () => {
896
- const mediaDevices = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
897
- if (!mediaDevices) return;
898
- if (typeof mediaDevices.getSupportedConstraints === "function") {
899
- setSupported(mediaDevices.getSupportedConstraints());
900
- }
901
- if (typeof mediaDevices.enumerateDevices === "function") {
902
- try {
903
- setDevices(await mediaDevices.enumerateDevices());
904
- } catch {
905
- }
906
- }
907
- }, []);
908
- useEffect2(() => {
909
- void refreshBrowserInfo();
910
- const mediaDevices = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
911
- if (!mediaDevices?.addEventListener) return;
912
- const handleDeviceChange = () => void refreshBrowserInfo();
913
- mediaDevices.addEventListener("devicechange", handleDeviceChange);
914
- return () => mediaDevices.removeEventListener("devicechange", handleDeviceChange);
915
- }, [refreshBrowserInfo]);
916
- useEffect2(() => {
917
- if (primaryStream || cameraStream) void refreshBrowserInfo();
918
- }, [cameraStream, primaryStream, refreshBrowserInfo]);
936
+ const { devices, supported, refresh } = useMediaDevices();
937
+ useEffect3(() => {
938
+ if (primaryStream || cameraStream) refresh();
939
+ }, [cameraStream, primaryStream, refresh]);
919
940
  const supports = (key) => supported === null || supported[key] === true;
920
941
  const hasAudioTrack = microphoneEnabled || systemAudioEnabled;
921
942
  const hasStandaloneAudioRecorder = separateAudioRecorder || microphoneEnabled && !cameraEnabled && !screenEnabled;
922
- const audioDevices = devices.filter((device) => device.kind === "audioinput");
923
- const videoDevices = devices.filter((device) => device.kind === "videoinput");
924
- const audioGroups = deviceGroups(audioDevices);
925
- const videoGroups = deviceGroups(videoDevices);
943
+ const audioOptions = recorderDeviceOptions(
944
+ devices,
945
+ "audioinput",
946
+ "Microphone",
947
+ value.audio.deviceId
948
+ );
949
+ const videoOptions = recorderDeviceOptions(
950
+ devices,
951
+ "videoinput",
952
+ "Camera",
953
+ value.camera.deviceId
954
+ );
955
+ const audioGroups = recorderDeviceGroups(audioOptions);
956
+ const videoGroups = recorderDeviceGroups(videoOptions);
926
957
  const patchAudio = (patch) => onChange({ ...value, audio: { ...value.audio, ...patch } });
927
958
  const patchCamera = (patch) => onChange({ ...value, camera: { ...value.camera, ...patch } });
928
959
  const patchScreen = (patch) => onChange({ ...value, screen: { ...value.screen, ...patch } });
@@ -972,7 +1003,7 @@ function RecorderDeviceSettingsPanel({
972
1003
  onChange: (event) => patchAudio({ deviceId: event.target.value }),
973
1004
  children: [
974
1005
  /* @__PURE__ */ jsx("option", { value: "", children: "System default" }),
975
- audioDevices.map((device, index) => /* @__PURE__ */ jsx("option", { value: device.deviceId, children: deviceLabel(device, index, "Microphone") }, device.deviceId))
1006
+ audioOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.deviceId, children: option.label }, option.deviceId))
976
1007
  ]
977
1008
  }
978
1009
  )
@@ -1072,7 +1103,7 @@ function RecorderDeviceSettingsPanel({
1072
1103
  onChange: (event) => patchCamera({ deviceId: event.target.value }),
1073
1104
  children: [
1074
1105
  /* @__PURE__ */ jsx("option", { value: "", children: "System default" }),
1075
- videoDevices.map((device, index) => /* @__PURE__ */ jsx("option", { value: device.deviceId, children: deviceLabel(device, index, "Camera") }, device.deviceId))
1106
+ videoOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.deviceId, children: option.label }, option.deviceId))
1076
1107
  ]
1077
1108
  }
1078
1109
  )
@@ -1422,6 +1453,99 @@ function RecorderDeviceSettingsPanel({
1422
1453
  ] });
1423
1454
  }
1424
1455
 
1456
+ // src/recorder/RecorderDeviceQuickPicks.tsx
1457
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1458
+ var rowStyle = {
1459
+ display: "grid",
1460
+ gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
1461
+ gap: "8px 10px",
1462
+ marginBottom: 12
1463
+ };
1464
+ var fieldStyle2 = {
1465
+ display: "flex",
1466
+ flexDirection: "column",
1467
+ gap: 3,
1468
+ minWidth: 0,
1469
+ fontSize: 12,
1470
+ color: "var(--squisq-recorder-muted)"
1471
+ };
1472
+ var selectStyle = {
1473
+ width: "100%",
1474
+ minWidth: 0,
1475
+ boxSizing: "border-box",
1476
+ padding: "5px 6px",
1477
+ font: "inherit",
1478
+ color: "var(--squisq-recorder-text)",
1479
+ background: "var(--squisq-recorder-input)",
1480
+ border: "1px solid var(--squisq-recorder-border)"
1481
+ };
1482
+ function DevicePick({
1483
+ label,
1484
+ testId,
1485
+ options,
1486
+ value,
1487
+ onChange,
1488
+ disabled
1489
+ }) {
1490
+ return /* @__PURE__ */ jsxs2("label", { style: fieldStyle2, children: [
1491
+ label,
1492
+ /* @__PURE__ */ jsxs2(
1493
+ "select",
1494
+ {
1495
+ "data-testid": testId,
1496
+ value,
1497
+ disabled,
1498
+ style: selectStyle,
1499
+ onChange: (event) => onChange(event.target.value),
1500
+ children: [
1501
+ /* @__PURE__ */ jsx2("option", { value: "", children: "System default" }),
1502
+ options.map((option) => /* @__PURE__ */ jsx2("option", { value: option.deviceId, children: option.label }, option.deviceId))
1503
+ ]
1504
+ }
1505
+ )
1506
+ ] });
1507
+ }
1508
+ function RecorderDeviceQuickPicks({
1509
+ devices,
1510
+ microphoneEnabled,
1511
+ cameraEnabled,
1512
+ audioDeviceId,
1513
+ cameraDeviceId,
1514
+ onAudioDeviceChange,
1515
+ onCameraDeviceChange,
1516
+ disabled = false
1517
+ }) {
1518
+ const audioOptions = recorderDeviceOptions(devices, "audioinput", "Microphone", audioDeviceId);
1519
+ const videoOptions = recorderDeviceOptions(devices, "videoinput", "Camera", cameraDeviceId);
1520
+ const showMicrophone = microphoneEnabled && hasRecorderDeviceChoice(audioOptions);
1521
+ const showCamera = cameraEnabled && hasRecorderDeviceChoice(videoOptions);
1522
+ if (!showMicrophone && !showCamera) return null;
1523
+ return /* @__PURE__ */ jsxs2("div", { style: rowStyle, "data-testid": "recorder-device-quick-picks", children: [
1524
+ showMicrophone && /* @__PURE__ */ jsx2(
1525
+ DevicePick,
1526
+ {
1527
+ label: "Microphone",
1528
+ testId: "recorder-quick-pick-microphone",
1529
+ options: audioOptions,
1530
+ value: audioDeviceId,
1531
+ onChange: onAudioDeviceChange,
1532
+ disabled
1533
+ }
1534
+ ),
1535
+ showCamera && /* @__PURE__ */ jsx2(
1536
+ DevicePick,
1537
+ {
1538
+ label: "Camera",
1539
+ testId: "recorder-quick-pick-camera",
1540
+ options: videoOptions,
1541
+ value: cameraDeviceId,
1542
+ onChange: onCameraDeviceChange,
1543
+ disabled
1544
+ }
1545
+ )
1546
+ ] });
1547
+ }
1548
+
1425
1549
  // src/recorder/deviceSettings.ts
1426
1550
  var DEFAULT_RECORDER_DEVICE_SETTINGS = {
1427
1551
  constraintMode: "ideal",
@@ -1532,7 +1656,7 @@ function recordedMediaKind(source, stream, mimeType) {
1532
1656
  import {
1533
1657
  Fragment as Fragment2,
1534
1658
  useCallback as useCallback3,
1535
- useEffect as useEffect3,
1659
+ useEffect as useEffect4,
1536
1660
  useId as useId2,
1537
1661
  useMemo,
1538
1662
  useRef as useRef2,
@@ -1565,7 +1689,7 @@ function narrationCaptureSummary(withCamera) {
1565
1689
  }
1566
1690
 
1567
1691
  // src/recorder/RecorderModal.tsx
1568
- import { Fragment as Fragment3, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1692
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1569
1693
  var overlayStyle = {
1570
1694
  position: "fixed",
1571
1695
  inset: 0,
@@ -2019,7 +2143,7 @@ function RecorderModal({
2019
2143
  [narrationOn]
2020
2144
  );
2021
2145
  const narrationMicDeviceId = stage.controller.prefs.micDeviceId ?? "";
2022
- useEffect3(() => {
2146
+ useEffect4(() => {
2023
2147
  if (!narrationOn) return;
2024
2148
  setDeviceSettings(
2025
2149
  (current) => current.audio.deviceId === narrationMicDeviceId ? current : {
@@ -2029,7 +2153,7 @@ function RecorderModal({
2029
2153
  );
2030
2154
  }, [narrationMicDeviceId, narrationOn]);
2031
2155
  const prevNarrationOnRef = useRef2(narrationOn);
2032
- useEffect3(() => {
2156
+ useEffect4(() => {
2033
2157
  const was = prevNarrationOnRef.current;
2034
2158
  prevNarrationOnRef.current = narrationOn;
2035
2159
  if (was === narrationOn) return;
@@ -2044,7 +2168,7 @@ function RecorderModal({
2044
2168
  if (s.float.isOpen) s.float.close();
2045
2169
  }
2046
2170
  }, [narrationOn, recorder]);
2047
- useEffect3(() => {
2171
+ useEffect4(() => {
2048
2172
  if (!narrationOn) return;
2049
2173
  const onKey = (event) => {
2050
2174
  if (event.key !== "Escape") return;
@@ -2057,7 +2181,7 @@ function RecorderModal({
2057
2181
  const narrationMicLive = stage.controller.mic.status === "live";
2058
2182
  const narrationPreviewWanted = narrationOn && stage.recorder.withCamera && narrationMicLive && stage.recorder.cameraStream === null && stage.recorder.state !== "processing" && stage.recorder.state !== "review" && stage.recorder.state !== "saving";
2059
2183
  const narrationPreviewRef = useRef2(null);
2060
- useEffect3(() => {
2184
+ useEffect4(() => {
2061
2185
  const existing = narrationPreviewRef.current;
2062
2186
  if (existing) {
2063
2187
  for (const track of existing.getTracks()) track.stop();
@@ -2105,6 +2229,12 @@ function RecorderModal({
2105
2229
  void stageRef.current.recorder.start();
2106
2230
  }, []);
2107
2231
  const narrationCameraStream = narrationOn ? stage.recorder.cameraStream ?? narrationPreview : null;
2232
+ const activePrimaryStream = narrationOn ? stage.controller.mic.stream : recorder.stream;
2233
+ const activeCameraStream = narrationOn ? narrationCameraStream : recorder.camera?.stream ?? null;
2234
+ const { devices: mediaDevices, refresh: refreshMediaDevices } = useMediaDevices();
2235
+ useEffect4(() => {
2236
+ if (activePrimaryStream || activeCameraStream) refreshMediaDevices();
2237
+ }, [activeCameraStream, activePrimaryStream, refreshMediaDevices]);
2108
2238
  useStreamPreview(
2109
2239
  previewRef,
2110
2240
  narrationOn ? narrationCameraStream : recorder.state === "stopped" ? null : recorder.stream
@@ -2113,7 +2243,7 @@ function RecorderModal({
2113
2243
  cameraPreviewRef,
2114
2244
  !narrationOn && isDual && recorder.state !== "stopped" ? recorder.camera?.stream ?? null : null
2115
2245
  );
2116
- useEffect3(() => {
2246
+ useEffect4(() => {
2117
2247
  setPlaybackPositionMs(0);
2118
2248
  if (!recorder.blob) {
2119
2249
  setPlaybackUrl(null);
@@ -2126,7 +2256,7 @@ function RecorderModal({
2126
2256
  };
2127
2257
  }, [recorder.blob]);
2128
2258
  const cameraBlob = recorder.camera?.blob ?? null;
2129
- useEffect3(() => {
2259
+ useEffect4(() => {
2130
2260
  if (!cameraBlob) {
2131
2261
  setCameraPlaybackUrl(null);
2132
2262
  return;
@@ -2140,7 +2270,7 @@ function RecorderModal({
2140
2270
  const captureKey = `${source}:${cameraOn ? micOn : ""}:${includeSystemAudio}:${JSON.stringify(deviceSettings)}`;
2141
2271
  const previousKeyRef = useRef2(captureKey);
2142
2272
  const dualSaveProgressRef = useRef2({});
2143
- useEffect3(() => {
2273
+ useEffect4(() => {
2144
2274
  if (previousKeyRef.current !== captureKey) {
2145
2275
  previousKeyRef.current = captureKey;
2146
2276
  dualSaveProgressRef.current = {};
@@ -2375,6 +2505,8 @@ function RecorderModal({
2375
2505
  stage.recorder.state
2376
2506
  );
2377
2507
  const deviceSettingsLocked = narrationOn ? !narrationRecorderIdle : togglesLocked;
2508
+ const microphoneEnabled = narrationOn ? true : micOn;
2509
+ const cameraEnabled = narrationOn ? stage.recorder.withCamera : cameraOn;
2378
2510
  const narrationToggleFor = (key) => {
2379
2511
  switch (key) {
2380
2512
  case "mic":
@@ -2410,14 +2542,14 @@ function RecorderModal({
2410
2542
  };
2411
2543
  }
2412
2544
  };
2413
- return /* @__PURE__ */ jsx2(
2545
+ return /* @__PURE__ */ jsx3(
2414
2546
  "div",
2415
2547
  {
2416
2548
  ref: overlayRef,
2417
2549
  className: "squisq-editor-shell squisq-recorder-overlay",
2418
2550
  "data-theme": colorScheme,
2419
2551
  style: { ...overlayStyle, ...recorderThemeStyle(colorScheme) },
2420
- children: /* @__PURE__ */ jsxs2(
2552
+ children: /* @__PURE__ */ jsxs3(
2421
2553
  "div",
2422
2554
  {
2423
2555
  ref: dialogRef,
@@ -2434,15 +2566,15 @@ function RecorderModal({
2434
2566
  "aria-labelledby": headingId,
2435
2567
  tabIndex: -1,
2436
2568
  children: [
2437
- /* @__PURE__ */ jsx2("h2", { id: headingId, style: titleStyle, children: "Record media" }),
2438
- /* @__PURE__ */ jsxs2("div", { style: narrationOn ? bodyRowStyle : void 0, children: [
2439
- /* @__PURE__ */ jsxs2("div", { style: narrationOn ? leftColStyle : void 0, children: [
2440
- /* @__PURE__ */ jsx2("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs2(Fragment2, { children: [
2441
- groupIndex > 0 && /* @__PURE__ */ jsx2("div", { "aria-hidden": "true", style: groupDividerStyle }),
2442
- /* @__PURE__ */ jsx2("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
2569
+ /* @__PURE__ */ jsx3("h2", { id: headingId, style: titleStyle, children: "Record media" }),
2570
+ /* @__PURE__ */ jsxs3("div", { style: narrationOn ? bodyRowStyle : void 0, children: [
2571
+ /* @__PURE__ */ jsxs3("div", { style: narrationOn ? leftColStyle : void 0, children: [
2572
+ /* @__PURE__ */ jsx3("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs3(Fragment2, { children: [
2573
+ groupIndex > 0 && /* @__PURE__ */ jsx3("div", { "aria-hidden": "true", style: groupDividerStyle }),
2574
+ /* @__PURE__ */ jsx3("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
2443
2575
  if (t.key === "systemAudio" && !canIncludeSystemAudio) return null;
2444
2576
  const props = narrationOn ? narrationToggleFor(t.key) : simpleToggleProps(t.key);
2445
- return /* @__PURE__ */ jsx2(
2577
+ return /* @__PURE__ */ jsx3(
2446
2578
  "button",
2447
2579
  {
2448
2580
  type: "button",
@@ -2457,9 +2589,9 @@ function RecorderModal({
2457
2589
  );
2458
2590
  }) })
2459
2591
  ] }, group.label)) }),
2460
- /* @__PURE__ */ jsx2("p", { style: summaryStyle, children: narrationOn ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
2461
- narrationAvailable && /* @__PURE__ */ jsxs2("label", { style: checkboxRowStyle, children: [
2462
- /* @__PURE__ */ jsx2(
2592
+ /* @__PURE__ */ jsx3("p", { style: summaryStyle, children: narrationOn ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
2593
+ narrationAvailable && /* @__PURE__ */ jsxs3("label", { style: checkboxRowStyle, children: [
2594
+ /* @__PURE__ */ jsx3(
2463
2595
  "input",
2464
2596
  {
2465
2597
  type: "checkbox",
@@ -2472,25 +2604,44 @@ function RecorderModal({
2472
2604
  ),
2473
2605
  "Show narration mode"
2474
2606
  ] }),
2475
- /* @__PURE__ */ jsx2(
2607
+ /* @__PURE__ */ jsx3(
2608
+ RecorderDeviceQuickPicks,
2609
+ {
2610
+ devices: mediaDevices,
2611
+ microphoneEnabled,
2612
+ cameraEnabled,
2613
+ audioDeviceId: deviceSettings.audio.deviceId,
2614
+ cameraDeviceId: deviceSettings.camera.deviceId,
2615
+ onAudioDeviceChange: (deviceId) => handleDeviceSettingsChange({
2616
+ ...deviceSettings,
2617
+ audio: { ...deviceSettings.audio, deviceId }
2618
+ }),
2619
+ onCameraDeviceChange: (deviceId) => handleDeviceSettingsChange({
2620
+ ...deviceSettings,
2621
+ camera: { ...deviceSettings.camera, deviceId }
2622
+ }),
2623
+ disabled: deviceSettingsLocked
2624
+ }
2625
+ ),
2626
+ /* @__PURE__ */ jsx3(
2476
2627
  RecorderDeviceSettingsPanel,
2477
2628
  {
2478
2629
  value: deviceSettings,
2479
2630
  onChange: handleDeviceSettingsChange,
2480
2631
  disabled: deviceSettingsLocked,
2481
- microphoneEnabled: narrationOn ? true : micOn,
2482
- cameraEnabled: narrationOn ? stage.recorder.withCamera : cameraOn,
2632
+ microphoneEnabled,
2633
+ cameraEnabled,
2483
2634
  screenEnabled: narrationOn ? false : screenOn,
2484
2635
  systemAudioEnabled: !narrationOn && includeSystemAudio,
2485
2636
  separateAudioRecorder: narrationOn,
2486
- primaryStream: narrationOn ? stage.controller.mic.stream : recorder.stream,
2487
- cameraStream: narrationOn ? narrationCameraStream : recorder.camera?.stream ?? null
2637
+ primaryStream: activePrimaryStream,
2638
+ cameraStream: activeCameraStream
2488
2639
  }
2489
2640
  ),
2490
- !narrationOn && recorder.error && /* @__PURE__ */ jsx2("div", { style: errorStyle, children: recorder.error.message }),
2491
- !narrationOn && saveError && /* @__PURE__ */ jsx2("div", { style: errorStyle, children: saveError }),
2492
- narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx2("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
2493
- narrationOn && narrationCameraStream && /* @__PURE__ */ jsx2("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx2(
2641
+ !narrationOn && recorder.error && /* @__PURE__ */ jsx3("div", { style: errorStyle, children: recorder.error.message }),
2642
+ !narrationOn && saveError && /* @__PURE__ */ jsx3("div", { style: errorStyle, children: saveError }),
2643
+ narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx3("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
2644
+ narrationOn && narrationCameraStream && /* @__PURE__ */ jsx3("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx3(
2494
2645
  "video",
2495
2646
  {
2496
2647
  ref: previewRef,
@@ -2500,10 +2651,10 @@ function RecorderModal({
2500
2651
  style: { width: "100%", height: "100%", objectFit: "contain" }
2501
2652
  }
2502
2653
  ) }),
2503
- narrationOn && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx2("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
2504
- narrationOn && !narrationCameraStream && !narrationTakeDone && stage.recorder.withCamera && /* @__PURE__ */ jsx2("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx2("span", { children: narrationPreviewWanted ? "Camera starting\u2026" : "Click Start preview to turn on your camera." }) }),
2505
- narrationOn && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs2("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
2506
- /* @__PURE__ */ jsx2(
2654
+ narrationOn && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx3("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
2655
+ narrationOn && !narrationCameraStream && !narrationTakeDone && stage.recorder.withCamera && /* @__PURE__ */ jsx3("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx3("span", { children: narrationPreviewWanted ? "Camera starting\u2026" : "Click Start preview to turn on your camera." }) }),
2656
+ narrationOn && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs3("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
2657
+ /* @__PURE__ */ jsx3(
2507
2658
  "div",
2508
2659
  {
2509
2660
  "aria-hidden": "true",
@@ -2513,11 +2664,11 @@ function RecorderModal({
2513
2664
  }
2514
2665
  }
2515
2666
  ),
2516
- /* @__PURE__ */ jsx2("span", { style: { position: "relative" }, children: stage.recorder.state === "recording" ? "\u25CF Recording narration" : narrationMicLive ? stage.controller.voiceActive ? "Voice detected" : "Microphone ready" : "Click Start preview to check your mic." })
2667
+ /* @__PURE__ */ jsx3("span", { style: { position: "relative" }, children: stage.recorder.state === "recording" ? "\u25CF Recording narration" : narrationMicLive ? stage.controller.voiceActive ? "Voice detected" : "Microphone ready" : "Click Start preview to check your mic." })
2517
2668
  ] }),
2518
- !narrationOn && !showPreview && /* @__PURE__ */ jsx2("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx2("span", { children: "Click Start Preview to start a recording." }) }),
2519
- !narrationOn && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
2520
- /* @__PURE__ */ jsx2(
2669
+ !narrationOn && !showPreview && /* @__PURE__ */ jsx3("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx3("span", { children: "Click Start Preview to start a recording." }) }),
2670
+ !narrationOn && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs3("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
2671
+ /* @__PURE__ */ jsx3(
2521
2672
  "video",
2522
2673
  {
2523
2674
  ref: previewRef,
@@ -2527,7 +2678,7 @@ function RecorderModal({
2527
2678
  style: { width: "100%", height: "100%", objectFit: "contain" }
2528
2679
  }
2529
2680
  ),
2530
- isDual && /* @__PURE__ */ jsx2(
2681
+ isDual && /* @__PURE__ */ jsx3(
2531
2682
  "video",
2532
2683
  {
2533
2684
  ref: cameraPreviewRef,
@@ -2548,12 +2699,12 @@ function RecorderModal({
2548
2699
  }
2549
2700
  )
2550
2701
  ] }),
2551
- !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx2("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs2(Fragment3, { children: [
2702
+ !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx3("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
2552
2703
  "\u25CF Recording ",
2553
2704
  formatDurationMs(recorder.durationMs)
2554
- ] }) : /* @__PURE__ */ jsx2(Fragment3, { children: "Microphone ready" }) }),
2555
- !narrationOn && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
2556
- /* @__PURE__ */ jsx2(
2705
+ ] }) : /* @__PURE__ */ jsx3(Fragment3, { children: "Microphone ready" }) }),
2706
+ !narrationOn && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs3("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
2707
+ /* @__PURE__ */ jsx3(
2557
2708
  "video",
2558
2709
  {
2559
2710
  src: playbackUrl,
@@ -2565,7 +2716,7 @@ function RecorderModal({
2565
2716
  style: { width: "100%", height: "100%", objectFit: "contain" }
2566
2717
  }
2567
2718
  ),
2568
- /* @__PURE__ */ jsxs2(
2719
+ /* @__PURE__ */ jsxs3(
2569
2720
  "div",
2570
2721
  {
2571
2722
  role: "timer",
@@ -2579,9 +2730,9 @@ function RecorderModal({
2579
2730
  }
2580
2731
  )
2581
2732
  ] }),
2582
- !narrationOn && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 12 }, children: [
2583
- /* @__PURE__ */ jsx2("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
2584
- /* @__PURE__ */ jsx2(
2733
+ !narrationOn && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 12 }, children: [
2734
+ /* @__PURE__ */ jsx3("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
2735
+ /* @__PURE__ */ jsx3(
2585
2736
  "video",
2586
2737
  {
2587
2738
  src: cameraPlaybackUrl,
@@ -2597,16 +2748,16 @@ function RecorderModal({
2597
2748
  }
2598
2749
  )
2599
2750
  ] }),
2600
- !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 12 }, children: [
2601
- /* @__PURE__ */ jsxs2("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
2751
+ !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 12 }, children: [
2752
+ /* @__PURE__ */ jsxs3("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
2602
2753
  "\u2713 Recorded ",
2603
2754
  formatDurationMs(recorder.durationMs)
2604
2755
  ] }),
2605
- /* @__PURE__ */ jsx2("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
2756
+ /* @__PURE__ */ jsx3("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
2606
2757
  ] }),
2607
- !narrationOn && source === "mic" && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2608
- /* @__PURE__ */ jsx2("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
2609
- /* @__PURE__ */ jsx2(
2758
+ !narrationOn && source === "mic" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2759
+ /* @__PURE__ */ jsx3("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
2760
+ /* @__PURE__ */ jsx3(
2610
2761
  "textarea",
2611
2762
  {
2612
2763
  id: "recorder-source-text",
@@ -2618,8 +2769,8 @@ function RecorderModal({
2618
2769
  }
2619
2770
  )
2620
2771
  ] }),
2621
- /* @__PURE__ */ jsx2("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
2622
- /* @__PURE__ */ jsx2(
2772
+ /* @__PURE__ */ jsx3("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
2773
+ /* @__PURE__ */ jsx3(
2623
2774
  "input",
2624
2775
  {
2625
2776
  id: "recorder-basename",
@@ -2631,12 +2782,12 @@ function RecorderModal({
2631
2782
  disabled: narrationOn ? !narrationRecorderIdle : recorder.state === "recording"
2632
2783
  }
2633
2784
  ),
2634
- !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: recordingStatusStyle, children: [
2785
+ !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs3("div", { style: recordingStatusStyle, children: [
2635
2786
  "\u25CF Recording ",
2636
2787
  formatDurationMs(recorder.durationMs)
2637
2788
  ] }),
2638
- /* @__PURE__ */ jsxs2("div", { style: buttonRowStyle, children: [
2639
- /* @__PURE__ */ jsx2(
2789
+ /* @__PURE__ */ jsxs3("div", { style: buttonRowStyle, children: [
2790
+ /* @__PURE__ */ jsx3(
2640
2791
  "button",
2641
2792
  {
2642
2793
  type: "button",
@@ -2646,8 +2797,8 @@ function RecorderModal({
2646
2797
  children: "Close"
2647
2798
  }
2648
2799
  ),
2649
- narrationOn && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2650
- narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx2(
2800
+ narrationOn && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2801
+ narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx3(
2651
2802
  "button",
2652
2803
  {
2653
2804
  type: "button",
@@ -2657,14 +2808,14 @@ function RecorderModal({
2657
2808
  children: narrationRequesting ? "Requesting\u2026" : "Start preview"
2658
2809
  }
2659
2810
  ),
2660
- narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs2("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
2661
- /* @__PURE__ */ jsx2(
2811
+ narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs3("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
2812
+ /* @__PURE__ */ jsx3(
2662
2813
  "span",
2663
2814
  {
2664
2815
  className: "squisq-recorder-record-dot",
2665
2816
  style: recordDotFrameStyle,
2666
2817
  "aria-hidden": "true",
2667
- children: /* @__PURE__ */ jsx2(
2818
+ children: /* @__PURE__ */ jsx3(
2668
2819
  "span",
2669
2820
  {
2670
2821
  className: "squisq-recorder-record-dot-center",
@@ -2675,7 +2826,7 @@ function RecorderModal({
2675
2826
  ),
2676
2827
  "Record"
2677
2828
  ] }),
2678
- (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx2(
2829
+ (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx3(
2679
2830
  "button",
2680
2831
  {
2681
2832
  type: "button",
@@ -2684,11 +2835,11 @@ function RecorderModal({
2684
2835
  children: "Stop"
2685
2836
  }
2686
2837
  ),
2687
- stage.recorder.state === "processing" && /* @__PURE__ */ jsx2("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
2688
- stage.recorder.state === "saving" && /* @__PURE__ */ jsx2("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
2689
- stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2690
- /* @__PURE__ */ jsx2("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
2691
- /* @__PURE__ */ jsx2(
2838
+ stage.recorder.state === "processing" && /* @__PURE__ */ jsx3("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
2839
+ stage.recorder.state === "saving" && /* @__PURE__ */ jsx3("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
2840
+ stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2841
+ /* @__PURE__ */ jsx3("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
2842
+ /* @__PURE__ */ jsx3(
2692
2843
  "button",
2693
2844
  {
2694
2845
  type: "button",
@@ -2699,8 +2850,8 @@ function RecorderModal({
2699
2850
  )
2700
2851
  ] })
2701
2852
  ] }),
2702
- !narrationOn && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2703
- (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx2(
2853
+ !narrationOn && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2854
+ (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx3(
2704
2855
  "button",
2705
2856
  {
2706
2857
  type: "button",
@@ -2710,14 +2861,14 @@ function RecorderModal({
2710
2861
  children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
2711
2862
  }
2712
2863
  ),
2713
- canRecord && /* @__PURE__ */ jsxs2("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
2714
- /* @__PURE__ */ jsx2(
2864
+ canRecord && /* @__PURE__ */ jsxs3("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
2865
+ /* @__PURE__ */ jsx3(
2715
2866
  "span",
2716
2867
  {
2717
2868
  className: "squisq-recorder-record-dot",
2718
2869
  style: recordDotFrameStyle,
2719
2870
  "aria-hidden": "true",
2720
- children: /* @__PURE__ */ jsx2(
2871
+ children: /* @__PURE__ */ jsx3(
2721
2872
  "span",
2722
2873
  {
2723
2874
  className: "squisq-recorder-record-dot-center",
@@ -2728,9 +2879,9 @@ function RecorderModal({
2728
2879
  ),
2729
2880
  "Record"
2730
2881
  ] }),
2731
- canStop && /* @__PURE__ */ jsx2("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
2732
- canSave && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2733
- /* @__PURE__ */ jsx2(
2882
+ canStop && /* @__PURE__ */ jsx3("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
2883
+ canSave && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2884
+ /* @__PURE__ */ jsx3(
2734
2885
  "button",
2735
2886
  {
2736
2887
  type: "button",
@@ -2740,7 +2891,7 @@ function RecorderModal({
2740
2891
  children: "Discard & re-record"
2741
2892
  }
2742
2893
  ),
2743
- /* @__PURE__ */ jsx2(
2894
+ /* @__PURE__ */ jsx3(
2744
2895
  "button",
2745
2896
  {
2746
2897
  type: "button",
@@ -2754,7 +2905,7 @@ function RecorderModal({
2754
2905
  ] })
2755
2906
  ] })
2756
2907
  ] }),
2757
- narrationOn && narration && /* @__PURE__ */ jsx2("div", { style: rightColStyle, children: /* @__PURE__ */ jsx2(
2908
+ narrationOn && narration && /* @__PURE__ */ jsx3("div", { style: rightColStyle, children: /* @__PURE__ */ jsx3(
2758
2909
  NarrationStage,
2759
2910
  {
2760
2911
  stage,
@@ -2777,7 +2928,7 @@ function RecorderModal({
2777
2928
  // src/recorder/RecorderPanel.tsx
2778
2929
  import { useCallback as useCallback4, useState as useState4 } from "react";
2779
2930
  import { createPortal } from "react-dom";
2780
- import { Fragment as Fragment4, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2931
+ import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2781
2932
  function RecorderPanel({
2782
2933
  mediaProvider,
2783
2934
  container = null,
@@ -2801,8 +2952,8 @@ function RecorderPanel({
2801
2952
  [controlledOpen, onOpenChange]
2802
2953
  );
2803
2954
  const handleClose = useCallback4(() => setOpen(false), [setOpen]);
2804
- return /* @__PURE__ */ jsxs3(Fragment4, { children: [
2805
- showTrigger && /* @__PURE__ */ jsx3(
2955
+ return /* @__PURE__ */ jsxs4(Fragment4, { children: [
2956
+ showTrigger && /* @__PURE__ */ jsx4(
2806
2957
  "button",
2807
2958
  {
2808
2959
  type: "button",
@@ -2811,11 +2962,11 @@ function RecorderPanel({
2811
2962
  "aria-label": tooltip,
2812
2963
  "aria-expanded": open,
2813
2964
  onClick: () => setOpen(!open),
2814
- children: /* @__PURE__ */ jsx3(Icon, { icon: "fa-solid fa-microphone" })
2965
+ children: /* @__PURE__ */ jsx4(Icon, { icon: "fa-solid fa-microphone" })
2815
2966
  }
2816
2967
  ),
2817
2968
  open && typeof document !== "undefined" && createPortal(
2818
- /* @__PURE__ */ jsx3(
2969
+ /* @__PURE__ */ jsx4(
2819
2970
  RecorderModal,
2820
2971
  {
2821
2972
  mediaProvider,
@@ -2838,7 +2989,12 @@ export {
2838
2989
  requestScreenStream,
2839
2990
  getCaptureKind,
2840
2991
  useMediaRecorder,
2992
+ recorderDeviceOptions,
2993
+ hasRecorderDeviceChoice,
2994
+ recorderDeviceGroups,
2995
+ useMediaDevices,
2841
2996
  RecorderDeviceSettingsPanel,
2997
+ RecorderDeviceQuickPicks,
2842
2998
  DEFAULT_RECORDER_DEVICE_SETTINGS,
2843
2999
  buildRecorderAudioConstraints,
2844
3000
  buildRecorderCameraConstraints,