@stream-io/video-react-sdk 1.34.1 → 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.
package/dist/index.es.js CHANGED
@@ -1232,6 +1232,41 @@ const AudioVolumeIndicator = () => {
1232
1232
  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})` } }) })] }));
1233
1233
  };
1234
1234
 
1235
+ const LEVEL_BARS = 5;
1236
+ const DeviceLevelIndicator = ({ deviceId }) => {
1237
+ const [audioLevel, setAudioLevel] = useState(0);
1238
+ useEffect(() => {
1239
+ let cancelled = false;
1240
+ let dispose;
1241
+ navigator.mediaDevices
1242
+ .getUserMedia({
1243
+ audio: { deviceId: { exact: deviceId } },
1244
+ video: false,
1245
+ })
1246
+ .then((mediaStream) => {
1247
+ if (cancelled) {
1248
+ mediaStream.getTracks().forEach((t) => t.stop());
1249
+ return;
1250
+ }
1251
+ dispose = createSoundDetector(mediaStream, ({ audioLevel: al }) => setAudioLevel(al), { detectionFrequencyInMs: 80 });
1252
+ })
1253
+ .catch(console.error);
1254
+ return () => {
1255
+ cancelled = true;
1256
+ dispose?.().catch(console.error);
1257
+ };
1258
+ }, [deviceId]);
1259
+ const activeBars = Math.round((audioLevel / 100) * LEVEL_BARS);
1260
+ 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', {
1261
+ 'str-video__device-level-indicator__bar--active': i < activeBars,
1262
+ }) }, i))) }));
1263
+ };
1264
+ const DeviceAudioPreviewItem = ({ device, onSelect, }) => {
1265
+ if (device.deviceId === 'default')
1266
+ return null;
1267
+ 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 })] }));
1268
+ };
1269
+
1235
1270
  const SelectContext = createContext({});
1236
1271
  const Select = (props) => {
1237
1272
  const { children, icon, defaultSelectedLabel, defaultSelectedIndex, handleSelect: handleSelectProp, } = props;
@@ -1323,6 +1358,18 @@ const DeviceSelectorList = (props) => {
1323
1358
  }, name: type, selected: device.isSelected }, device.deviceId));
1324
1359
  }), children] }));
1325
1360
  };
1361
+ const DeviceSelectorPreview = (props) => {
1362
+ const { devices = [], selectedDeviceId, title, onChange, children, PreviewItem, } = props;
1363
+ const { close } = useMenuContext();
1364
+ const { deviceList } = useDeviceList(devices, selectedDeviceId);
1365
+ const onSelect = useCallback((deviceId) => {
1366
+ if (deviceId === 'default')
1367
+ return;
1368
+ onChange?.(deviceId);
1369
+ close?.();
1370
+ }, [onChange, close]);
1371
+ 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] }));
1372
+ };
1326
1373
  const DeviceSelectorDropdown = (props) => {
1327
1374
  const { devices = [], selectedDeviceId, title, onChange, icon } = props;
1328
1375
  const { deviceList, selectedDeviceInfo, selectedIndex } = useDeviceList(devices, selectedDeviceId);
@@ -1335,6 +1382,10 @@ const DeviceSelectorDropdown = (props) => {
1335
1382
  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))) })] }));
1336
1383
  };
1337
1384
  const DeviceSelector = (props) => {
1385
+ if (props.visualType === 'preview') {
1386
+ const { PreviewItem, ...rest } = props;
1387
+ return jsx(DeviceSelectorPreview, { ...rest, PreviewItem: PreviewItem });
1388
+ }
1338
1389
  const { visualType = 'list', icon, ...rest } = props;
1339
1390
  if (visualType === 'list') {
1340
1391
  return jsx(DeviceSelectorList, { ...rest });
@@ -1352,7 +1403,7 @@ const SpeakerTest = (props) => {
1352
1403
  const audioElementRef = useRef(null);
1353
1404
  const [isPlaying, setIsPlaying] = useState(false);
1354
1405
  const { t } = useI18n();
1355
- const { audioUrl = `https://unpkg.com/${"@stream-io/video-react-sdk"}@${"1.34.1"}/assets/piano.mp3`, } = props;
1406
+ const { audioUrl = `https://unpkg.com/${"@stream-io/video-react-sdk"}@${"1.35.0"}/assets/piano.mp3`, } = props;
1356
1407
  // Update audio output device when selection changes
