@bendyline/squisq-editor-react 2.4.4 → 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.
@@ -16,7 +16,7 @@ import {
16
16
  timingPathFor,
17
17
  useNarrationStage,
18
18
  useStreamPreview
19
- } from "./chunk-PDCKJCOS.js";
19
+ } from "./chunk-OWOKKZAU.js";
20
20
 
21
21
  // src/recorder/sources/screenStream.ts
22
22
  function mixAudioTracks(streams) {
@@ -43,7 +43,7 @@ async function requestScreenStream(options) {
43
43
  const includeMic = options?.includeMicrophone ?? false;
44
44
  const displayStream = await navigator.mediaDevices.getDisplayMedia({
45
45
  video,
46
- audio: systemAudio
46
+ audio: systemAudio ? options?.systemAudioConstraints ?? true : false
47
47
  });
48
48
  if (!includeMic) {
49
49
  return {
@@ -100,11 +100,14 @@ async function requestScreenStream(options) {
100
100
  import { useCallback, useEffect, useRef, useState } from "react";
101
101
 
102
102
  // src/recorder/sources/systemAudioStream.ts
103
- async function requestSystemAudioStream() {
103
+ async function requestSystemAudioStream(audioConstraints) {
104
104
  if (!supportsDisplayMedia()) {
105
105
  throw new Error("navigator.mediaDevices.getDisplayMedia is not available in this environment.");
106
106
  }
107
- const display = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
107
+ const display = await navigator.mediaDevices.getDisplayMedia({
108
+ video: true,
109
+ audio: audioConstraints ?? true
110
+ });
108
111
  display.getVideoTracks().forEach((t) => t.stop());
109
112
  const audio = display.getAudioTracks();
110
113
  if (audio.length === 0) return null;
@@ -152,7 +155,7 @@ async function acquireStream(source, opts) {
152
155
  case "mic": {
153
156
  const audio = typeof opts.audioConstraints === "object" ? opts.audioConstraints : void 0;
154
157
  if (opts.systemAudio) {
155
- const systemAudio = await requestSystemAudioStream();
158
+ const systemAudio = await requestSystemAudioStream(opts.screenAudioConstraints);
156
159
  let base;
157
160
  try {
158
161
  base = await requestMicStream(audio);
@@ -172,7 +175,7 @@ async function acquireStream(source, opts) {
172
175
  const video = opts.videoConstraints ?? true;
173
176
  const audio = opts.includeMicrophone === false ? false : opts.audioConstraints ?? true;
174
177
  if (opts.systemAudio) {
175
- const systemAudio = await requestSystemAudioStream();
178
+ const systemAudio = await requestSystemAudioStream(opts.screenAudioConstraints);
176
179
  let base;
177
180
  try {
178
181
  base = await requestCameraStream({ video, audio });
@@ -191,8 +194,9 @@ async function acquireStream(source, opts) {
191
194
  case "screen":
192
195
  case "screen+mic": {
193
196
  const handle = await requestScreenStream({
194
- video: opts.videoConstraints ?? true,
197
+ video: opts.screenVideoConstraints ?? opts.videoConstraints ?? true,
195
198
  systemAudio: opts.systemAudio ?? false,
199
+ systemAudioConstraints: opts.screenAudioConstraints,
196
200
  includeMicrophone: source === "screen+mic",
197
201
  microphoneConstraints: typeof opts.audioConstraints === "object" ? opts.audioConstraints : void 0
198
202
  });
@@ -202,8 +206,9 @@ async function acquireStream(source, opts) {
202
206
  }
203
207
  async function acquireDualStreams(opts, isStale) {
204
208
  const screen = await requestScreenStream({
205
- video: opts.videoConstraints ?? true,
209
+ video: opts.screenVideoConstraints ?? opts.videoConstraints ?? true,
206
210
  systemAudio: opts.systemAudio ?? false,
211
+ systemAudioConstraints: opts.screenAudioConstraints,
207
212
  includeMicrophone: false
208
213
  });
209
214
  const releaseScreen = () => {
@@ -217,7 +222,7 @@ async function acquireDualStreams(opts, isStale) {
217
222
  let camera;
218
223
  try {
219
224
  camera = await requestCameraStream({
220
- video: true,
225
+ video: opts.videoConstraints ?? true,
221
226
  audio: opts.includeMicrophone === false ? false : opts.audioConstraints ?? true
222
227
  });
223
228
  } catch (err) {
@@ -381,6 +386,20 @@ function useMediaRecorder(options = {}) {
381
386
  if (optionsRef.current.bitsPerSecond) {
382
387
  recorderOptions.bitsPerSecond = optionsRef.current.bitsPerSecond;
383
388
  }
389
+ if (optionsRef.current.audioBitsPerSecond) {
390
+ recorderOptions.audioBitsPerSecond = optionsRef.current.audioBitsPerSecond;
391
+ }
392
+ if (optionsRef.current.videoBitsPerSecond) {
393
+ recorderOptions.videoBitsPerSecond = optionsRef.current.videoBitsPerSecond;
394
+ }
395
+ if (optionsRef.current.audioBitrateMode) {
396
+ recorderOptions.audioBitrateMode = optionsRef.current.audioBitrateMode;
397
+ }
398
+ if (optionsRef.current.videoKeyFrameIntervalDuration) {
399
+ recorderOptions.videoKeyFrameIntervalDuration = optionsRef.current.videoKeyFrameIntervalDuration;
400
+ } else if (optionsRef.current.videoKeyFrameIntervalCount) {
401
+ recorderOptions.videoKeyFrameIntervalCount = optionsRef.current.videoKeyFrameIntervalCount;
402
+ }
384
403
  if (source === "screen+camera") {
385
404
  dual = await acquireDualStreams(
386
405
  optionsRef.current,
@@ -699,14 +718,818 @@ function useMediaRecorder(options = {}) {
699
718
  };
700
719
  }
701
720
 
721
+ // src/recorder/RecorderDeviceSettingsPanel.tsx
722
+ import { useCallback as useCallback2, useEffect as useEffect2, useId, useState as useState2 } from "react";
723
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
724
+ var detailsStyle = {
725
+ border: "1px solid var(--squisq-recorder-border)",
726
+ marginBottom: 12
727
+ };
728
+ var summaryToggleStyle = {
729
+ cursor: "pointer",
730
+ padding: "8px 10px",
731
+ fontSize: 13,
732
+ fontWeight: 600,
733
+ color: "var(--squisq-recorder-text)",
734
+ background: "var(--squisq-recorder-surface-muted)"
735
+ };
736
+ var panelStyle = {
737
+ padding: "4px 10px 10px"
738
+ };
739
+ var fieldsetStyle = {
740
+ border: 0,
741
+ borderTop: "1px solid var(--squisq-recorder-border)",
742
+ margin: "10px 0 0",
743
+ padding: "10px 0 0"
744
+ };
745
+ var legendStyle = {
746
+ padding: "0 6px 0 0",
747
+ fontSize: 12,
748
+ fontWeight: 700,
749
+ color: "var(--squisq-recorder-text)"
750
+ };
751
+ var gridStyle = {
752
+ display: "grid",
753
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
754
+ gap: "8px 10px"
755
+ };
756
+ var fieldStyle = {
757
+ display: "flex",
758
+ flexDirection: "column",
759
+ gap: 3,
760
+ minWidth: 0,
761
+ fontSize: 12,
762
+ color: "var(--squisq-recorder-muted)"
763
+ };
764
+ var inputStyle = {
765
+ width: "100%",
766
+ minWidth: 0,
767
+ boxSizing: "border-box",
768
+ padding: "5px 6px",
769
+ font: "inherit",
770
+ color: "var(--squisq-recorder-text)",
771
+ background: "var(--squisq-recorder-input)",
772
+ border: "1px solid var(--squisq-recorder-border)"
773
+ };
774
+ var noteStyle = {
775
+ margin: "8px 0 0",
776
+ fontSize: 11,
777
+ lineHeight: 1.4,
778
+ color: "var(--squisq-recorder-muted)"
779
+ };
780
+ var actualStyle = {
781
+ margin: "6px 0 0",
782
+ padding: 6,
783
+ overflowX: "auto",
784
+ fontSize: 10,
785
+ lineHeight: 1.35,
786
+ color: "var(--squisq-recorder-muted)",
787
+ background: "var(--squisq-recorder-surface-muted)"
788
+ };
789
+ var AUDIO_MIME_TYPES = [
790
+ "audio/webm;codecs=opus",
791
+ "audio/webm",
792
+ "audio/mp4",
793
+ "audio/ogg;codecs=opus"
794
+ ];
795
+ var VIDEO_MIME_TYPES = [
796
+ "video/webm;codecs=vp9,opus",
797
+ "video/webm;codecs=vp8,opus",
798
+ "video/webm",
799
+ "video/mp4"
800
+ ];
801
+ function numberFromInput(value) {
802
+ if (value.trim() === "") return void 0;
803
+ const number = Number(value);
804
+ return Number.isFinite(number) && number > 0 ? number : void 0;
805
+ }
806
+ function triState(value) {
807
+ return value === void 0 ? "default" : value ? "true" : "false";
808
+ }
809
+ function booleanFromTriState(value) {
810
+ return value === "default" ? void 0 : value === "true";
811
+ }
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
+ function readSettings(track) {
824
+ if (!track || typeof track.getSettings !== "function") return null;
825
+ try {
826
+ return track.getSettings();
827
+ } catch {
828
+ return null;
829
+ }
830
+ }
831
+ function NumericField({
832
+ label,
833
+ value,
834
+ onChange,
835
+ disabled,
836
+ min,
837
+ step
838
+ }) {
839
+ return /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
840
+ label,
841
+ /* @__PURE__ */ jsx(
842
+ "input",
843
+ {
844
+ type: "number",
845
+ min: min ?? 0,
846
+ step: step ?? 1,
847
+ value: value ?? "",
848
+ disabled,
849
+ style: inputStyle,
850
+ onChange: (event) => onChange(numberFromInput(event.target.value))
851
+ }
852
+ )
853
+ ] });
854
+ }
855
+ function BooleanField({
856
+ label,
857
+ value,
858
+ onChange,
859
+ disabled
860
+ }) {
861
+ return /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
862
+ label,
863
+ /* @__PURE__ */ jsxs(
864
+ "select",
865
+ {
866
+ value: triState(value),
867
+ disabled,
868
+ style: inputStyle,
869
+ onChange: (event) => onChange(booleanFromTriState(event.target.value)),
870
+ children: [
871
+ /* @__PURE__ */ jsx("option", { value: "default", children: "Browser default" }),
872
+ /* @__PURE__ */ jsx("option", { value: "true", children: "On" }),
873
+ /* @__PURE__ */ jsx("option", { value: "false", children: "Off" })
874
+ ]
875
+ }
876
+ )
877
+ ] });
878
+ }
879
+ function RecorderDeviceSettingsPanel({
880
+ value,
881
+ onChange,
882
+ disabled = false,
883
+ microphoneEnabled,
884
+ cameraEnabled,
885
+ screenEnabled,
886
+ systemAudioEnabled = false,
887
+ separateAudioRecorder = false,
888
+ primaryStream = null,
889
+ cameraStream = null
890
+ }) {
891
+ const [devices, setDevices] = useState2([]);
892
+ const [supported, setSupported] = useState2(null);
893
+ const audioMimeListId = useId();
894
+ 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]);
919
+ const supports = (key) => supported === null || supported[key] === true;
920
+ const hasAudioTrack = microphoneEnabled || systemAudioEnabled;
921
+ 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);
926
+ const patchAudio = (patch) => onChange({ ...value, audio: { ...value.audio, ...patch } });
927
+ const patchCamera = (patch) => onChange({ ...value, camera: { ...value.camera, ...patch } });
928
+ const patchScreen = (patch) => onChange({ ...value, screen: { ...value.screen, ...patch } });
929
+ const patchScreenAudio = (patch) => onChange({ ...value, screenAudio: { ...value.screenAudio, ...patch } });
930
+ const patchEncoding = (patch) => onChange({ ...value, encoding: { ...value.encoding, ...patch } });
931
+ const primaryAudioSettings = readSettings(primaryStream?.getAudioTracks()[0]);
932
+ const primaryVideoSettings = readSettings(primaryStream?.getVideoTracks()[0]);
933
+ const secondaryVideoSettings = readSettings(cameraStream?.getVideoTracks()[0]);
934
+ const actualSettings = {
935
+ ...primaryAudioSettings ? { audio: primaryAudioSettings } : {},
936
+ ...primaryVideoSettings ? { [screenEnabled ? "screen" : "camera"]: primaryVideoSettings } : {},
937
+ ...secondaryVideoSettings ? { camera: secondaryVideoSettings } : {}
938
+ };
939
+ return /* @__PURE__ */ jsxs("details", { style: detailsStyle, "data-testid": "recorder-device-settings", children: [
940
+ /* @__PURE__ */ jsx("summary", { style: summaryToggleStyle, children: "Advanced device settings" }),
941
+ /* @__PURE__ */ jsxs("div", { style: panelStyle, children: [
942
+ /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
943
+ "Constraint strength",
944
+ /* @__PURE__ */ jsxs(
945
+ "select",
946
+ {
947
+ value: value.constraintMode,
948
+ disabled,
949
+ style: inputStyle,
950
+ onChange: (event) => onChange({
951
+ ...value,
952
+ constraintMode: event.target.value === "exact" ? "exact" : "ideal"
953
+ }),
954
+ children: [
955
+ /* @__PURE__ */ jsx("option", { value: "ideal", children: "Preferred (use closest available)" }),
956
+ /* @__PURE__ */ jsx("option", { value: "exact", children: "Required (fail when unavailable)" })
957
+ ]
958
+ }
959
+ )
960
+ ] }),
961
+ microphoneEnabled && /* @__PURE__ */ jsxs("fieldset", { style: fieldsetStyle, disabled, children: [
962
+ /* @__PURE__ */ jsx("legend", { style: legendStyle, children: "Microphone" }),
963
+ /* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
964
+ /* @__PURE__ */ jsxs("label", { style: { ...fieldStyle, gridColumn: "1 / -1" }, children: [
965
+ "Input device",
966
+ /* @__PURE__ */ jsxs(
967
+ "select",
968
+ {
969
+ "aria-label": "Recording microphone",
970
+ value: value.audio.deviceId,
971
+ style: inputStyle,
972
+ onChange: (event) => patchAudio({ deviceId: event.target.value }),
973
+ children: [
974
+ /* @__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))
976
+ ]
977
+ }
978
+ )
979
+ ] }),
980
+ supports("groupId") && audioGroups.length > 0 && /* @__PURE__ */ jsxs("label", { style: { ...fieldStyle, gridColumn: "1 / -1" }, children: [
981
+ "Device group",
982
+ /* @__PURE__ */ jsxs(
983
+ "select",
984
+ {
985
+ value: value.audio.groupId ?? "",
986
+ style: inputStyle,
987
+ onChange: (event) => patchAudio({ groupId: event.target.value || void 0 }),
988
+ children: [
989
+ /* @__PURE__ */ jsx("option", { value: "", children: "Any group" }),
990
+ audioGroups.map((group) => /* @__PURE__ */ jsx("option", { value: group.id, children: group.label }, group.id))
991
+ ]
992
+ }
993
+ )
994
+ ] }),
995
+ supports("sampleRate") && /* @__PURE__ */ jsx(
996
+ NumericField,
997
+ {
998
+ label: "Sample rate (Hz)",
999
+ value: value.audio.sampleRate,
1000
+ onChange: (sampleRate) => patchAudio({ sampleRate }),
1001
+ disabled
1002
+ }
1003
+ ),
1004
+ supports("sampleSize") && /* @__PURE__ */ jsx(
1005
+ NumericField,
1006
+ {
1007
+ label: "Sample size (bits)",
1008
+ value: value.audio.sampleSize,
1009
+ onChange: (sampleSize) => patchAudio({ sampleSize }),
1010
+ disabled
1011
+ }
1012
+ ),
1013
+ supports("channelCount") && /* @__PURE__ */ jsx(
1014
+ NumericField,
1015
+ {
1016
+ label: "Channel count",
1017
+ value: value.audio.channelCount,
1018
+ onChange: (channelCount) => patchAudio({ channelCount }),
1019
+ disabled
1020
+ }
1021
+ ),
1022
+ supports("latency") && /* @__PURE__ */ jsx(
1023
+ NumericField,
1024
+ {
1025
+ label: "Latency (seconds)",
1026
+ value: value.audio.latency,
1027
+ onChange: (latency) => patchAudio({ latency }),
1028
+ disabled,
1029
+ step: 1e-3
1030
+ }
1031
+ ),
1032
+ supports("echoCancellation") && /* @__PURE__ */ jsx(
1033
+ BooleanField,
1034
+ {
1035
+ label: "Echo cancellation",
1036
+ value: value.audio.echoCancellation,
1037
+ onChange: (echoCancellation) => patchAudio({ echoCancellation }),
1038
+ disabled
1039
+ }
1040
+ ),
1041
+ supports("noiseSuppression") && /* @__PURE__ */ jsx(
1042
+ BooleanField,
1043
+ {
1044
+ label: "Noise suppression",
1045
+ value: value.audio.noiseSuppression,
1046
+ onChange: (noiseSuppression) => patchAudio({ noiseSuppression }),
1047
+ disabled
1048
+ }
1049
+ ),
1050
+ supports("autoGainControl") && /* @__PURE__ */ jsx(
1051
+ BooleanField,
1052
+ {
1053
+ label: "Automatic gain control",
1054
+ value: value.audio.autoGainControl,
1055
+ onChange: (autoGainControl) => patchAudio({ autoGainControl }),
1056
+ disabled
1057
+ }
1058
+ )
1059
+ ] })
1060
+ ] }),
1061
+ cameraEnabled && /* @__PURE__ */ jsxs("fieldset", { style: fieldsetStyle, disabled, children: [
1062
+ /* @__PURE__ */ jsx("legend", { style: legendStyle, children: "Camera" }),
1063
+ /* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
1064
+ /* @__PURE__ */ jsxs("label", { style: { ...fieldStyle, gridColumn: "1 / -1" }, children: [
1065
+ "Video input",
1066
+ /* @__PURE__ */ jsxs(
1067
+ "select",
1068
+ {
1069
+ "aria-label": "Recording camera",
1070
+ value: value.camera.deviceId,
1071
+ style: inputStyle,
1072
+ onChange: (event) => patchCamera({ deviceId: event.target.value }),
1073
+ children: [
1074
+ /* @__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))
1076
+ ]
1077
+ }
1078
+ )
1079
+ ] }),
1080
+ supports("groupId") && videoGroups.length > 0 && /* @__PURE__ */ jsxs("label", { style: { ...fieldStyle, gridColumn: "1 / -1" }, children: [
1081
+ "Device group",
1082
+ /* @__PURE__ */ jsxs(
1083
+ "select",
1084
+ {
1085
+ value: value.camera.groupId ?? "",
1086
+ style: inputStyle,
1087
+ onChange: (event) => patchCamera({ groupId: event.target.value || void 0 }),
1088
+ children: [
1089
+ /* @__PURE__ */ jsx("option", { value: "", children: "Any group" }),
1090
+ videoGroups.map((group) => /* @__PURE__ */ jsx("option", { value: group.id, children: group.label }, group.id))
1091
+ ]
1092
+ }
1093
+ )
1094
+ ] }),
1095
+ supports("width") && /* @__PURE__ */ jsx(
1096
+ NumericField,
1097
+ {
1098
+ label: "Width (px)",
1099
+ value: value.camera.width,
1100
+ onChange: (width) => patchCamera({ width }),
1101
+ disabled
1102
+ }
1103
+ ),
1104
+ supports("height") && /* @__PURE__ */ jsx(
1105
+ NumericField,
1106
+ {
1107
+ label: "Height (px)",
1108
+ value: value.camera.height,
1109
+ onChange: (height) => patchCamera({ height }),
1110
+ disabled
1111
+ }
1112
+ ),
1113
+ supports("aspectRatio") && /* @__PURE__ */ jsx(
1114
+ NumericField,
1115
+ {
1116
+ label: "Aspect ratio (width / height)",
1117
+ value: value.camera.aspectRatio,
1118
+ onChange: (aspectRatio) => patchCamera({ aspectRatio }),
1119
+ disabled,
1120
+ step: 0.01
1121
+ }
1122
+ ),
1123
+ supports("frameRate") && /* @__PURE__ */ jsx(
1124
+ NumericField,
1125
+ {
1126
+ label: "Frame rate (fps)",
1127
+ value: value.camera.frameRate,
1128
+ onChange: (frameRate) => patchCamera({ frameRate }),
1129
+ disabled,
1130
+ step: 0.01
1131
+ }
1132
+ ),
1133
+ supports("facingMode") && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1134
+ "Facing mode",
1135
+ /* @__PURE__ */ jsxs(
1136
+ "select",
1137
+ {
1138
+ value: value.camera.facingMode ?? "",
1139
+ disabled,
1140
+ style: inputStyle,
1141
+ onChange: (event) => patchCamera({ facingMode: event.target.value || void 0 }),
1142
+ children: [
1143
+ /* @__PURE__ */ jsx("option", { value: "", children: "Browser default" }),
1144
+ /* @__PURE__ */ jsx("option", { value: "user", children: "User/front" }),
1145
+ /* @__PURE__ */ jsx("option", { value: "environment", children: "Environment/rear" }),
1146
+ /* @__PURE__ */ jsx("option", { value: "left", children: "Left" }),
1147
+ /* @__PURE__ */ jsx("option", { value: "right", children: "Right" })
1148
+ ]
1149
+ }
1150
+ )
1151
+ ] }),
1152
+ supports("resizeMode") && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1153
+ "Resize mode",
1154
+ /* @__PURE__ */ jsxs(
1155
+ "select",
1156
+ {
1157
+ value: value.camera.resizeMode ?? "",
1158
+ disabled,
1159
+ style: inputStyle,
1160
+ onChange: (event) => patchCamera({ resizeMode: event.target.value || void 0 }),
1161
+ children: [
1162
+ /* @__PURE__ */ jsx("option", { value: "", children: "Browser default" }),
1163
+ /* @__PURE__ */ jsx("option", { value: "none", children: "None" }),
1164
+ /* @__PURE__ */ jsx("option", { value: "crop-and-scale", children: "Crop and scale" })
1165
+ ]
1166
+ }
1167
+ )
1168
+ ] }),
1169
+ supports("backgroundBlur") && /* @__PURE__ */ jsx(
1170
+ BooleanField,
1171
+ {
1172
+ label: "Background blur",
1173
+ value: value.camera.backgroundBlur,
1174
+ onChange: (backgroundBlur) => patchCamera({ backgroundBlur }),
1175
+ disabled
1176
+ }
1177
+ )
1178
+ ] })
1179
+ ] }),
1180
+ (screenEnabled || systemAudioEnabled) && /* @__PURE__ */ jsxs("fieldset", { style: fieldsetStyle, disabled, children: [
1181
+ /* @__PURE__ */ jsx("legend", { style: legendStyle, children: screenEnabled ? "Screen capture" : "System audio capture" }),
1182
+ /* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
1183
+ screenEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
1184
+ supports("displaySurface") && /* @__PURE__ */ jsxs("label", { style: { ...fieldStyle, gridColumn: "1 / -1" }, children: [
1185
+ "Preferred surface",
1186
+ /* @__PURE__ */ jsxs(
1187
+ "select",
1188
+ {
1189
+ value: value.screen.displaySurface ?? "",
1190
+ disabled,
1191
+ style: inputStyle,
1192
+ onChange: (event) => patchScreen({ displaySurface: event.target.value || void 0 }),
1193
+ children: [
1194
+ /* @__PURE__ */ jsx("option", { value: "", children: "Browser picker default" }),
1195
+ /* @__PURE__ */ jsx("option", { value: "monitor", children: "Entire monitor" }),
1196
+ /* @__PURE__ */ jsx("option", { value: "window", children: "Application window" }),
1197
+ /* @__PURE__ */ jsx("option", { value: "browser", children: "Browser tab" })
1198
+ ]
1199
+ }
1200
+ )
1201
+ ] }),
1202
+ supports("cursor") && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1203
+ "Mouse cursor",
1204
+ /* @__PURE__ */ jsxs(
1205
+ "select",
1206
+ {
1207
+ value: value.screen.cursor ?? "",
1208
+ disabled,
1209
+ style: inputStyle,
1210
+ onChange: (event) => patchScreen({ cursor: event.target.value || void 0 }),
1211
+ children: [
1212
+ /* @__PURE__ */ jsx("option", { value: "", children: "Browser default" }),
1213
+ /* @__PURE__ */ jsx("option", { value: "always", children: "Always" }),
1214
+ /* @__PURE__ */ jsx("option", { value: "motion", children: "While moving" }),
1215
+ /* @__PURE__ */ jsx("option", { value: "never", children: "Never" })
1216
+ ]
1217
+ }
1218
+ )
1219
+ ] }),
1220
+ supports("logicalSurface") && /* @__PURE__ */ jsx(
1221
+ BooleanField,
1222
+ {
1223
+ label: "Logical surface",
1224
+ value: value.screen.logicalSurface,
1225
+ onChange: (logicalSurface) => patchScreen({ logicalSurface }),
1226
+ disabled
1227
+ }
1228
+ ),
1229
+ supports("resizeMode") && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1230
+ "Resize mode",
1231
+ /* @__PURE__ */ jsxs(
1232
+ "select",
1233
+ {
1234
+ value: value.screen.resizeMode ?? "",
1235
+ disabled,
1236
+ style: inputStyle,
1237
+ onChange: (event) => patchScreen({ resizeMode: event.target.value || void 0 }),
1238
+ children: [
1239
+ /* @__PURE__ */ jsx("option", { value: "", children: "Browser default" }),
1240
+ /* @__PURE__ */ jsx("option", { value: "none", children: "Full detail" }),
1241
+ /* @__PURE__ */ jsx("option", { value: "crop-and-scale", children: "Downscale" })
1242
+ ]
1243
+ }
1244
+ )
1245
+ ] }),
1246
+ supports("width") && /* @__PURE__ */ jsx(
1247
+ NumericField,
1248
+ {
1249
+ label: "Width (px)",
1250
+ value: value.screen.width,
1251
+ onChange: (width) => patchScreen({ width }),
1252
+ disabled
1253
+ }
1254
+ ),
1255
+ supports("height") && /* @__PURE__ */ jsx(
1256
+ NumericField,
1257
+ {
1258
+ label: "Height (px)",
1259
+ value: value.screen.height,
1260
+ onChange: (height) => patchScreen({ height }),
1261
+ disabled
1262
+ }
1263
+ ),
1264
+ supports("aspectRatio") && /* @__PURE__ */ jsx(
1265
+ NumericField,
1266
+ {
1267
+ label: "Aspect ratio (width / height)",
1268
+ value: value.screen.aspectRatio,
1269
+ onChange: (aspectRatio) => patchScreen({ aspectRatio }),
1270
+ disabled,
1271
+ step: 0.01
1272
+ }
1273
+ ),
1274
+ supports("frameRate") && /* @__PURE__ */ jsx(
1275
+ NumericField,
1276
+ {
1277
+ label: "Frame rate (fps)",
1278
+ value: value.screen.frameRate,
1279
+ onChange: (frameRate) => patchScreen({ frameRate }),
1280
+ disabled,
1281
+ step: 0.01
1282
+ }
1283
+ )
1284
+ ] }),
1285
+ systemAudioEnabled && supports("restrictOwnAudio") && /* @__PURE__ */ jsx(
1286
+ BooleanField,
1287
+ {
1288
+ label: "Remove this tab's audio",
1289
+ value: value.screenAudio.restrictOwnAudio,
1290
+ onChange: (restrictOwnAudio) => patchScreenAudio({ restrictOwnAudio }),
1291
+ disabled
1292
+ }
1293
+ ),
1294
+ systemAudioEnabled && supports("suppressLocalAudioPlayback") && /* @__PURE__ */ jsx(
1295
+ BooleanField,
1296
+ {
1297
+ label: "Mute captured tab locally",
1298
+ value: value.screenAudio.suppressLocalAudioPlayback,
1299
+ onChange: (suppressLocalAudioPlayback) => patchScreenAudio({ suppressLocalAudioPlayback }),
1300
+ disabled
1301
+ }
1302
+ )
1303
+ ] }),
1304
+ /* @__PURE__ */ jsx("p", { style: noteStyle, children: "The browser still shows its own surface picker and may limit which display constraints it honors." })
1305
+ ] }),
1306
+ /* @__PURE__ */ jsxs("fieldset", { style: fieldsetStyle, disabled, children: [
1307
+ /* @__PURE__ */ jsx("legend", { style: legendStyle, children: "Encoding" }),
1308
+ /* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
1309
+ hasStandaloneAudioRecorder && /* @__PURE__ */ jsxs("label", { style: { ...fieldStyle, gridColumn: "1 / -1" }, children: [
1310
+ "Audio MIME type / codecs",
1311
+ /* @__PURE__ */ jsx(
1312
+ "input",
1313
+ {
1314
+ type: "text",
1315
+ list: audioMimeListId,
1316
+ value: value.encoding.audioMimeType,
1317
+ style: inputStyle,
1318
+ placeholder: "Automatic",
1319
+ onChange: (event) => patchEncoding({ audioMimeType: event.target.value })
1320
+ }
1321
+ ),
1322
+ /* @__PURE__ */ jsx("datalist", { id: audioMimeListId, children: AUDIO_MIME_TYPES.map((mime) => /* @__PURE__ */ jsx("option", { value: mime }, mime)) })
1323
+ ] }),
1324
+ hasAudioTrack && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
1325
+ NumericField,
1326
+ {
1327
+ label: "Audio bitrate (kbps)",
1328
+ value: value.encoding.audioBitsPerSecond,
1329
+ onChange: (audioBitsPerSecond) => patchEncoding({ audioBitsPerSecond }),
1330
+ disabled
1331
+ }
1332
+ ) }),
1333
+ (cameraEnabled || screenEnabled) && /* @__PURE__ */ jsxs(Fragment, { children: [
1334
+ /* @__PURE__ */ jsxs("label", { style: { ...fieldStyle, gridColumn: "1 / -1" }, children: [
1335
+ "Video MIME type / codecs",
1336
+ /* @__PURE__ */ jsx(
1337
+ "input",
1338
+ {
1339
+ type: "text",
1340
+ list: videoMimeListId,
1341
+ value: value.encoding.videoMimeType,
1342
+ style: inputStyle,
1343
+ placeholder: "Automatic",
1344
+ onChange: (event) => patchEncoding({ videoMimeType: event.target.value })
1345
+ }
1346
+ ),
1347
+ /* @__PURE__ */ jsx("datalist", { id: videoMimeListId, children: VIDEO_MIME_TYPES.map((mime) => /* @__PURE__ */ jsx("option", { value: mime }, mime)) })
1348
+ ] }),
1349
+ /* @__PURE__ */ jsx(
1350
+ NumericField,
1351
+ {
1352
+ label: "Video bitrate (kbps)",
1353
+ value: value.encoding.videoBitsPerSecond,
1354
+ onChange: (videoBitsPerSecond) => patchEncoding({ videoBitsPerSecond }),
1355
+ disabled
1356
+ }
1357
+ )
1358
+ ] }),
1359
+ /* @__PURE__ */ jsx(
1360
+ NumericField,
1361
+ {
1362
+ label: "Total bitrate (kbps)",
1363
+ value: value.encoding.bitsPerSecond,
1364
+ onChange: (bitsPerSecond) => patchEncoding({ bitsPerSecond }),
1365
+ disabled
1366
+ }
1367
+ ),
1368
+ hasAudioTrack && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1369
+ "Audio bitrate mode",
1370
+ /* @__PURE__ */ jsxs(
1371
+ "select",
1372
+ {
1373
+ value: value.encoding.audioBitrateMode ?? "",
1374
+ disabled,
1375
+ style: inputStyle,
1376
+ onChange: (event) => patchEncoding({
1377
+ audioBitrateMode: event.target.value === "constant" || event.target.value === "variable" ? event.target.value : void 0
1378
+ }),
1379
+ children: [
1380
+ /* @__PURE__ */ jsx("option", { value: "", children: "Browser default" }),
1381
+ /* @__PURE__ */ jsx("option", { value: "variable", children: "Variable" }),
1382
+ /* @__PURE__ */ jsx("option", { value: "constant", children: "Constant" })
1383
+ ]
1384
+ }
1385
+ )
1386
+ ] }),
1387
+ (cameraEnabled || screenEnabled) && /* @__PURE__ */ jsxs(Fragment, { children: [
1388
+ /* @__PURE__ */ jsx(
1389
+ NumericField,
1390
+ {
1391
+ label: "Keyframe interval (ms)",
1392
+ value: value.encoding.videoKeyFrameIntervalDuration,
1393
+ onChange: (videoKeyFrameIntervalDuration) => patchEncoding({
1394
+ videoKeyFrameIntervalDuration,
1395
+ videoKeyFrameIntervalCount: void 0
1396
+ }),
1397
+ disabled
1398
+ }
1399
+ ),
1400
+ /* @__PURE__ */ jsx(
1401
+ NumericField,
1402
+ {
1403
+ label: "Keyframe interval (frames)",
1404
+ value: value.encoding.videoKeyFrameIntervalCount,
1405
+ onChange: (videoKeyFrameIntervalCount) => patchEncoding({
1406
+ videoKeyFrameIntervalCount,
1407
+ videoKeyFrameIntervalDuration: void 0
1408
+ }),
1409
+ disabled
1410
+ }
1411
+ )
1412
+ ] })
1413
+ ] }),
1414
+ /* @__PURE__ */ jsx("p", { style: noteStyle, children: "MIME and bitrate values are hints. Unsupported MIME choices fall back to the recorder's automatic format selection. A total bitrate overrides separate audio/video bitrate hints." })
1415
+ ] }),
1416
+ Object.keys(actualSettings).length > 0 && /* @__PURE__ */ jsxs("fieldset", { style: fieldsetStyle, children: [
1417
+ /* @__PURE__ */ jsx("legend", { style: legendStyle, children: "Active track settings" }),
1418
+ /* @__PURE__ */ jsx("pre", { style: actualStyle, children: JSON.stringify(actualSettings, null, 2) })
1419
+ ] }),
1420
+ /* @__PURE__ */ jsx("p", { style: noteStyle, children: "Only constraints advertised by this browser are shown. Device names may appear only after microphone or camera permission is granted." })
1421
+ ] })
1422
+ ] });
1423
+ }
1424
+
1425
+ // src/recorder/deviceSettings.ts
1426
+ var DEFAULT_RECORDER_DEVICE_SETTINGS = {
1427
+ constraintMode: "ideal",
1428
+ audio: { deviceId: "" },
1429
+ camera: { deviceId: "" },
1430
+ screen: {},
1431
+ screenAudio: {},
1432
+ encoding: {
1433
+ audioMimeType: "",
1434
+ videoMimeType: ""
1435
+ }
1436
+ };
1437
+ function preferredNumber(value, mode) {
1438
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return void 0;
1439
+ return { [mode]: value };
1440
+ }
1441
+ function preferredInteger(value, mode) {
1442
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return void 0;
1443
+ return { [mode]: Math.round(value) };
1444
+ }
1445
+ function preferredString(value, mode) {
1446
+ const trimmed = value?.trim();
1447
+ return trimmed ? { [mode]: trimmed } : void 0;
1448
+ }
1449
+ function buildRecorderAudioConstraints(settings) {
1450
+ const { audio, constraintMode } = settings;
1451
+ const result = {};
1452
+ if (audio.deviceId) result.deviceId = { exact: audio.deviceId };
1453
+ const groupId = preferredString(audio.groupId, constraintMode);
1454
+ if (groupId) result.groupId = groupId;
1455
+ if (audio.autoGainControl !== void 0) result.autoGainControl = audio.autoGainControl;
1456
+ if (audio.echoCancellation !== void 0) result.echoCancellation = audio.echoCancellation;
1457
+ if (audio.noiseSuppression !== void 0) result.noiseSuppression = audio.noiseSuppression;
1458
+ const channelCount = preferredInteger(audio.channelCount, constraintMode);
1459
+ const latency = preferredNumber(audio.latency, constraintMode);
1460
+ const sampleRate = preferredInteger(audio.sampleRate, constraintMode);
1461
+ const sampleSize = preferredInteger(audio.sampleSize, constraintMode);
1462
+ if (channelCount) result.channelCount = channelCount;
1463
+ if (latency) result.latency = latency;
1464
+ if (sampleRate) result.sampleRate = sampleRate;
1465
+ if (sampleSize) result.sampleSize = sampleSize;
1466
+ return result;
1467
+ }
1468
+ function buildRecorderCameraConstraints(settings) {
1469
+ const { camera, constraintMode } = settings;
1470
+ const result = {};
1471
+ if (camera.deviceId) result.deviceId = { exact: camera.deviceId };
1472
+ const groupId = preferredString(camera.groupId, constraintMode);
1473
+ if (groupId) result.groupId = groupId;
1474
+ if (camera.backgroundBlur !== void 0) result.backgroundBlur = camera.backgroundBlur;
1475
+ const aspectRatio = preferredNumber(camera.aspectRatio, constraintMode);
1476
+ const facingMode = preferredString(camera.facingMode, constraintMode);
1477
+ const frameRate = preferredNumber(camera.frameRate, constraintMode);
1478
+ const height = preferredInteger(camera.height, constraintMode);
1479
+ const resizeMode = preferredString(camera.resizeMode, constraintMode);
1480
+ const width = preferredInteger(camera.width, constraintMode);
1481
+ if (aspectRatio) result.aspectRatio = aspectRatio;
1482
+ if (facingMode) result.facingMode = facingMode;
1483
+ if (frameRate) result.frameRate = frameRate;
1484
+ if (height) result.height = height;
1485
+ if (resizeMode) result.resizeMode = resizeMode;
1486
+ if (width) result.width = width;
1487
+ return result;
1488
+ }
1489
+ function buildRecorderScreenConstraints(settings) {
1490
+ const { screen } = settings;
1491
+ const result = {};
1492
+ const aspectRatio = preferredNumber(screen.aspectRatio, "ideal");
1493
+ const cursor = preferredString(screen.cursor, "ideal");
1494
+ const displaySurface = preferredString(screen.displaySurface, "ideal");
1495
+ const frameRate = preferredNumber(screen.frameRate, "ideal");
1496
+ const height = preferredInteger(screen.height, "ideal");
1497
+ const resizeMode = preferredString(screen.resizeMode, "ideal");
1498
+ const width = preferredInteger(screen.width, "ideal");
1499
+ if (screen.logicalSurface !== void 0) result.logicalSurface = screen.logicalSurface;
1500
+ if (aspectRatio) result.aspectRatio = aspectRatio;
1501
+ if (cursor) result.cursor = cursor;
1502
+ if (displaySurface) result.displaySurface = displaySurface;
1503
+ if (frameRate) result.frameRate = frameRate;
1504
+ if (height) result.height = height;
1505
+ if (resizeMode) result.resizeMode = resizeMode;
1506
+ if (width) result.width = width;
1507
+ return result;
1508
+ }
1509
+ function buildRecorderScreenAudioConstraints(settings) {
1510
+ const result = {};
1511
+ if (settings.screenAudio.restrictOwnAudio !== void 0) {
1512
+ result.restrictOwnAudio = settings.screenAudio.restrictOwnAudio;
1513
+ }
1514
+ if (settings.screenAudio.suppressLocalAudioPlayback !== void 0) {
1515
+ result.suppressLocalAudioPlayback = settings.screenAudio.suppressLocalAudioPlayback;
1516
+ }
1517
+ return result;
1518
+ }
1519
+ function recorderBitsPerSecond(value) {
1520
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return void 0;
1521
+ return Math.round(value * 1e3);
1522
+ }
1523
+
702
1524
  // src/recorder/RecorderModal.tsx
