@stream-io/video-react-sdk 1.34.2 → 1.35.0

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.
@@ -1034,6 +1034,41 @@ const AudioVolumeIndicator = () => {
1034
1034
  return (jsxs("div", { className: "str-video__audio-volume-indicator", children: [jsx(Icon, { icon: isEnabled ? 'mic' : 'mic-off' }), jsx("div", { className: "str-video__audio-volume-indicator__bar", children: jsx("div", { className: "str-video__audio-volume-indicator__bar-value", style: { transform: `scaleX(${audioLevel / 100})` } }) })] }));
1035
1035
  };
1036
1036
 
1037
+ const LEVEL_BARS = 5;
1038
+ const DeviceLevelIndicator = ({ deviceId }) => {
1039
+ const [audioLevel, setAudioLevel] = useState(0);
1040
+ useEffect(() => {
1041
+ let cancelled = false;
1042
+ let dispose;
1043
+ navigator.mediaDevices
1044
+ .getUserMedia({
1045
+ audio: { deviceId: { exact: deviceId } },
1046
+ video: false,
1047
+ })
1048
+ .then((mediaStream) => {
1049
+ if (cancelled) {
1050
+ mediaStream.getTracks().forEach((t) => t.stop());
1051
+ return;
1052
+ }
1053
+ dispose = createSoundDetector(mediaStream, ({ audioLevel: al }) => setAudioLevel(al), { detectionFrequencyInMs: 80 });
1054
+ })
1055
+ .catch(console.error);
1056
+ return () => {
1057
+ cancelled = true;
1058
+ dispose?.().catch(console.error);
1059
+ };
1060
+ }, [deviceId]);
1061
+ const activeBars = Math.round((audioLevel / 100) * LEVEL_BARS);
1062
+ return (jsx("div", { className: "str-video__device-level-indicator", "aria-label": "Audio level", children: Array.from({ length: LEVEL_BARS }, (_, i) => (jsx("div", { className: clsx('str-video__device-level-indicator__bar', {
1063
+ 'str-video__device-level-indicator__bar--active': i < activeBars,
1064
+ }) }, i))) }));
1065
+ };
1066
+ const DeviceAudioPreviewItem = ({ device, onSelect, }) => {
1067
+ if (device.deviceId === 'default')
1068
+ return null;
1069
+ return (jsxs("label", { className: `str-video__device-settings__option${device.isSelected ? ' str-video__device-settings__option--selected' : ''}`, htmlFor: `audioinput--${device.deviceId}`, children: [jsx("input", { type: "radio", name: "audioinput", value: device.deviceId, id: `audioinput--${device.deviceId}`, checked: device.isSelected, onChange: (e) => onSelect(e.target.value) }), device.label, jsx(DeviceLevelIndicator, { deviceId: device.deviceId })] }));
1070
+ };
1071
+
1037
1072
  const SelectContext = createContext({});
1038
1073
  const Select = (props) => {
1039
1074
  const { children, icon, defaultSelectedLabel, defaultSelectedIndex, handleSelect: handleSelectProp, } = props;
@@ -1125,6 +1160,18 @@ const DeviceSelectorList = (props) => {
1125
1160
  }, name: type, selected: device.isSelected }, device.deviceId));
1126
1161
  }), children] }));
1127
1162
  };