1357
1408
  useEffect(() => {
1358
1409
  const audio = audioElementRef.current;
@@ -1395,7 +1446,7 @@ const DeviceSelectorAudioInput = ({ title, visualType, volumeIndicatorVisible =
1395
1446
  const { microphone, selectedDevice, devices } = useMicrophoneState();
1396
1447
  return (jsx(DeviceSelector, { devices: devices || [], selectedDeviceId: selectedDevice, type: "audioinput", onChange: async (deviceId) => {
1397
1448
  await microphone.select(deviceId);
1398
- }, title: title, visualType: visualType, icon: "mic", children: volumeIndicatorVisible && (jsxs(Fragment, { children: [jsx("hr", { className: "str-video__device-settings__separator" }), jsx(AudioVolumeIndicator, {})] })) }));
1449
+ }, title: title, visualType: visualType, icon: "mic", PreviewItem: DeviceAudioPreviewItem, children: volumeIndicatorVisible && (jsxs(Fragment, { children: [jsx("hr", { className: "str-video__device-settings__separator" }), jsx(AudioVolumeIndicator, {})] })) }));
1399
1450
  };
1400
1451
  const DeviceSelectorAudioOutput = ({ title, visualType, speakerTestVisible = true, speakerTestAudioUrl, }) => {
1401
1452
  const { useSpeakerState } = useCallStateHooks();
@@ -1407,12 +1458,48 @@ const DeviceSelectorAudioOutput = ({ title, visualType, speakerTestVisible = tru
1407
1458
  }, title: title, visualType: visualType, icon: "speaker", children: speakerTestVisible && (jsxs(Fragment, { children: [jsx("hr", { className: "str-video__device-settings__separator" }), jsx(SpeakerTest, { audioUrl: speakerTestAudioUrl })] })) }));
1408
1459
  };
1409
1460
 
1461
+ const DeviceVideoPreview = ({ deviceId }) => {
1462
+ const videoRef = useRef(null);
1463
+ useEffect(() => {
1464
+ let cancelled = false;
1465
+ let stream;
1466
+ navigator.mediaDevices
1467
+ .getUserMedia({
1468
+ video: { deviceId: { exact: deviceId } },
1469
+ audio: false,
1470
+ })
1471
+ .then((mediaStream) => {
1472
+ if (cancelled) {
1473
+ mediaStream.getTracks().forEach((t) => t.stop());
1474
+ return;
1475
+ }
1476
+ stream = mediaStream;
1477
+ if (videoRef.current) {
1478
+ videoRef.current.srcObject = mediaStream;
1479
+ }
1480
+ })
1481
+ .catch(console.error);
1482
+ return () => {
1483
+ cancelled = true;
1484
+ stream?.getTracks().forEach((t) => t.stop());
1485
+ };
1486
+ }, [deviceId]);
1487
+ 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" }) }));
1488
+ };
1489
+ const DeviceVideoPreviewItem = ({ device, onSelect, }) => {
1490
+ if (device.deviceId === 'default')
1491
+ return null;
1492
+ return (jsxs("button", { type: "button", className: clsx('str-video__device-preview', {
1493
+ 'str-video__device-preview--selected': device.isSelected,
1494
+ }), 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 })] }));
1495
+ };
1496
+
1410
1497
  const DeviceSelectorVideo = ({ title, visualType, }) => {
1411
1498
  const { useCameraState } = useCallStateHooks();
1412
1499
  const { camera, devices, selectedDevice } = useCameraState();
1413
1500
  return (jsx(DeviceSelector, { devices: devices || [], type: "videoinput", selectedDeviceId: selectedDevice, onChange: async (deviceId) => {
1414
1501
  await camera.select(deviceId);
1415
- }, title: title, visualType: visualType, icon: "camera" }));
1502
+ }, title: title, visualType: visualType, icon: "camera", PreviewItem: DeviceVideoPreviewItem }));
1416
1503
  };
