@stream-io/video-react-sdk 1.27.2 → 1.28.1

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.cjs.js CHANGED
@@ -466,6 +466,136 @@ function useDeviceList(devices, selectedDeviceId) {
466
466
  }, [devices, selectedDeviceId, t]);
467
467
  }
468
468
 
469
+ const isFullScreenBlurPlatformSupported = () => {
470
+ if (typeof window === 'undefined' ||
471
+ typeof OffscreenCanvas === 'undefined' ||
472
+ typeof VideoFrame === 'undefined' ||
473
+ !window.WebGL2RenderingContext) {
474
+ return false;
475
+ }
476
+ try {
477
+ const canvas = new OffscreenCanvas(1, 1);
478
+ return !!canvas.getContext('webgl2', {
479
+ alpha: false,
480
+ antialias: false,
481
+ desynchronized: true,
482
+ });
483
+ }
484
+ catch {
485
+ return false;
486
+ }
487
+ };
488
+ const useModeration = (options) => {
489
+ const { duration = 5000 } = options || {};
490
+ const call = videoReactBindings.useCall();
491
+ const timeoutRef = react.useRef(null);
492
+ const processorRef = react.useRef(null);
493
+ const unregisterRef = react.useRef(null);
494
+ const blurModulePromise = react.useRef(null);
495
+ /**
496
+ * Lazily loads and caches the video-filters-web module.
497
+ */
498
+ const loadVideoFiltersWebModule = react.useCallback(() => {
499
+ if (!blurModulePromise.current) {
500
+ blurModulePromise.current = import('@stream-io/video-filters-web')
501
+ .then((module) => module.FullScreenBlur)
502
+ .catch((error) => {
503
+ console.error('[moderation] Failed to import blur module:', error);
504
+ throw error;
505
+ });
506
+ }
507
+ return blurModulePromise.current;
508
+ }, []);
509
+ const disableBlur = react.useCallback(() => {
510
+ if (timeoutRef.current) {
511
+ clearTimeout(timeoutRef.current);
512
+ timeoutRef.current = null;
513
+ }
514
+ unregisterRef
515
+ .current?.()
516
+ .catch((err) => console.error('[moderation] unregister error:', err));
517
+ unregisterRef.current = null;
518
+ }, []);
519
+ const handleFallback = react.useCallback(async () => {
520
+ try {
521
+ await call?.camera.disable();
522
+ }
523
+ catch (error) {
524
+ console.error('[moderation] Failed to disable camera:', error);
525
+ }
526
+ }, [call]);
527
+ react.useEffect(() => {
528
+ if (!call)
529
+ return;
530
+ return call.on('call.moderation_warning', async () => {
531
+ try {
532
+ await loadVideoFiltersWebModule();
533
+ }
534
+ catch (importErr) {
535
+ console.error('[moderation] Failed to import blur module:', importErr);
536
+ }
537
+ });
538
+ }, [call, loadVideoFiltersWebModule]);
539
+ react.useEffect(() => {
540
+ if (!call)
541
+ return;
542
+ return call.on('call.moderation_blur', async () => {
543
+ if (unregisterRef.current)
544
+ return;
545
+ let FullScreenBlurClass;
546
+ try {
547
+ FullScreenBlurClass = await loadVideoFiltersWebModule();
548
+ }
549
+ catch (importErr) {
550
+ console.error('[moderation] Failed to import blur module:', importErr);
551
+ await handleFallback();
552
+ return;
553
+ }
554
+ if (!isFullScreenBlurPlatformSupported()) {
555
+ console.warn('[moderation] Blur not supported on this platform');
556
+ await handleFallback();
557
+ return;
558
+ }
559
+ const { unregister } = call.camera.registerFilter((inputStream) => {
560
+ unregisterRef.current = unregister;
561
+ const outputPromise = new Promise(async (resolve, reject) => {
562
+ const [track] = inputStream.getVideoTracks();
563
+ let processor;
564
+ try {
565
+ processor = new FullScreenBlurClass(track);
566
+ processorRef.current = processor;
567
+ const result = await processor.start();
568
+ const output = new MediaStream([result]);
569
+ resolve(output);
570
+ if (duration > 0) {
571
+ timeoutRef.current = setTimeout(disableBlur, duration);
572
+ }
573
+ }
574
+ catch (error) {
575
+ reject(error);
576
+ console.error('[moderation] Processor init failed:', error);
577
+ await unregisterRef.current?.();
578
+ unregisterRef.current = null;
579
+ processorRef.current = null;
580
+ await handleFallback();
581
+ return;
582
+ }
583
+ });
584
+ return {
585
+ output: outputPromise,
586
+ stop: () => {
587
+ if (processorRef.current) {
588
+ processorRef.current.stop();
589
+ processorRef.current = null;
590
+ }
591
+ },
592
+ };
593
+ });
594
+ });
595
+ }, [call, loadVideoFiltersWebModule, disableBlur, handleFallback, duration]);
596
+ react.useEffect(() => disableBlur, [disableBlur]);
597
+ };
598
+
469
599
  exports.MenuVisualType = void 0;