1163
+ const DeviceSelectorPreview = (props) => {
1164
+ const { devices = [], selectedDeviceId, title, onChange, children, PreviewItem, } = props;
1165
+ const { close } = useMenuContext();
1166
+ const { deviceList } = useDeviceList(devices, selectedDeviceId);
1167
+ const onSelect = useCallback((deviceId) => {
1168
+ if (deviceId === 'default')
1169
+ return;
1170
+ onChange?.(deviceId);
1171
+ close?.();
1172
+ }, [onChange, close]);
1173
+ return (jsxs("div", { className: "str-video__device-settings__device-kind", children: [title && (jsx("div", { className: "str-video__device-settings__device-selector-title", children: title })), deviceList.map((device) => (jsx(PreviewItem, { device: device, onSelect: onSelect }, device.deviceId))), children] }));
1174
+ };
1128
1175
  const DeviceSelectorDropdown = (props) => {
1129
1176
  const { devices = [], selectedDeviceId, title, onChange, icon } = props;
1130
1177
  const { deviceList, selectedDeviceInfo, selectedIndex } = useDeviceList(devices, selectedDeviceId);
@@ -1137,6 +1184,10 @@ const DeviceSelectorDropdown = (props) => {
1137
1184
  return (jsxs("div", { className: "str-video__device-settings__device-kind", children: [jsx("div", { className: "str-video__device-settings__device-selector-title", children: title }), jsx(DropDownSelect, { icon: icon, defaultSelectedIndex: selectedIndex, defaultSelectedLabel: selectedDeviceInfo.label, handleSelect: handleSelect, children: deviceList.map((device) => (jsx(DropDownSelectOption, { icon: icon, label: device.label, selected: device.isSelected }, device.deviceId))) })] }));
1138
1185
  };
1139
1186
  const DeviceSelector = (props) => {
1187
+ if (props.visualType === 'preview') {
1188
+ const { PreviewItem, ...rest } = props;
1189
+ return jsx(DeviceSelectorPreview, { ...rest, PreviewItem: PreviewItem });
1190
+ }
1140
1191
  const { visualType = 'list', icon, ...rest } = props;
1141
1192
  if (visualType === 'list') {
1142
1193
  return jsx(DeviceSelectorList, { ...rest });
@@ -1154,7 +1205,7 @@ const SpeakerTest = (props) => {
1154
1205
  const audioElementRef = useRef(null);
1155
1206
  const [isPlaying, setIsPlaying] = useState(false);
1156
1207
  const { t } = useI18n();
1157
- const { audioUrl = `https://unpkg.com/${"@stream-io/video-react-sdk"}@${"1.34.2"}/assets/piano.mp3`, } = props;
1208
+ const { audioUrl = `https://unpkg.com/${"@stream-io/video-react-sdk"}@${"1.35.0"}/assets/piano.mp3`, } = props;
1158
1209
  // Update audio output device when selection changes
1159
1210
  useEffect(() => {
1160
1211
  const audio = audioElementRef.current;
@@ -1197,7 +1248,7 @@ const DeviceSelectorAudioInput = ({ title, visualType, volumeIndicatorVisible =
1197
1248
  const { microphone, selectedDevice, devices } = useMicrophoneState();
1198
1249
  return (jsx(DeviceSelector, { devices: devices || [], selectedDeviceId: selectedDevice, type: "audioinput", onChange: async (deviceId) => {
1199
1250
  await microphone.select(deviceId);
1200
- }, title: title, visualType: visualType, icon: "mic", children: volumeIndicatorVisible && (jsxs(Fragment, { children: [jsx("hr", { className: "str-video__device-settings__separator" }), jsx(AudioVolumeIndicator, {})] })) }));
1251
+ }, title: title, visualType: visualType, icon: "mic", PreviewItem: DeviceAudioPreviewItem, children: volumeIndicatorVisible && (jsxs(Fragment, { children: [jsx("hr", { className: "str-video__device-settings__separator" }), jsx(AudioVolumeIndicator, {})] })) }));
1201
1252
  };
1202
1253
  const DeviceSelectorAudioOutput = ({ title, visualType, speakerTestVisible = true, speakerTestAudioUrl, }) => {
1203
1254
  const { useSpeakerState } = useCallStateHooks();
@@ -1209,12 +1260,48 @@ const DeviceSelectorAudioOutput = ({ title, visualType, speakerTestVisible = tru
1209
1260
  }, title: title, visualType: visualType, icon: "speaker", children: speakerTestVisible && (jsxs(Fragment, { children: [jsx("hr", { className: "str-video__device-settings__separator" }), jsx(SpeakerTest, { audioUrl: speakerTestAudioUrl })] })) }));
1210
1261
  };
1211
1262
 
1263
+ const DeviceVideoPreview = ({ deviceId }) => {
1264
+ const videoRef = useRef(null);
1265
+ useEffect(() => {
1266
+ let cancelled = false;
1267
+ let stream;
1268
+ navigator.mediaDevices
1269
+ .getUserMedia({
1270
+ video: { deviceId: { exact: deviceId } },
1271
+ audio: false,
1272
+ })
1273
+ .then((mediaStream) => {
1274
+ if (cancelled) {
1275
+ mediaStream.getTracks().forEach((t) => t.stop());
1276
+ return;
1277
+ }
1278
+ stream = mediaStream;
1279
+ if (videoRef.current) {
1280
+ videoRef.current.srcObject = mediaStream;
1281
+ }
1282
+ })
1283
+ .catch(console.error);
1284
+ return () => {
1285
+ cancelled = true;
1286
+ stream?.getTracks().forEach((t) => t.stop());
1287
+ };
1288
+ }, [deviceId]);
1289
+ return (jsx("div", { className: "str-video__device-video-preview", children: jsx("video", { ref: videoRef, autoPlay: true, playsInline: true, muted: true, className: "str-video__device-video-preview__video" }) }));
1290
+ };
1291
+ const DeviceVideoPreviewItem = ({ device, onSelect, }) => {
1292
+ if (device.deviceId === 'default')
1293
+ return null;
1294
+ return (jsxs("button", { type: "button", className: clsx('str-video__device-preview', {
1295
+ 'str-video__device-preview--selected': device.isSelected,
1296
+ }), onClick: () => onSelect(device.deviceId), "aria-pressed": device.isSelected, children: [jsx(DeviceVideoPreview, { deviceId: device.deviceId }), jsx("span", { className: "str-video__device-preview__label", children: device.label })] }));
1297
+ };
1298
+
1212
1299
  const DeviceSelectorVideo = ({ title, visualType, }) => {
1213
1300
  const { useCameraState } = useCallStateHooks();
1214
1301
  const { camera, devices, selectedDevice } = useCameraState();
1215
1302
  return (jsx(DeviceSelector, { devices: devices || [], type: "videoinput", selectedDeviceId: selectedDevice, onChange: async (deviceId) => {
1216
1303
  await camera.select(deviceId);
1217
- }, title: title, visualType: visualType, icon: "camera" }));
1304
+ }, title: title, visualType: visualType, icon: "camera", PreviewItem: DeviceVideoPreviewItem }));
1218
1305
  };
1219
1306
 
1220
1307
  forwardRef(function ToggleDeviceSettingsMenuButton({ menuShown }, ref) {