1417
1504
 
1418
1505
  const DeviceSettings = ({ visualType = MenuVisualType.MENU, }) => {
@@ -2295,9 +2382,7 @@ const ParticipantDetails = ({ indicatorsVisible = true, }) => {
2295
2382
  const isTrackPaused = trackType !== 'none' ? hasPausedTrack(participant, trackType) : false;
2296
2383
  const isAudioTrackUnmuted = useIsTrackUnmuted(participant);
2297
2384
  const isAudioConnecting = hasAudioTrack && !isAudioTrackUnmuted;
2298
- return (jsxs(Fragment, { children: [jsx("div", { className: "str-video__participant-details", children: jsxs("div", { className: "str-video__participant-details__name", children: [name || userId, indicatorsVisible && isAudioConnecting && (jsx(LoadingIndicator, { className: "str-video__participant-details__name--audio-connecting", tooltip: t('Audio is connecting...') })), indicatorsVisible && !hasAudioTrack && (jsx("span", { className: "str-video__participant-details__name--audio-muted" })), indicatorsVisible && !hasVideoTrack && (jsx("span", { className: "str-video__participant-details__name--video-muted" })), indicatorsVisible && isTrackPaused && (jsx("span", { title: t('Video paused due to insufficient bandwidth'), className: "str-video__participant-details__name--track-paused" })), indicatorsVisible && canUnpin && (
2299
- // TODO: remove this monstrosity once we have a proper design
2300
- jsx("span", { title: t('Unpin'), onClick: () => call?.unpin(sessionId), className: "str-video__participant-details__name--pinned" })), indicatorsVisible && jsx(SpeechIndicator, {})] }) }), indicatorsVisible && (jsx(Notification, { isVisible: isLocalParticipant &&
2385
+ return (jsxs(Fragment, { children: [jsx("div", { className: "str-video__participant-details", children: jsxs("div", { className: "str-video__participant-details__name", children: [name || userId, indicatorsVisible && isAudioConnecting && (jsx(LoadingIndicator, { className: "str-video__participant-details__name--audio-connecting", tooltip: t('Audio is connecting...') })), indicatorsVisible && !hasAudioTrack && (jsx("span", { className: "str-video__participant-details__name--audio-muted" })), indicatorsVisible && !hasVideoTrack && (jsx("span", { className: "str-video__participant-details__name--video-muted" })), indicatorsVisible && isTrackPaused && (jsx("span", { title: t('Video paused due to insufficient bandwidth'), className: "str-video__participant-details__name--track-paused" })), indicatorsVisible && pin && (jsx("span", { title: canUnpin ? t('Unpin') : t('Pinned'), onClick: canUnpin ? () => call?.unpin(sessionId) : undefined, className: "str-video__participant-details__name--pinned" })), indicatorsVisible && jsx(SpeechIndicator, {})] }) }), indicatorsVisible && (jsx(Notification, { isVisible: isLocalParticipant &&
2301
2386
  connectionQuality === SfuModels.ConnectionQuality.POOR, message: t('Poor connection quality'), children: connectionQualityAsString && (jsx("span", { className: clsx('str-video__participant-details__connection-quality', `str-video__participant-details__connection-quality--${connectionQualityAsString}`), title: connectionQualityAsString })) }))] }));
2302
2387
  };
2303
2388
  const SpeechIndicator = () => {
@@ -3175,7 +3260,7 @@ const checkCanJoinEarly = (startsAt, joinAheadTimeSeconds) => {
3175
3260
  return Date.now() >= +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
3176
3261
  };
3177
3262
 
3178
- const [major, minor, patch] = ("1.34.1").split('.');
3263
+ const [major, minor, patch] = ("1.35.0").split('.');
3179
3264
  setSdkInfo({
3180
3265
  type: SfuModels.SdkType.REACT,
3181
3266
  major,