@stream-io/video-react-sdk 1.20.2 → 1.21.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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.21.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-sdk-1.20.2...@stream-io/video-react-sdk-1.21.0) (2025-09-09)
6
+
7
+ ### Dependency Updates
8
+
9
+ - `@stream-io/video-client` updated to version `1.29.0`
10
+ - `@stream-io/video-react-bindings` updated to version `1.8.0`
11
+
12
+ ### Features
13
+
14
+ - opt-out from optimistic updates ([#1904](https://github.com/GetStream/stream-video-js/issues/1904)) ([45dba34](https://github.com/GetStream/stream-video-js/commit/45dba34d38dc64f456e37b593e38e420426529f5))
15
+
16
+ ### Bug Fixes
17
+
18
+ - capabilities and call grants ([#1899](https://github.com/GetStream/stream-video-js/issues/1899)) ([5725dfa](https://github.com/GetStream/stream-video-js/commit/5725dfa29b1e5fdb6fe4e26825ce7b268664d2fa))
19
+ - **LivestreamLayout:** handle enter/exit fullscreen gracefully ([#1916](https://github.com/GetStream/stream-video-js/issues/1916)) ([7dd2a0b](https://github.com/GetStream/stream-video-js/commit/7dd2a0b74d9767aae8463fb665a14b944e6cb204)), closes [#1915](https://github.com/GetStream/stream-video-js/issues/1915)
20
+
5
21
  ## [1.20.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-sdk-1.20.1...@stream-io/video-react-sdk-1.20.2) (2025-09-02)
6
22
 
7
23
  ### Dependency Updates
package/dist/index.cjs.js CHANGED
@@ -1072,11 +1072,13 @@ const PermissionNotification = (props) => {
1072
1072
  const prevHasPermission = react.useRef(hasPermission);
1073
1073
  const [showNotification, setShowNotification] = react.useState();
1074
1074
  react.useEffect(() => {
1075
- if (hasPermission && !prevHasPermission.current) {
1075
+ if (prevHasPermission.current === hasPermission)
1076
+ return;
1077
+ if (hasPermission) {
1076
1078
  setShowNotification('granted');
1077
1079
  prevHasPermission.current = true;
1078
1080
  }
1079
- else if (!hasPermission && prevHasPermission.current) {
1081
+ else {
1080
1082
  setShowNotification('revoked');
1081
1083
  prevHasPermission.current = false;
1082
1084
  }
@@ -1301,16 +1303,19 @@ function isNotAllowedError(error) {
1301
1303
 
1302
1304
  const ScreenShareButton = (props) => {
1303
1305
  const { t } = videoReactBindings.useI18n();
1304
- const { caption } = props;
1306
+ const { caption, optimisticUpdates } = props;
1305
1307
  const { useHasOngoingScreenShare, useScreenShareState, useCallSettings } = videoReactBindings.useCallStateHooks();
1306
1308
  const isSomeoneScreenSharing = useHasOngoingScreenShare();
1307
1309
  const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(videoClient.OwnCapability.SCREENSHARE);
1308
1310
  const callSettings = useCallSettings();
1309
1311
  const isScreenSharingAllowed = callSettings?.screensharing.enabled;
1310
- const { screenShare, optimisticIsMute } = useScreenShareState();
1311
- const amIScreenSharing = !optimisticIsMute;
1312
- const disableScreenShareButton = !amIScreenSharing &&
1313
- (isSomeoneScreenSharing || isScreenSharingAllowed === false);
1312
+ const { screenShare, optionsAwareIsMute, isTogglePending } = useScreenShareState({
1313
+ optimisticUpdates,
1314
+ });
1315
+ const amIScreenSharing = !optionsAwareIsMute;
1316
+ const disableScreenShareButton = (!amIScreenSharing &&
1317
+ (isSomeoneScreenSharing || isScreenSharingAllowed === false)) ||
1318
+ (!optimisticUpdates && isTogglePending);
1314
1319
  const handleClick = createCallControlHandler(props, async () => {
1315
1320
  if (!hasPermission) {
1316
1321
  await requestPermission();
@@ -1474,27 +1479,27 @@ const ToggleDeviceSettingsMenuButton = react.forwardRef(function ToggleDeviceSet
1474
1479
  });
1475
1480
 
1476
1481
  const ToggleAudioPreviewButton = (props) => {
1477
- const { caption, Menu = DeviceSelectorAudioInput, menuPlacement = 'top', onMenuToggle, ...restCompositeButtonProps } = props;
1482
+ const { caption, Menu = DeviceSelectorAudioInput, menuPlacement = 'top', onMenuToggle, optimisticUpdates, ...restCompositeButtonProps } = props;
1478
1483
  const { t } = videoReactBindings.useI18n();
1479
1484
  const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
1480
- const { microphone, optimisticIsMute, hasBrowserPermission, isPromptingPermission, } = useMicrophoneState();
1485
+ const { microphone, hasBrowserPermission, isPromptingPermission, optionsAwareIsMute, isTogglePending, } = useMicrophoneState({ optimisticUpdates });
1481
1486
  const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
1482
1487
  const handleClick = createCallControlHandler(props, () => microphone.toggle());
1483
1488
  return (jsxRuntime.jsx(WithTooltip, { title: !hasBrowserPermission
1484
1489
  ? t('Check your browser audio permissions')
1485
- : (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", disabled: !hasBrowserPermission, "data-testid": optimisticIsMute
1490
+ : (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optionsAwareIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", disabled: !hasBrowserPermission || (!optimisticUpdates && isTogglePending), "data-testid": optionsAwareIsMute
1486
1491
  ? 'preview-audio-unmute-button'
1487
1492
  : 'preview-audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1488
1493
  setTooltipDisabled(shown);
1489
1494
  onMenuToggle?.(shown);
1490
- }, children: [jsxRuntime.jsx(Icon, { icon: !optimisticIsMute ? 'mic' : 'mic-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser audio permissions'), children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }));
1495
+ }, children: [jsxRuntime.jsx(Icon, { icon: !optionsAwareIsMute ? 'mic' : 'mic-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser audio permissions'), children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }));
1491
1496
  };
1492
1497
  const ToggleAudioPublishingButton = (props) => {
1493
1498
  const { t } = videoReactBindings.useI18n();
1494
- const { caption, Menu = jsxRuntime.jsx(DeviceSelectorAudioInput, { visualType: "list" }), menuPlacement = 'top', onMenuToggle, ...restCompositeButtonProps } = props;
1499
+ const { caption, Menu = jsxRuntime.jsx(DeviceSelectorAudioInput, { visualType: "list" }), menuPlacement = 'top', onMenuToggle, optimisticUpdates, ...restCompositeButtonProps } = props;
1495
1500
  const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(videoClient.OwnCapability.SEND_AUDIO);
1496
1501
  const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
1497
- const { microphone, optimisticIsMute, hasBrowserPermission, isPromptingPermission, } = useMicrophoneState();
1502
+ const { microphone, hasBrowserPermission, isPromptingPermission, isTogglePending, optionsAwareIsMute, } = useMicrophoneState({ optimisticUpdates });
1498
1503
  const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
1499
1504
  const handleClick = createCallControlHandler(props, async () => {
1500
1505
  if (!hasPermission) {
@@ -1508,34 +1513,37 @@ const ToggleAudioPublishingButton = (props) => {
1508
1513
  ? t('You have no permission to share your audio')
1509
1514
  : !hasBrowserPermission
1510
1515
  ? t('Check your browser mic permissions')
1511
- : (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission || !hasPermission, "data-testid": optimisticIsMute ? 'audio-unmute-button' : 'audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1516
+ : (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optionsAwareIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission ||
1517
+ !hasPermission ||
1518
+ // disable button while the toggle action is pending when not using optimistic updates
1519
+ (!optimisticUpdates && isTogglePending), "data-testid": optionsAwareIsMute ? 'audio-unmute-button' : 'audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1512
1520
  setTooltipDisabled(shown);
1513
1521
  onMenuToggle?.(shown);
1514
- }, children: [jsxRuntime.jsx(Icon, { icon: optimisticIsMute ? 'mic-off' : 'mic' }), (!hasBrowserPermission || !hasPermission) && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }) }) }));
1522
+ }, children: [jsxRuntime.jsx(Icon, { icon: optionsAwareIsMute ? 'mic-off' : 'mic' }), (!hasBrowserPermission || !hasPermission) && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }) }) }));
1515
1523
  };
1516
1524
 
1517
1525
  const ToggleVideoPreviewButton = (props) => {
1518
- const { caption, Menu = DeviceSelectorVideo, menuPlacement = 'top', onMenuToggle, ...restCompositeButtonProps } = props;
1526
+ const { caption, Menu = DeviceSelectorVideo, menuPlacement = 'top', onMenuToggle, optimisticUpdates, ...restCompositeButtonProps } = props;
1519
1527
  const { t } = videoReactBindings.useI18n();
1520
1528
  const { useCameraState } = videoReactBindings.useCallStateHooks();
1521
- const { camera, optimisticIsMute, hasBrowserPermission, isPromptingPermission, } = useCameraState();
1529
+ const { camera, hasBrowserPermission, isPromptingPermission, isTogglePending, optionsAwareIsMute, } = useCameraState({ optimisticUpdates });
1522
1530
  const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
1523
1531
  const handleClick = createCallControlHandler(props, () => camera.toggle());
1524
1532
  return (jsxRuntime.jsx(WithTooltip, { title: !hasBrowserPermission
1525
1533
  ? t('Check your browser video permissions')
1526
- : (caption ?? t('Video')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", "data-testid": optimisticIsMute
1534
+ : (caption ?? t('Video')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optionsAwareIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", "data-testid": optionsAwareIsMute
1527
1535
  ? 'preview-video-unmute-button'
1528
- : 'preview-video-mute-button', onClick: handleClick, disabled: !hasBrowserPermission, Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1536
+ : 'preview-video-mute-button', onClick: handleClick, disabled: !hasBrowserPermission || (!optimisticUpdates && isTogglePending), Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1529
1537
  setTooltipDisabled(shown);
1530
1538
  onMenuToggle?.(shown);
1531
- }, children: [jsxRuntime.jsx(Icon, { icon: !optimisticIsMute ? 'camera' : 'camera-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser video permissions'), children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }));
1539
+ }, children: [jsxRuntime.jsx(Icon, { icon: !optionsAwareIsMute ? 'camera' : 'camera-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser video permissions'), children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }));
1532
1540
  };
1533
1541
  const ToggleVideoPublishingButton = (props) => {
1534
1542
  const { t } = videoReactBindings.useI18n();
1535
- const { caption, Menu = jsxRuntime.jsx(DeviceSelectorVideo, { visualType: "list" }), menuPlacement = 'top', onMenuToggle, ...restCompositeButtonProps } = props;
1543
+ const { caption, Menu = jsxRuntime.jsx(DeviceSelectorVideo, { visualType: "list" }), menuPlacement = 'top', onMenuToggle, optimisticUpdates, ...restCompositeButtonProps } = props;
1536
1544
  const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(videoClient.OwnCapability.SEND_VIDEO);
1537
1545
  const { useCameraState, useCallSettings } = videoReactBindings.useCallStateHooks();
1538
- const { camera, optimisticIsMute, hasBrowserPermission, isPromptingPermission, } = useCameraState();
1546
+ const { camera, optionsAwareIsMute, hasBrowserPermission, isPromptingPermission, isTogglePending, } = useCameraState({ optimisticUpdates });
1539
1547
  const callSettings = useCallSettings();
1540
1548
  const isPublishingVideoAllowed = callSettings?.video.enabled;
1541
1549
  const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
@@ -1553,12 +1561,13 @@ const ToggleVideoPublishingButton = (props) => {
1553
1561
  ? t('Check your browser video permissions')
1554
1562
  : !isPublishingVideoAllowed
1555
1563
  ? t('Video publishing is disabled by the system')
1556
- : caption || t('Video'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission ||
1564
+ : caption || t('Video'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optionsAwareIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission ||
1557
1565
  !hasPermission ||
1558
- !isPublishingVideoAllowed, "data-testid": optimisticIsMute ? 'video-unmute-button' : 'video-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1566
+ !isPublishingVideoAllowed ||
1567
+ (!optimisticUpdates && isTogglePending), "data-testid": optionsAwareIsMute ? 'video-unmute-button' : 'video-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1559
1568
  setTooltipDisabled(shown);
1560
1569
  onMenuToggle?.(shown);
1561
- }, children: [jsxRuntime.jsx(Icon, { icon: optimisticIsMute ? 'camera-off' : 'camera' }), (!hasBrowserPermission ||
1570
+ }, children: [jsxRuntime.jsx(Icon, { icon: optionsAwareIsMute ? 'camera-off' : 'camera' }), (!hasBrowserPermission ||
1562
1571
  !hasPermission ||
1563
1572
  !isPublishingVideoAllowed) && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }) }) }));
1564
1573
  };
@@ -2648,16 +2657,23 @@ const useUpdateCallDuration = () => {
2648
2657
  };
2649
2658
  const useToggleFullScreen = () => {
2650
2659
  const { participantViewElement } = useParticipantViewContext();
2651
- const [isFullscreen, setIsFullscreen] = react.useState(false);
2660
+ const [isFullscreen, setIsFullscreen] = react.useState(!!document.fullscreenElement);
2661
+ react.useEffect(() => {
2662
+ const handler = () => setIsFullscreen(!!document.fullscreenElement);
2663
+ document.addEventListener('fullscreenchange', handler);
2664
+ return () => {
2665
+ document.removeEventListener('fullscreenchange', handler);
2666
+ };
2667
+ }, []);
2652
2668
  return react.useCallback(() => {
2653
2669
  if (isFullscreen) {
2654
- document.exitFullscreen().then(() => {
2655
- setIsFullscreen(false);
2670
+ document.exitFullscreen().catch((err) => {
2671
+ console.error('Failed to exit fullscreen', err);
2656
2672
  });
2657
2673
  }
2658
2674
  else {
2659
- participantViewElement?.requestFullscreen().then(() => {
2660
- setIsFullscreen(true);
2675
+ participantViewElement?.requestFullscreen().catch((err) => {
2676
+ console.error('Failed to enter fullscreen', err);
2661
2677
  });
2662
2678
  }
2663
2679
  }, [isFullscreen, participantViewElement]);
@@ -2954,7 +2970,7 @@ const checkCanJoinEarly = (startsAt, joinAheadTimeSeconds) => {
2954
2970
  return Date.now() >= +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
2955
2971
  };
2956
2972
 
2957
- const [major, minor, patch] = ("1.20.2").split('.');
2973
+ const [major, minor, patch] = ("1.21.0").split('.');
2958
2974
  videoClient.setSdkInfo({
2959
2975
  type: videoClient.SfuModels.SdkType.REACT,
2960
2976
  major,