703
1525
  import {
704
- Fragment,
705
- useCallback as useCallback2,
706
- useEffect as useEffect2,
707
- useId,
1526
+ Fragment as Fragment2,
1527
+ useCallback as useCallback3,
1528
+ useEffect as useEffect3,
1529
+ useId as useId2,
1530
+ useMemo,
708
1531
  useRef as useRef2,
709
- useState as useState2
1532
+ useState as useState3
710
1533
  } from "react";
711
1534
 
712
1535
  // src/modal/useModalDialog.ts
@@ -735,7 +1558,7 @@ function narrationCaptureSummary(withCamera) {
735
1558
  }
736
1559
 
737
1560
  // src/recorder/RecorderModal.tsx
738
- import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
1561
+ import { Fragment as Fragment3, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
739
1562
  var overlayStyle = {
740
1563
  position: "fixed",
741
1564
  inset: 0,
@@ -750,6 +1573,7 @@ function recorderThemeStyle(colorScheme) {
750
1573
  return {
751
1574
  colorScheme,
752
1575
  "--squisq-recorder-surface": `var(--squisq-bg, ${dark ? "#1f2937" : "#fffdf7"})`,
1576
+ "--squisq-recorder-surface-muted": `var(--squisq-panel-bg, ${dark ? "#111827" : "#f8f4e8"})`,
753
1577
  "--squisq-recorder-input": `var(--squisq-input-bg, ${dark ? "#374151" : "#fff"})`,
754
1578
  "--squisq-recorder-border": `var(--squisq-border, ${dark ? "#4b5563" : "#c9b98a"})`,
755
1579
  "--squisq-recorder-text": `var(--squisq-text, ${dark ? "#e5e7eb" : "#4a3c1f"})`,
@@ -788,7 +1612,7 @@ var labelStyle = {
788
1612
  marginBottom: 4,
789
1613
  color: "var(--squisq-recorder-text)"
790
1614
  };
791
- var inputStyle = {
1615
+ var inputStyle2 = {
792
1616
  width: "100%",
793
1617
  padding: "6px 8px",
794
1618
  fontSize: 13,
@@ -801,7 +1625,7 @@ var inputStyle = {
801
1625
  boxSizing: "border-box"
802
1626
  };
803
1627
  var textareaStyle = {
804
- ...inputStyle,
1628
+ ...inputStyle2,
805
1629
  resize: "vertical",
806
1630
  minHeight: 72
807
1631
  };
@@ -1082,23 +1906,31 @@ function RecorderModal({
1082
1906
  narration = null
1083
1907
  }) {
1084
1908
  const initialToggles = toggleStateFromMode(initialMode);
1085
- const [micOn, setMicOn] = useState2(initialToggles.micOn);
1086
- const [cameraOn, setCameraOn] = useState2(initialToggles.cameraOn);
1087
- const [screenOn, setScreenOn] = useState2(initialToggles.screenOn);
1088
- const [sourceText, setSourceText] = useState2("");
1089
- const [basename, setBasename] = useState2("");
1090
- const [includeSystemAudio, setIncludeSystemAudio] = useState2(false);
1091
- const [isSaving, setIsSaving] = useState2(false);
1092
- const [saveError, setSaveError] = useState2(null);
1093
- const [playbackUrl, setPlaybackUrl] = useState2(null);
1094
- const [cameraPlaybackUrl, setCameraPlaybackUrl] = useState2(null);
1095
- const [playbackPositionMs, setPlaybackPositionMs] = useState2(0);
1096
- const [narrationOn, setNarrationOn] = useState2(false);
1097
- const [narrationRequesting, setNarrationRequesting] = useState2(false);
1098
- const [narrationPreview, setNarrationPreview] = useState2(null);
1909
+ const [micOn, setMicOn] = useState3(initialToggles.micOn);
1910
+ const [cameraOn, setCameraOn] = useState3(initialToggles.cameraOn);
1911
+ const [screenOn, setScreenOn] = useState3(initialToggles.screenOn);
1912
+ const [sourceText, setSourceText] = useState3("");
1913
+ const [basename, setBasename] = useState3("");
1914
+ const [includeSystemAudio, setIncludeSystemAudio] = useState3(false);
1915
+ const [isSaving, setIsSaving] = useState3(false);
1916
+ const [saveError, setSaveError] = useState3(null);
1917
+ const [playbackUrl, setPlaybackUrl] = useState3(null);
1918
+ const [cameraPlaybackUrl, setCameraPlaybackUrl] = useState3(null);
1919
+ const [playbackPositionMs, setPlaybackPositionMs] = useState3(0);
1920
+ const [narrationOn, setNarrationOn] = useState3(false);
1921
+ const [narrationRequesting, setNarrationRequesting] = useState3(false);
1922
+ const [narrationPreview, setNarrationPreview] = useState3(null);
1923
+ const [deviceSettings, setDeviceSettings] = useState3(() => ({
1924
+ ...DEFAULT_RECORDER_DEVICE_SETTINGS,
1925
+ audio: { ...DEFAULT_RECORDER_DEVICE_SETTINGS.audio },
1926
+ camera: { ...DEFAULT_RECORDER_DEVICE_SETTINGS.camera },
1927
+ screen: { ...DEFAULT_RECORDER_DEVICE_SETTINGS.screen },
1928
+ screenAudio: { ...DEFAULT_RECORDER_DEVICE_SETTINGS.screenAudio },
1929
+ encoding: { ...DEFAULT_RECORDER_DEVICE_SETTINGS.encoding }
1930
+ }));
1099
1931
  const overlayRef = useRef2(null);
1100
1932
  const dialogRef = useRef2(null);
1101
- const headingId = useId();
1933
+ const headingId = useId2();
1102
1934
  const previewRef = useRef2(null);
1103
1935
  const cameraPreviewRef = useRef2(null);
1104
1936
  const derivedSource = deriveSource({ micOn, cameraOn, screenOn });
@@ -1106,9 +1938,37 @@ function RecorderModal({
1106
1938
  const source = derivedSource ?? "mic";
1107
1939
  const isDual = source === "screen+camera";
1108
1940
  const canIncludeSystemAudio = supportsSystemAudioCapture();
1941
+ const audioConstraints = useMemo(
1942
+ () => buildRecorderAudioConstraints(deviceSettings),
1943
+ [deviceSettings]
1944
+ );
1945
+ const cameraConstraints = useMemo(
1946
+ () => buildRecorderCameraConstraints(deviceSettings),
1947
+ [deviceSettings]
1948
+ );
1949
+ const screenConstraints = useMemo(
1950
+ () => buildRecorderScreenConstraints(deviceSettings),
1951
+ [deviceSettings]
1952
+ );
1953
+ const screenAudioConstraints = useMemo(
1954
+ () => buildRecorderScreenAudioConstraints(deviceSettings),
1955
+ [deviceSettings]
1956
+ );
1957
+ const encoding = deviceSettings.encoding;
1109
1958
  const recorder = useMediaRecorder({
1110
1959
  source,
1111
1960
  includeMicrophone: cameraOn ? micOn : void 0,
1961
+ audioConstraints,
1962
+ videoConstraints: cameraConstraints,
1963
+ screenVideoConstraints: screenConstraints,
1964
+ screenAudioConstraints,
1965
+ mimeType: (source === "mic" ? encoding.audioMimeType : encoding.videoMimeType).trim() || void 0,
1966
+ bitsPerSecond: recorderBitsPerSecond(encoding.bitsPerSecond),
1967
+ audioBitsPerSecond: recorderBitsPerSecond(encoding.audioBitsPerSecond),
1968
+ videoBitsPerSecond: recorderBitsPerSecond(encoding.videoBitsPerSecond),
1969
+ audioBitrateMode: encoding.audioBitrateMode,
1970
+ videoKeyFrameIntervalDuration: encoding.videoKeyFrameIntervalDuration,
1971
+ videoKeyFrameIntervalCount: encoding.videoKeyFrameIntervalCount,
1112
1972
  // System audio rides the screen capture when Screen is on, and is otherwise
1113
1973
  // captured via a separate (video-discarded) display capture mixed into the
1114
1974
  // mic/camera file — so it is no longer gated on Screen.
@@ -1121,12 +1981,48 @@ function RecorderModal({
1121
1981
  const stage = useNarrationStage({
1122
1982
  doc: narration?.doc ?? null,
1123
1983
  recording: narration?.recording ?? null,
1124
- getAudioBasename: () => basenameRef.current.trim() || void 0
1984
+ getAudioBasename: () => basenameRef.current.trim() || void 0,
1985
+ micConstraints: audioConstraints,
1986
+ cameraConstraints,
1987
+ audioRecorderOptions: {
1988
+ mimeType: encoding.audioMimeType.trim() || void 0,
1989
+ bitsPerSecond: recorderBitsPerSecond(encoding.bitsPerSecond),
1990
+ audioBitsPerSecond: recorderBitsPerSecond(encoding.audioBitsPerSecond),
1991
+ audioBitrateMode: encoding.audioBitrateMode
1992
+ },
1993
+ cameraRecorderOptions: {
1994
+ mimeType: encoding.videoMimeType.trim() || void 0,
1995
+ bitsPerSecond: recorderBitsPerSecond(encoding.bitsPerSecond),
1996
+ videoBitsPerSecond: recorderBitsPerSecond(encoding.videoBitsPerSecond),
1997
+ videoKeyFrameIntervalDuration: encoding.videoKeyFrameIntervalDuration,
1998
+ videoKeyFrameIntervalCount: encoding.videoKeyFrameIntervalCount
1999
+ }
1125
2000
  });
1126
2001
  const stageRef = useRef2(stage);
1127
2002
  stageRef.current = stage;
2003
+ const handleDeviceSettingsChange = useCallback3(
2004
+ (next) => {
2005
+ setDeviceSettings(next);
2006
+ if (narrationOn) {
2007
+ stageRef.current.controller.setPrefs({
2008
+ micDeviceId: next.audio.deviceId || null
2009
+ });
2010
+ }
2011
+ },
2012
+ [narrationOn]
2013
+ );
2014
+ const narrationMicDeviceId = stage.controller.prefs.micDeviceId ?? "";
2015
+ useEffect3(() => {
2016
+ if (!narrationOn) return;
2017
+ setDeviceSettings(
2018
+ (current) => current.audio.deviceId === narrationMicDeviceId ? current : {
2019
+ ...current,
2020
+ audio: { ...current.audio, deviceId: narrationMicDeviceId }
2021
+ }
2022
+ );
2023
+ }, [narrationMicDeviceId, narrationOn]);
1128
2024
  const prevNarrationOnRef = useRef2(narrationOn);
1129
- useEffect2(() => {
2025
+ useEffect3(() => {
1130
2026
  const was = prevNarrationOnRef.current;
1131
2027
  prevNarrationOnRef.current = narrationOn;
1132
2028
  if (was === narrationOn) return;
@@ -1141,7 +2037,7 @@ function RecorderModal({
1141
2037
  if (s.float.isOpen) s.float.close();
1142
2038
  }
1143
2039
  }, [narrationOn, recorder]);
1144
- useEffect2(() => {
2040
+ useEffect3(() => {
1145
2041
  if (!narrationOn) return;
1146
2042
  const onKey = (event) => {
1147
2043
  if (event.key !== "Escape") return;
@@ -1154,21 +2050,23 @@ function RecorderModal({
1154
2050
  const narrationMicLive = stage.controller.mic.status === "live";
1155
2051
  const narrationPreviewWanted = narrationOn && stage.recorder.withCamera && narrationMicLive && stage.recorder.cameraStream === null && stage.recorder.state !== "processing" && stage.recorder.state !== "review" && stage.recorder.state !== "saving";
1156
2052
  const narrationPreviewRef = useRef2(null);
1157
- useEffect2(() => {
1158
- if (!narrationPreviewWanted) {
1159
- const existing = narrationPreviewRef.current;
1160
- if (existing) {
1161
- for (const track of existing.getTracks()) track.stop();
1162
- narrationPreviewRef.current = null;
1163
- setNarrationPreview(null);
1164
- }
1165
- return;
2053
+ useEffect3(() => {
2054
+ const existing = narrationPreviewRef.current;
2055
+ if (existing) {
2056
+ for (const track of existing.getTracks()) track.stop();
2057
+ narrationPreviewRef.current = null;
2058
+ setNarrationPreview(null);
1166
2059
  }
1167
- if (narrationPreviewRef.current) return;
2060
+ if (!narrationPreviewWanted) return;
1168
2061
  let cancelled = false;
2062
+ let owned = null;
1169
2063
  void (async () => {
1170
2064
  try {
1171
- const stream = await requestCameraStream({ video: true, audio: false });
2065
+ const stream = await requestCameraStream({
2066
+ video: cameraConstraints,
2067
+ audio: false
2068
+ });
2069
+ owned = stream;
1172
2070
  if (cancelled) {
1173
2071
  for (const track of stream.getTracks()) track.stop();
1174
2072
  return;
@@ -1180,15 +2078,14 @@ function RecorderModal({
1180
2078
  })();
1181
2079
  return () => {
1182
2080
  cancelled = true;
2081
+ if (owned && narrationPreviewRef.current === owned) {
2082
+ for (const track of owned.getTracks()) track.stop();
2083
+ narrationPreviewRef.current = null;
2084
+ setNarrationPreview(null);
2085
+ }
1183
2086
  };
1184
- }, [narrationPreviewWanted]);
1185
- useEffect2(() => {
1186
- return () => {
1187
- const existing = narrationPreviewRef.current;
1188
- if (existing) for (const track of existing.getTracks()) track.stop();
1189
- };
1190
- }, []);
1191
- const handleNarrationPreview = useCallback2(async () => {
2087
+ }, [cameraConstraints, narrationPreviewWanted]);
2088
+ const handleNarrationPreview = useCallback3(async () => {
1192
2089
  setNarrationRequesting(true);
1193
2090
  try {
1194
2091
  const controller = stageRef.current.controller;
@@ -1197,7 +2094,7 @@ function RecorderModal({
1197
2094
  setNarrationRequesting(false);
1198
2095
  }
1199
2096
  }, []);
1200
- const handleNarrationRecord = useCallback2(() => {
2097
+ const handleNarrationRecord = useCallback3(() => {
1201
2098
  void stageRef.current.recorder.start();
1202
2099
  }, []);
1203
2100
  const narrationCameraStream = narrationOn ? stage.recorder.cameraStream ?? narrationPreview : null;
@@ -1209,7 +2106,7 @@ function RecorderModal({
1209
2106
  cameraPreviewRef,
1210
2107
  !narrationOn && isDual && recorder.state !== "stopped" ? recorder.camera?.stream ?? null : null
1211
2108
  );
1212
- useEffect2(() => {
2109
+ useEffect3(() => {
1213
2110
  setPlaybackPositionMs(0);
1214
2111
  if (!recorder.blob) {
1215
2112
  setPlaybackUrl(null);
@@ -1222,7 +2119,7 @@ function RecorderModal({
1222
2119
  };
1223
2120
  }, [recorder.blob]);
1224
2121
  const cameraBlob = recorder.camera?.blob ?? null;
1225
- useEffect2(() => {
2122
+ useEffect3(() => {
1226
2123
  if (!cameraBlob) {
1227
2124
  setCameraPlaybackUrl(null);
1228
2125
  return;
@@ -1233,17 +2130,17 @@ function RecorderModal({
1233
2130
  URL.revokeObjectURL(url);
1234
2131
  };
1235
2132
  }, [cameraBlob]);
1236
- const captureKey = `${source}:${cameraOn ? micOn : ""}:${includeSystemAudio}`;
2133
+ const captureKey = `${source}:${cameraOn ? micOn : ""}:${includeSystemAudio}:${JSON.stringify(deviceSettings)}`;
1237
2134
  const previousKeyRef = useRef2(captureKey);
1238
2135
  const dualSaveProgressRef = useRef2({});
1239
- useEffect2(() => {
2136
+ useEffect3(() => {
1240
2137
  if (previousKeyRef.current !== captureKey) {
1241
2138
  previousKeyRef.current = captureKey;
1242
2139
  dualSaveProgressRef.current = {};
1243
2140
  recorder.cancel();
1244
2141
  }
1245
2142
  }, [captureKey, recorder]);
1246
- const handleClose = useCallback2(() => {
2143
+ const handleClose = useCallback3(() => {
1247
2144
  const s = stageRef.current;
1248
2145
  if (closeNeedsConfirm(narrationOn, s.recorder.state, s.recorder.take !== null)) {
1249
2146
  if (!window.confirm("Discard the current narration take?")) return;
@@ -1268,23 +2165,23 @@ function RecorderModal({
1268
2165
  stage.controller.transport
1269
2166
  )
1270
2167
  });
1271
- const handleRequest = useCallback2(async () => {
2168
+ const handleRequest = useCallback3(async () => {
1272
2169
  setSaveError(null);
1273
2170
  try {
1274
2171
  await recorder.request();
1275
2172
  } catch {
1276
2173
  }
1277
2174
  }, [recorder]);
1278
- const handleStart = useCallback2(() => {
2175
+ const handleStart = useCallback3(() => {
1279
2176
  setSaveError(null);
1280
2177
  dualSaveProgressRef.current = {};
1281
2178
  recorder.start();
1282
2179
  }, [recorder]);
1283
- const handleStop = useCallback2(async () => {
2180
+ const handleStop = useCallback3(async () => {
1284
2181
  setSaveError(null);
1285
2182
  await recorder.stop();
1286
2183
  }, [recorder]);
1287
- const handleSave = useCallback2(async () => {
2184
+ const handleSave = useCallback3(async () => {
1288
2185
  if (!recorder.blob || !recorder.mimeType || !recorder.extension || !recorder.directory) {
1289
2186
  setSaveError("Nothing to save yet \u2014 record something first.");
1290
2187
  return;
@@ -1423,11 +2320,11 @@ function RecorderModal({
1423
2320
  onSave,
1424
2321
  handleClose
1425
2322
  ]);
1426
- const handleDiscard = useCallback2(() => {
2323
+ const handleDiscard = useCallback3(() => {
1427
2324
  dualSaveProgressRef.current = {};
1428
2325
  recorder.reset();
1429
2326
  }, [recorder]);
1430
- const handlePlaybackTimeUpdate = useCallback2(
2327
+ const handlePlaybackTimeUpdate = useCallback3(
1431
2328
  (media) => {
1432
2329
  const currentMs = Number.isFinite(media.currentTime) ? media.currentTime * 1e3 : 0;
1433
2330
  setPlaybackPositionMs(Math.min(recorder.durationMs, Math.max(0, currentMs)));
@@ -1471,6 +2368,7 @@ function RecorderModal({
1471
2368
  recorder.blob !== null || recorder.camera?.blob != null,
1472
2369
  stage.recorder.state
1473
2370
  );
2371
+ const deviceSettingsLocked = narrationOn ? !narrationRecorderIdle : togglesLocked;
1474
2372
  const narrationToggleFor = (key) => {
1475
2373
  switch (key) {
1476
2374
  case "mic":
@@ -1506,14 +2404,14 @@ function RecorderModal({
1506
2404
  };
1507
2405
  }
1508
2406
  };
1509
- return /* @__PURE__ */ jsx(
2407
+ return /* @__PURE__ */ jsx2(
1510
2408
  "div",
1511
2409
  {
1512
2410
  ref: overlayRef,
1513
2411
  className: "squisq-editor-shell squisq-recorder-overlay",
1514
2412
  "data-theme": colorScheme,
1515
2413
  style: { ...overlayStyle, ...recorderThemeStyle(colorScheme) },
1516
- children: /* @__PURE__ */ jsxs(
2414
+ children: /* @__PURE__ */ jsxs2(
1517
2415
  "div",
1518
2416
  {
1519
2417
  ref: dialogRef,
@@ -1530,15 +2428,15 @@ function RecorderModal({
1530
2428
  "aria-labelledby": headingId,
1531
2429
  tabIndex: -1,
1532
2430
  children: [
1533
- /* @__PURE__ */ jsx("h2", { id: headingId, style: titleStyle, children: "Record media" }),
1534
- /* @__PURE__ */ jsxs("div", { style: narrationOn ? bodyRowStyle : void 0, children: [
1535
- /* @__PURE__ */ jsxs("div", { style: narrationOn ? leftColStyle : void 0, children: [
1536
- /* @__PURE__ */ jsx("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs(Fragment, { children: [
1537
- groupIndex > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: groupDividerStyle }),
1538
- /* @__PURE__ */ jsx("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
2431
+ /* @__PURE__ */ jsx2("h2", { id: headingId, style: titleStyle, children: "Record media" }),
2432
+ /* @__PURE__ */ jsxs2("div", { style: narrationOn ? bodyRowStyle : void 0, children: [
2433
+ /* @__PURE__ */ jsxs2("div", { style: narrationOn ? leftColStyle : void 0, children: [
2434
+ /* @__PURE__ */ jsx2("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs2(Fragment2, { children: [
2435
+ groupIndex > 0 && /* @__PURE__ */ jsx2("div", { "aria-hidden": "true", style: groupDividerStyle }),
2436
+ /* @__PURE__ */ jsx2("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
1539
2437
  if (t.key === "systemAudio" && !canIncludeSystemAudio) return null;
1540
2438
  const props = narrationOn ? narrationToggleFor(t.key) : simpleToggleProps(t.key);
1541
- return /* @__PURE__ */ jsx(
2439
+ return /* @__PURE__ */ jsx2(
1542
2440
  "button",
1543
2441
  {
1544
2442
  type: "button",
@@ -1553,9 +2451,9 @@ function RecorderModal({
1553
2451
  );
1554
2452
  }) })
1555
2453
  ] }, group.label)) }),
1556
- /* @__PURE__ */ jsx("p", { style: summaryStyle, children: narrationOn ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
1557
- narrationAvailable && /* @__PURE__ */ jsxs("label", { style: checkboxRowStyle, children: [
1558
- /* @__PURE__ */ jsx(
2454
+ /* @__PURE__ */ jsx2("p", { style: summaryStyle, children: narrationOn ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
2455
+ narrationAvailable && /* @__PURE__ */ jsxs2("label", { style: checkboxRowStyle, children: [
2456
+ /* @__PURE__ */ jsx2(
1559
2457
  "input",
1560
2458
  {
1561
2459
  type: "checkbox",
@@ -1568,10 +2466,25 @@ function RecorderModal({
1568
2466
  ),
1569
2467
  "Show narration mode"
1570
2468
  ] }),
1571
- !narrationOn && recorder.error && /* @__PURE__ */ jsx("div", { style: errorStyle, children: recorder.error.message }),
1572
- !narrationOn && saveError && /* @__PURE__ */ jsx("div", { style: errorStyle, children: saveError }),
1573
- narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
1574
- narrationOn && narrationCameraStream && /* @__PURE__ */ jsx("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx(
2469
+ /* @__PURE__ */ jsx2(
2470
+ RecorderDeviceSettingsPanel,
2471
+ {
2472
+ value: deviceSettings,
2473
+ onChange: handleDeviceSettingsChange,
2474
+ disabled: deviceSettingsLocked,
2475
+ microphoneEnabled: narrationOn ? true : micOn,
2476
+ cameraEnabled: narrationOn ? stage.recorder.withCamera : cameraOn,
2477
+ screenEnabled: narrationOn ? false : screenOn,
2478
+ systemAudioEnabled: !narrationOn && includeSystemAudio,
2479
+ separateAudioRecorder: narrationOn,
2480
+ primaryStream: narrationOn ? stage.controller.mic.stream : recorder.stream,
2481
+ cameraStream: narrationOn ? narrationCameraStream : recorder.camera?.stream ?? null
2482
+ }
2483
+ ),
2484
+ !narrationOn && recorder.error && /* @__PURE__ */ jsx2("div", { style: errorStyle, children: recorder.error.message }),
2485
+ !narrationOn && saveError && /* @__PURE__ */ jsx2("div", { style: errorStyle, children: saveError }),
2486
+ narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx2("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
2487
+ narrationOn && narrationCameraStream && /* @__PURE__ */ jsx2("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx2(
1575
2488
  "video",
1576
2489
  {
1577
2490
  ref: previewRef,
@@ -1581,10 +2494,10 @@ function RecorderModal({
1581
2494
  style: { width: "100%", height: "100%", objectFit: "contain" }
1582
2495
  }
1583
2496
  ) }),
1584
- narrationOn && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
1585
- narrationOn && !narrationCameraStream && !narrationTakeDone && stage.recorder.withCamera && /* @__PURE__ */ jsx("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx("span", { children: narrationPreviewWanted ? "Camera starting\u2026" : "Click Start preview to turn on your camera." }) }),
1586
- narrationOn && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
1587
- /* @__PURE__ */ jsx(
2497
+ narrationOn && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx2("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
2498
+ 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." }) }),
2499
+ narrationOn && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs2("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
2500
+ /* @__PURE__ */ jsx2(
1588
2501
  "div",
1589
2502
  {
1590
2503
  "aria-hidden": "true",
@@ -1594,11 +2507,11 @@ function RecorderModal({
1594
2507
  }
1595
2508
  }
1596
2509
  ),
1597
- /* @__PURE__ */ jsx("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." })
2510
+ /* @__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." })
1598
2511
  ] }),
1599
- !narrationOn && !showPreview && /* @__PURE__ */ jsx("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx("span", { children: "Click Start Preview to start a recording." }) }),
1600
- !narrationOn && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
1601
- /* @__PURE__ */ jsx(
2512
+ !narrationOn && !showPreview && /* @__PURE__ */ jsx2("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx2("span", { children: "Click Start Preview to start a recording." }) }),
2513
+ !narrationOn && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
2514
+ /* @__PURE__ */ jsx2(
1602
2515
  "video",
1603
2516
  {
1604
2517
  ref: previewRef,
@@ -1608,7 +2521,7 @@ function RecorderModal({
1608
2521
  style: { width: "100%", height: "100%", objectFit: "contain" }
1609
2522
  }
1610
2523
  ),
1611
- isDual && /* @__PURE__ */ jsx(
2524
+ isDual && /* @__PURE__ */ jsx2(
1612
2525
  "video",
1613
2526
  {
1614
2527
  ref: cameraPreviewRef,
@@ -1629,12 +2542,12 @@ function RecorderModal({
1629
2542
  }
1630
2543
  )
1631
2544
  ] }),
1632
- !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs(Fragment2, { children: [
2545
+ !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx2("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs2(Fragment3, { children: [
1633
2546
  "\u25CF Recording ",
1634
2547
  formatDurationMs(recorder.durationMs)
1635
- ] }) : /* @__PURE__ */ jsx(Fragment2, { children: "Microphone ready" }) }),
1636
- !narrationOn && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
1637
- /* @__PURE__ */ jsx(
2548
+ ] }) : /* @__PURE__ */ jsx2(Fragment3, { children: "Microphone ready" }) }),
2549
+ !narrationOn && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
2550
+ /* @__PURE__ */ jsx2(
1638
2551
  "video",
1639
2552
  {
1640
2553
  src: playbackUrl,
@@ -1646,7 +2559,7 @@ function RecorderModal({
1646
2559
  style: { width: "100%", height: "100%", objectFit: "contain" }
1647
2560
  }
1648
2561
  ),
1649
- /* @__PURE__ */ jsxs(
2562
+ /* @__PURE__ */ jsxs2(
1650
2563
  "div",
1651
2564
  {
1652
2565
  role: "timer",
@@ -1660,9 +2573,9 @@ function RecorderModal({
1660
2573
  }
1661
2574
  )
1662
2575
  ] }),
1663
- !narrationOn && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
1664
- /* @__PURE__ */ jsx("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
1665
- /* @__PURE__ */ jsx(
2576
+ !narrationOn && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 12 }, children: [
2577
+ /* @__PURE__ */ jsx2("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
2578
+ /* @__PURE__ */ jsx2(
1666
2579
  "video",
1667
2580
  {
1668
2581
  src: cameraPlaybackUrl,
@@ -1678,16 +2591,16 @@ function RecorderModal({
1678
2591
  }
1679
2592
  )
1680
2593
  ] }),
1681
- !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
1682
- /* @__PURE__ */ jsxs("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
2594
+ !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 12 }, children: [
2595
+ /* @__PURE__ */ jsxs2("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
1683
2596
  "\u2713 Recorded ",
1684
2597
  formatDurationMs(recorder.durationMs)
1685
2598
  ] }),
1686
- /* @__PURE__ */ jsx("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
2599
+ /* @__PURE__ */ jsx2("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
1687
2600
  ] }),
1688
- !narrationOn && source === "mic" && /* @__PURE__ */ jsxs(Fragment2, { children: [
1689
- /* @__PURE__ */ jsx("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
1690
- /* @__PURE__ */ jsx(
2601
+ !narrationOn && source === "mic" && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2602
+ /* @__PURE__ */ jsx2("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
2603
+ /* @__PURE__ */ jsx2(
1691
2604
  "textarea",
1692
2605
  {
1693
2606
  id: "recorder-source-text",
@@ -1699,25 +2612,25 @@ function RecorderModal({
1699
2612
  }
1700
2613
  )
1701
2614
  ] }),
1702
- /* @__PURE__ */ jsx("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
1703
- /* @__PURE__ */ jsx(
2615
+ /* @__PURE__ */ jsx2("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
2616
+ /* @__PURE__ */ jsx2(
1704
2617
  "input",
1705
2618
  {
1706
2619
  id: "recorder-basename",
1707
2620
  type: "text",
1708
- style: inputStyle,
2621
+ style: inputStyle2,
1709
2622
  placeholder: narrationOn ? "narration" : isDual ? "screen + camera" : filenameSeed,
1710
2623
  value: basename,
1711
2624
  onChange: (e) => setBasename(e.target.value),
1712
2625
  disabled: narrationOn ? !narrationRecorderIdle : recorder.state === "recording"
1713
2626
  }
1714
2627
  ),
1715
- !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs("div", { style: recordingStatusStyle, children: [
2628
+ !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: recordingStatusStyle, children: [
1716
2629
  "\u25CF Recording ",
1717
2630
  formatDurationMs(recorder.durationMs)
1718
2631
  ] }),
1719
- /* @__PURE__ */ jsxs("div", { style: buttonRowStyle, children: [
1720
- /* @__PURE__ */ jsx(
2632
+ /* @__PURE__ */ jsxs2("div", { style: buttonRowStyle, children: [
2633
+ /* @__PURE__ */ jsx2(
1721
2634
  "button",
1722
2635
  {
1723
2636
  type: "button",
@@ -1727,8 +2640,8 @@ function RecorderModal({
1727
2640
  children: "Close"
1728
2641
  }
1729
2642
  ),
1730
- narrationOn && /* @__PURE__ */ jsxs(Fragment2, { children: [
1731
- narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx(
2643
+ narrationOn && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2644
+ narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx2(
1732
2645
  "button",
1733
2646
  {
1734
2647
  type: "button",
@@ -1738,14 +2651,14 @@ function RecorderModal({
1738
2651
  children: narrationRequesting ? "Requesting\u2026" : "Start preview"
1739
2652
  }
1740
2653
  ),
1741
- narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
1742
- /* @__PURE__ */ jsx(
2654
+ narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs2("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
2655
+ /* @__PURE__ */ jsx2(
1743
2656
  "span",
1744
2657
  {
1745
2658
  className: "squisq-recorder-record-dot",
1746
2659
  style: recordDotFrameStyle,
1747
2660
  "aria-hidden": "true",
1748
- children: /* @__PURE__ */ jsx(
2661
+ children: /* @__PURE__ */ jsx2(
1749
2662
  "span",
1750
2663
  {
1751
2664
  className: "squisq-recorder-record-dot-center",
@@ -1756,7 +2669,7 @@ function RecorderModal({
1756
2669
  ),
1757
2670
  "Record"
1758
2671
  ] }),
1759
- (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx(
2672
+ (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx2(
1760
2673
  "button",
1761
2674
  {
1762
2675
  type: "button",
@@ -1765,11 +2678,11 @@ function RecorderModal({
1765
2678
  children: "Stop"
1766
2679
  }
1767
2680
  ),
1768
- stage.recorder.state === "processing" && /* @__PURE__ */ jsx("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
1769
- stage.recorder.state === "saving" && /* @__PURE__ */ jsx("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
1770
- stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs(Fragment2, { children: [
1771
- /* @__PURE__ */ jsx("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
1772
- /* @__PURE__ */ jsx(
2681
+ stage.recorder.state === "processing" && /* @__PURE__ */ jsx2("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
2682
+ stage.recorder.state === "saving" && /* @__PURE__ */ jsx2("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
2683
+ stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2684
+ /* @__PURE__ */ jsx2("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
2685
+ /* @__PURE__ */ jsx2(
1773
2686
  "button",
1774
2687
  {
1775
2688
  type: "button",
@@ -1780,8 +2693,8 @@ function RecorderModal({
1780
2693
  )
1781
2694
  ] })
1782
2695
  ] }),
1783
- !narrationOn && /* @__PURE__ */ jsxs(Fragment2, { children: [
1784
- (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx(
2696
+ !narrationOn && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2697
+ (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx2(
1785
2698
  "button",
1786
2699
  {
1787
2700
  type: "button",
@@ -1791,14 +2704,14 @@ function RecorderModal({
1791
2704
  children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
1792
2705
  }
1793
2706
  ),
1794
- canRecord && /* @__PURE__ */ jsxs("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
1795
- /* @__PURE__ */ jsx(
2707
+ canRecord && /* @__PURE__ */ jsxs2("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
2708
+ /* @__PURE__ */ jsx2(
1796
2709
  "span",
1797
2710
  {
1798
2711
  className: "squisq-recorder-record-dot",
1799
2712
  style: recordDotFrameStyle,
1800
2713
  "aria-hidden": "true",
1801
- children: /* @__PURE__ */ jsx(
2714
+ children: /* @__PURE__ */ jsx2(
1802
2715
  "span",
1803
2716
  {
1804
2717
  className: "squisq-recorder-record-dot-center",
@@ -1809,9 +2722,9 @@ function RecorderModal({
1809
2722
  ),
1810
2723
  "Record"
1811
2724
  ] }),
1812
- canStop && /* @__PURE__ */ jsx("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
1813
- canSave && /* @__PURE__ */ jsxs(Fragment2, { children: [
1814
- /* @__PURE__ */ jsx(
2725
+ canStop && /* @__PURE__ */ jsx2("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
2726
+ canSave && /* @__PURE__ */ jsxs2(Fragment3, { children: [
2727
+ /* @__PURE__ */ jsx2(
1815
2728
  "button",
1816
2729
  {
1817
2730
  type: "button",
@@ -1821,7 +2734,7 @@ function RecorderModal({
1821
2734
  children: "Discard & re-record"
1822
2735
  }
1823
2736
  ),
1824
- /* @__PURE__ */ jsx(
2737
+ /* @__PURE__ */ jsx2(
1825
2738
  "button",
1826
2739
  {
1827
2740
  type: "button",
@@ -1835,7 +2748,7 @@ function RecorderModal({
1835
2748
  ] })
1836
2749
  ] })
1837
2750
  ] }),
1838
- narrationOn && narration && /* @__PURE__ */ jsx("div", { style: rightColStyle, children: /* @__PURE__ */ jsx(
2751
+ narrationOn && narration && /* @__PURE__ */ jsx2("div", { style: rightColStyle, children: /* @__PURE__ */ jsx2(
1839
2752
  NarrationStage,
1840
2753
  {
1841
2754
  stage,
@@ -1856,9 +2769,9 @@ function RecorderModal({
1856
2769
  }
1857
2770
 
1858
2771
  // src/recorder/RecorderPanel.tsx
1859
- import { useCallback as useCallback3, useState as useState3 } from "react";
2772
+ import { useCallback as useCallback4, useState as useState4 } from "react";
1860
2773
  import { createPortal } from "react-dom";
1861
- import { Fragment as Fragment3, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2774
+ import { Fragment as Fragment4, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1862
2775
  function RecorderPanel({
1863
2776
  mediaProvider,
1864
2777
  container = null,
@@ -1872,18 +2785,18 @@ function RecorderPanel({
1872
2785
  onOpenChange,
1873
2786
  showTrigger = true
1874
2787
  }) {
1875
- const [uncontrolledOpen, setUncontrolledOpen] = useState3(false);
2788
+ const [uncontrolledOpen, setUncontrolledOpen] = useState4(false);
1876
2789
  const open = controlledOpen ?? uncontrolledOpen;
1877
- const setOpen = useCallback3(
2790
+ const setOpen = useCallback4(
1878
2791
  (nextOpen) => {
1879
2792
  if (controlledOpen === void 0) setUncontrolledOpen(nextOpen);
1880
2793
  onOpenChange?.(nextOpen);
1881
2794
  },
1882
2795
  [controlledOpen, onOpenChange]
1883
2796
  );
1884
- const handleClose = useCallback3(() => setOpen(false), [setOpen]);
1885
- return /* @__PURE__ */ jsxs2(Fragment3, { children: [
1886
- showTrigger && /* @__PURE__ */ jsx2(
2797
+ const handleClose = useCallback4(() => setOpen(false), [setOpen]);
2798
+ return /* @__PURE__ */ jsxs3(Fragment4, { children: [
2799
+ showTrigger && /* @__PURE__ */ jsx3(
1887
2800
  "button",
1888
2801
  {
1889
2802
  type: "button",
@@ -1892,11 +2805,11 @@ function RecorderPanel({
1892
2805
  "aria-label": tooltip,
1893
2806
  "aria-expanded": open,
1894
2807
  onClick: () => setOpen(!open),
1895
- children: /* @__PURE__ */ jsx2(Icon, { icon: "fa-solid fa-microphone" })
2808
+ children: /* @__PURE__ */ jsx3(Icon, { icon: "fa-solid fa-microphone" })
1896
2809
  }
1897
2810
  ),
1898
2811
  open && typeof document !== "undefined" && createPortal(
1899
- /* @__PURE__ */ jsx2(
2812
+ /* @__PURE__ */ jsx3(
1900
2813
  RecorderModal,
1901
2814
  {
1902
2815
  mediaProvider,
@@ -1919,6 +2832,13 @@ export {
1919
2832
  requestScreenStream,
1920
2833
  getCaptureKind,
1921
2834
  useMediaRecorder,
2835
+ RecorderDeviceSettingsPanel,
2836
+ DEFAULT_RECORDER_DEVICE_SETTINGS,
2837
+ buildRecorderAudioConstraints,
2838
+ buildRecorderCameraConstraints,
2839
+ buildRecorderScreenConstraints,
2840
+ buildRecorderScreenAudioConstraints,
2841
+ recorderBitsPerSecond,
1922
2842
  useModalDialog,
1923
2843
  RecorderModal,
1924
2844
  RecorderPanel