470
600
  (function (MenuVisualType) {
471
601
  MenuVisualType["PORTAL"] = "portal";
@@ -1656,7 +1786,7 @@ const SpeakerTest = (props) => {
1656
1786
  const audioElementRef = react.useRef(null);
1657
1787
  const [isPlaying, setIsPlaying] = react.useState(false);
1658
1788
  const { t } = videoReactBindings.useI18n();
1659
- const { audioUrl = `https://unpkg.com/${"@stream-io/video-react-sdk"}@${"1.27.2"}/assets/piano.mp3`, } = props;
1789
+ const { audioUrl = `https://unpkg.com/${"@stream-io/video-react-sdk"}@${"1.28.1"}/assets/piano.mp3`, } = props;
1660
1790
  // Update audio output device when selection changes
1661
1791
  react.useEffect(() => {
1662
1792
  const audio = audioElementRef.current;
@@ -1886,7 +2016,7 @@ var Status;
1886
2016
  Status["BAD"] = "Bad";
1887
2017
  })(Status || (Status = {}));
1888
2018
  const CallStats = (props) => {
1889
- const { latencyLowBound = 75, latencyHighBound = 400, showCodecInfo = false, LatencyChartSuspenseFallback = null, } = props;
2019
+ const { latencyLowBound = 75, latencyHighBound = 400, audioJitterLowBound = 10, audioJitterHighBound = 30, videoJitterLowBound = 20, videoJitterHighBound = 50, showCodecInfo = false, LatencyChartSuspenseFallback = null, } = props;
1890
2020
  const [latencyBuffer, setLatencyBuffer] = react.useState(() => {
1891
2021
  const now = Date.now();
1892
2022
  return Array.from({ length: 20 }, (_, i) => ({ x: now + i, y: 0 }));
@@ -1894,6 +2024,8 @@ const CallStats = (props) => {
1894
2024
  const { t } = videoReactBindings.useI18n();
1895
2025
  const [publishBitrate, setPublishBitrate] = react.useState('-');
1896
2026
  const [subscribeBitrate, setSubscribeBitrate] = react.useState('-');
2027
+ const [publishAudioBitrate, setPublishAudioBitrate] = react.useState('-');
2028
+ const [subscribeAudioBitrate, setSubscribeAudioBitrate] = react.useState('-');
1897
2029
  const previousStats = react.useRef(undefined);
1898
2030
  const { useCallStatsReport } = videoReactBindings.useCallStateHooks();
1899
2031
  const callStatsReport = useCallStatsReport();
@@ -1911,11 +2043,18 @@ const CallStats = (props) => {
1911
2043
  setSubscribeBitrate(() => {
1912
2044
  return calculateSubscribeBitrate(previousCallStatsReport, callStatsReport);
1913
2045
  });
2046
+ setPublishAudioBitrate(() => {
2047
+ return calculatePublishAudioBitrate(previousCallStatsReport, callStatsReport);
2048
+ });
2049
+ setSubscribeAudioBitrate(() => {
2050
+ return calculateSubscribeAudioBitrate(previousCallStatsReport, callStatsReport);
2051
+ });
1914
2052
  setLatencyBuffer((latencyBuf) => {
1915
2053
  const newLatencyBuffer = latencyBuf.slice(-19);
1916
2054
  newLatencyBuffer.push({
1917
2055
  x: callStatsReport.timestamp,
1918
- y: callStatsReport.publisherStats.averageRoundTripTimeInMs,
2056
+ y: callStatsReport.publisherStats.averageRoundTripTimeInMs ||
2057
+ callStatsReport.publisherAudioStats.averageRoundTripTimeInMs,
1919
2058
  });
1920
2059
  return newLatencyBuffer;
1921
2060
  });
@@ -1926,13 +2065,27 @@ const CallStats = (props) => {
1926
2065
  highBound: latencyHighBound,
1927
2066
  value: callStatsReport?.publisherStats.averageRoundTripTimeInMs || 0,
1928
2067
  };
1929
- return (jsxRuntime.jsx("div", { className: "str-video__call-stats", children: callStatsReport && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "str-video__call-stats__header", children: [jsxRuntime.jsxs("h3", { className: "str-video__call-stats__heading", children: [jsxRuntime.jsx(Icon, { className: "str-video__call-stats__icon", icon: "call-latency" }), t('Call Latency')] }), jsxRuntime.jsx("p", { className: "str-video__call-stats__description", children: t('Very high latency values may reduce call quality, cause lag, and make the call less enjoyable.') })] }), jsxRuntime.jsx("div", { className: "str-video__call-stats__latencychart", children: jsxRuntime.jsx(react.Suspense, { fallback: LatencyChartSuspenseFallback, children: jsxRuntime.jsx(CallStatsLatencyChart, { values: latencyBuffer }) }) }), jsxRuntime.jsxs("div", { className: "str-video__call-stats__header", children: [jsxRuntime.jsxs("h3", { className: "str-video__call-stats__heading", children: [jsxRuntime.jsx(Icon, { className: "str-video__call-stats__icon", icon: "network-quality" }), t('Call performance')] }), jsxRuntime.jsx("p", { className: "str-video__call-stats__description", children: t('Review the key data points below to assess call performance') })] }), jsxRuntime.jsxs("div", { className: "str-video__call-stats__card-container", children: [jsxRuntime.jsx(StatCard, { label: t('Region'), value: callStatsReport.datacenter }), jsxRuntime.jsx(StatCard, { label: t('Latency'), value: `${callStatsReport.publisherStats.averageRoundTripTimeInMs} ms.`, comparison: latencyComparison }), jsxRuntime.jsx(StatCard, { label: t('Receive jitter'), value: `${callStatsReport.subscriberStats.averageJitterInMs} ms.`, comparison: {
1930
- ...latencyComparison,
2068
+ const audioJitterComparison = {
2069
+ lowBound: audioJitterLowBound,
2070
+ highBound: audioJitterHighBound,
2071
+ };
2072
+ const videoJitterComparison = {
2073
+ lowBound: videoJitterLowBound,
2074
+ highBound: videoJitterHighBound,
2075
+ };
2076
+ return (jsxRuntime.jsx("div", { className: "str-video__call-stats", children: callStatsReport && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "str-video__call-stats__header", children: [jsxRuntime.jsxs("h3", { className: "str-video__call-stats__heading", children: [jsxRuntime.jsx(Icon, { className: "str-video__call-stats__icon", icon: "call-latency" }), t('Call Latency')] }), jsxRuntime.jsx("p", { className: "str-video__call-stats__description", children: t('Very high latency values may reduce call quality, cause lag, and make the call less enjoyable.') })] }), jsxRuntime.jsx("div", { className: "str-video__call-stats__latencychart", children: jsxRuntime.jsx(react.Suspense, { fallback: LatencyChartSuspenseFallback, children: jsxRuntime.jsx(CallStatsLatencyChart, { values: latencyBuffer }) }) }), jsxRuntime.jsxs("div", { className: "str-video__call-stats__header", children: [jsxRuntime.jsxs("h3", { className: "str-video__call-stats__heading", children: [jsxRuntime.jsx(Icon, { className: "str-video__call-stats__icon", icon: "network-quality" }), t('Video performance')] }), jsxRuntime.jsx("p", { className: "str-video__call-stats__description", children: t('Review the key data points below to assess call performance') })] }), jsxRuntime.jsxs("div", { className: "str-video__call-stats__card-container", children: [jsxRuntime.jsx(StatCard, { label: t('Region'), value: callStatsReport.datacenter }), jsxRuntime.jsx(StatCard, { label: t('Latency'), value: `${callStatsReport.publisherStats.averageRoundTripTimeInMs} ms.`, comparison: latencyComparison }), jsxRuntime.jsx(StatCard, { label: t('Receive jitter'), value: `${callStatsReport.subscriberStats.averageJitterInMs} ms.`, comparison: {
2077
+ ...videoJitterComparison,
1931
2078
  value: callStatsReport.subscriberStats.averageJitterInMs,
1932
2079
  } }), jsxRuntime.jsx(StatCard, { label: t('Publish jitter'), value: `${callStatsReport.publisherStats.averageJitterInMs} ms.`, comparison: {
1933
- ...latencyComparison,
2080
+ ...videoJitterComparison,
1934
2081
  value: callStatsReport.publisherStats.averageJitterInMs,
1935
- } }), jsxRuntime.jsx(StatCard, { label: `${t('Publish resolution')}${showCodecInfo ? formatCodec(callStatsReport) : ''}`, value: toFrameSize(callStatsReport.publisherStats) }), jsxRuntime.jsx(StatCard, { label: t('Publish quality drop reason'), value: callStatsReport.publisherStats.qualityLimitationReasons }), jsxRuntime.jsx(StatCard, { label: t('Receiving resolution'), value: toFrameSize(callStatsReport.subscriberStats) }), jsxRuntime.jsx(StatCard, { label: t('Receive quality drop reason'), value: callStatsReport.subscriberStats.qualityLimitationReasons }), jsxRuntime.jsx(StatCard, { label: t('Publish bitrate'), value: publishBitrate }), jsxRuntime.jsx(StatCard, { label: t('Receiving bitrate'), value: subscribeBitrate })] })] })) }));
2082
+ } }), jsxRuntime.jsx(StatCard, { label: `${t('Publish resolution')}${showCodecInfo ? formatCodec(callStatsReport) : ''}`, value: toFrameSize(callStatsReport.publisherStats) }), jsxRuntime.jsx(StatCard, { label: t('Publish quality drop reason'), value: callStatsReport.publisherStats.qualityLimitationReasons }), jsxRuntime.jsx(StatCard, { label: t('Receiving resolution'), value: toFrameSize(callStatsReport.subscriberStats) }), jsxRuntime.jsx(StatCard, { label: t('Receive quality drop reason'), value: callStatsReport.subscriberStats.qualityLimitationReasons }), jsxRuntime.jsx(StatCard, { label: t('Publish bitrate'), value: publishBitrate }), jsxRuntime.jsx(StatCard, { label: t('Receiving bitrate'), value: subscribeBitrate })] }), jsxRuntime.jsxs("div", { className: "str-video__call-stats__header", children: [jsxRuntime.jsxs("h3", { className: "str-video__call-stats__heading", children: [jsxRuntime.jsx(Icon, { className: "str-video__call-stats__icon", icon: "mic" }), t('Audio Performance')] }), jsxRuntime.jsx("p", { className: "str-video__call-stats__description", children: t('Review the key audio data points below to assess audio performance') })] }), jsxRuntime.jsxs("div", { className: "str-video__call-stats__card-container", children: [jsxRuntime.jsx(StatCard, { label: t('Latency'), value: `${callStatsReport.publisherAudioStats.averageRoundTripTimeInMs} ms.`, comparison: latencyComparison }), jsxRuntime.jsx(StatCard, { label: t('Audio bitrate (publish)'), value: publishAudioBitrate }), jsxRuntime.jsx(StatCard, { label: t('Audio bitrate (receive)'), value: subscribeAudioBitrate }), jsxRuntime.jsx(StatCard, { label: t('Audio jitter (publish)'), value: `${callStatsReport.publisherAudioStats.averageJitterInMs} ms.`, comparison: {
2083
+ ...audioJitterComparison,
2084
+ value: callStatsReport.publisherAudioStats.averageJitterInMs,
2085
+ } }), jsxRuntime.jsx(StatCard, { label: t('Audio jitter (receive)'), value: `${callStatsReport.subscriberAudioStats.averageJitterInMs} ms.`, comparison: {
2086
+ ...audioJitterComparison,
2087
+ value: callStatsReport.subscriberAudioStats.averageJitterInMs,
2088
+ } }), jsxRuntime.jsx(StatCard, { label: t('Audio codec'), value: formatAudioCodec(callStatsReport) })] })] })) }));
1936
2089
  };
1937
2090
  const StatCardExplanation = (props) => {
1938
2091
  const { description } = props;
@@ -1988,6 +2141,14 @@ const formatCodec = (callStatsReport) => {
1988
2141
  const [, name] = codecPerTrackType[videoClient.SfuModels.TrackType.VIDEO].split('/');
1989
2142
  return name ? ` (${name})` : '';
1990
2143
  };
2144
+ const formatAudioCodec = (callStatsReport) => {
2145
+ const { codecPerTrackType } = callStatsReport.publisherAudioStats;
2146
+ if (!codecPerTrackType || !codecPerTrackType[videoClient.SfuModels.TrackType.AUDIO]) {
2147
+ return '';
2148
+ }
2149
+ const [, name] = codecPerTrackType[videoClient.SfuModels.TrackType.AUDIO].split('/');
2150
+ return name ?? '';
2151
+ };
1991
2152
  const calculatePublishBitrate = (previousCallStatsReport, callStatsReport) => {
1992
2153
  const { publisherStats: { totalBytesSent: previousTotalBytesSent, timestamp: previousTimestamp, }, } = previousCallStatsReport;
1993
2154
  const { publisherStats: { totalBytesSent, timestamp }, } = callStatsReport;
@@ -2002,6 +2163,20 @@ const calculateSubscribeBitrate = (previousCallStatsReport, callStatsReport) =>
2002
2163
  const timeElapsed = timestamp - previousTimestamp;
2003
2164
  return `${((bytesReceived * 8) / timeElapsed).toFixed(2)} kbps`;
2004
2165
  };
2166
+ const calculatePublishAudioBitrate = (previousCallStatsReport, callStatsReport) => {
2167
+ const previousAudioStats = previousCallStatsReport.publisherAudioStats;
2168
+ const audioStats = callStatsReport.publisherAudioStats;
2169
+ const bytesSent = audioStats.totalBytesSent - previousAudioStats.totalBytesSent;
2170
+ const timeElapsed = audioStats.timestamp - previousAudioStats.timestamp;
2171
+ return `${((bytesSent * 8) / timeElapsed).toFixed(2)} kbps`;
2172
+ };
2173
+ const calculateSubscribeAudioBitrate = (previousCallStatsReport, callStatsReport) => {
2174
+ const previousAudioStats = previousCallStatsReport.subscriberAudioStats;
2175
+ const audioStats = callStatsReport.subscriberAudioStats;
2176
+ const bytesReceived = audioStats.totalBytesReceived - previousAudioStats.totalBytesReceived;
2177
+ const timeElapsed = audioStats.timestamp - previousAudioStats.timestamp;
2178
+ return `${((bytesReceived * 8) / timeElapsed).toFixed(2)} kbps`;
2179
+ };
2005
2180
 
2006
2181
  const CallStatsButton = () => (jsxRuntime.jsx(MenuToggle, { placement: "top-end", ToggleButton: ToggleMenuButton, children: jsxRuntime.jsx(CallStats, {}) }));
2007
2182
  const ToggleMenuButton = react.forwardRef(function ToggleMenuButton(props, ref) {
@@ -2901,7 +3076,7 @@ const LivestreamLayout = (props) => {
2901
3076
  : undefined;
2902
3077
  usePaginatedLayoutSortPreset(call);
2903
3078
  const { floatingParticipantProps, muted, ParticipantViewUI } = props;
2904
- const overlay = ParticipantViewUI ?? (jsxRuntime.jsx(ParticipantOverlay, { showParticipantCount: props.showParticipantCount, showDuration: props.showDuration, showLiveBadge: props.showLiveBadge, showSpeakerName: props.showSpeakerName, enableFullScreen: props.enableFullScreen }));
3079
+ const overlay = ParticipantViewUI ?? (jsxRuntime.jsx(ParticipantOverlay, { showParticipantCount: props.showParticipantCount, showDuration: props.showDuration, showLiveBadge: props.showLiveBadge, showMuteButton: props.showMuteButton, showSpeakerName: props.showSpeakerName, enableFullScreen: props.enableFullScreen }));
2905
3080
  const floatingParticipantOverlay = hasOngoingScreenShare &&
2906
3081
  (ParticipantViewUI ?? (jsxRuntime.jsx(ParticipantOverlay
2907
3082
  // these elements aren't needed for the video feed
@@ -2915,7 +3090,7 @@ const LivestreamLayout = (props) => {
2915
3090
  };
2916
3091
  LivestreamLayout.displayName = 'LivestreamLayout';
2917
3092
  const BackstageLayout = (props) => {
2918
- const { showEarlyParticipantCount = true } = props;
3093
+ const { showEarlyParticipantCount = true, humanizeParticipantCount = true } = props;
2919
3094
  const { useParticipantCount, useCallStartsAt } = videoReactBindings.useCallStateHooks();
2920
3095
  const participantCount = useParticipantCount();
2921
3096
  const startsAt = useCallStartsAt();
@@ -2923,24 +3098,32 @@ const BackstageLayout = (props) => {
2923
3098
  return (jsxRuntime.jsx("div", { className: "str-video__livestream-layout__wrapper", children: jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__backstage", children: [startsAt && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__starts-at", children: startsAt.getTime() < Date.now()
2924
3099
  ? t('Livestream starts soon')
2925
3100
  : t('Livestream starts at {{ startsAt }}', { startsAt }) })), showEarlyParticipantCount && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__early-viewers-count", children: t('{{ count }} participants joined early', {
2926
- count: participantCount,
3101
+ count: humanizeParticipantCount
3102
+ ? videoClient.humanize(participantCount)
3103
+ : participantCount,
2927
3104
  }) }))] }) }));
2928
3105
  };
2929
3106
  BackstageLayout.displayName = 'BackstageLayout';
2930
3107
  const ParticipantOverlay = (props) => {
2931
- const { enableFullScreen = true, showParticipantCount = true, showDuration = true, showLiveBadge = true, showSpeakerName = false, } = props;
3108
+ const { enableFullScreen = true, showParticipantCount = true, humanizeParticipantCount = true, showDuration = true, showLiveBadge = true, showMuteButton = true, showSpeakerName = false, } = props;
2932
3109
  const overlayBarVisible = enableFullScreen ||
2933
3110
  showParticipantCount ||
2934
3111
  showDuration ||
2935
3112
  showLiveBadge ||
3113
+ showMuteButton ||
2936
3114
  showSpeakerName;
2937
3115
  const { participant } = useParticipantViewContext();
2938
- const { useParticipantCount } = videoReactBindings.useCallStateHooks();
3116
+ const { useParticipantCount, useSpeakerState } = videoReactBindings.useCallStateHooks();
2939
3117
  const participantCount = useParticipantCount();
2940
3118
  const duration = useUpdateCallDuration();
2941
3119
  const toggleFullScreen = useToggleFullScreen();
3120
+ const { speaker, volume } = useSpeakerState();
3121
+ const isSpeakerMuted = volume === 0;
2942
3122
  const { t } = videoReactBindings.useI18n();
2943
- return (jsxRuntime.jsx("div", { className: "str-video__livestream-layout__overlay", children: overlayBarVisible && (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__overlay__bar", children: [showLiveBadge && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__live-badge", children: t('Live') })), showParticipantCount && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__viewers-count", children: participantCount })), showSpeakerName && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__speaker-name", title: participant.name || participant.userId || '', children: participant.name || participant.userId || '' })), showDuration && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__duration", children: formatDuration(duration) })), enableFullScreen && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__go-fullscreen", onClick: toggleFullScreen }))] })) }));
3123
+ return (jsxRuntime.jsx("div", { className: "str-video__livestream-layout__overlay", children: overlayBarVisible && (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__overlay__bar", children: [showLiveBadge && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__live-badge", children: t('Live') })), showParticipantCount && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__viewers-count", children: humanizeParticipantCount
3124
+ ? videoClient.humanize(participantCount)
3125
+ : participantCount })), showSpeakerName && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__speaker-name", title: participant.name || participant.userId || '', children: participant.name || participant.userId || '' })), showDuration && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__duration", children: formatDuration(duration) })), showMuteButton && (jsxRuntime.jsx("span", { className: clsx('str-video__livestream-layout__mute-button', isSpeakerMuted &&
3126
+ 'str-video__livestream-layout__mute-button--muted'), onClick: () => speaker.setVolume(isSpeakerMuted ? 1 : 0) })), enableFullScreen && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__go-fullscreen", onClick: toggleFullScreen }))] })) }));
2944
3127
  };
2945
3128
  const useUpdateCallDuration = () => {
2946
3129
  const { useIsCallLive, useCallSession } = videoReactBindings.useCallStateHooks();
@@ -3269,7 +3452,7 @@ const checkCanJoinEarly = (startsAt, joinAheadTimeSeconds) => {
3269
3452
  return Date.now() >= +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
3270
3453
  };
3271
3454
 
3272
- const [major, minor, patch] = ("1.27.2").split('.');
3455
+ const [major, minor, patch] = ("1.28.1").split('.');
3273
3456
  videoClient.setSdkInfo({
3274
3457
  type: videoClient.SfuModels.SdkType.REACT,
3275
3458
  major,
@@ -3368,6 +3551,7 @@ exports.useDeviceList = useDeviceList;
3368
3551
  exports.useFilteredParticipants = useFilteredParticipants;
3369
3552
  exports.useHorizontalScrollPosition = useHorizontalScrollPosition;
3370
3553
  exports.useMenuContext = useMenuContext;
3554
+ exports.useModeration = useModeration;
3371
3555
  exports.useNoiseCancellation = useNoiseCancellation;
3372
3556
  exports.useParticipantViewContext = useParticipantViewContext;
3373
3557
  exports.usePersistedDevicePreferences = usePersistedDevicePreferences;