@stream-io/video-react-sdk 1.26.1 → 1.27.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
@@ -7,7 +7,7 @@ import { useState, useEffect, Fragment as Fragment$1, createContext, useContext,
7
7
  import { useFloating, offset, shift, flip, size, autoUpdate, FloatingOverlay, FloatingPortal, arrow, FloatingArrow, useListItem, useListNavigation, useTypeahead, useClick, useDismiss, useRole, useInteractions, FloatingFocusManager, FloatingList, useHover } from '@floating-ui/react';
8
8
  import clsx from 'clsx';
9
9
  import { flushSync } from 'react-dom';
10
- import { isPlatformSupported, loadTFLite, createRenderer } from '@stream-io/video-filters-web';
10
+ import { loadTFLite, loadMediaPipe, isPlatformSupported, isMediaPipePlatformSupported, VirtualBackground, createRenderer } from '@stream-io/video-filters-web';
11
11
 
12
12
  const Audio = ({ participant, trackType = 'audioTrack', ...rest }) => {
13
13
  const call = useCall();
@@ -827,6 +827,34 @@ const AvatarFallback = ({ className, names, style, }) => {
827
827
  return (jsx("div", { className: clsx('str-video__avatar--initials-fallback', className), style: style, children: jsxs("div", { children: [names[0][0], names[1]?.[0]] }) }));
828
828
  };
829
829
 
830
+ /**
831
+ * Constants for FPS warning calculation.
832
+ * Smooths out quick spikes using an EMA, ignores brief outliers,
833
+ * and uses two thresholds to avoid flickering near the limit.
834
+ */
835
+ const ALPHA = 0.2;
836
+ const FPS_WARNING_THRESHOLD_LOWER = 23;
837
+ const FPS_WARNING_THRESHOLD_UPPER = 25;
838
+ const DEFAULT_FPS = 30;
839
+ const DEVIATION_LIMIT = 0.5;
840
+ const OUTLIER_PERSISTENCE = 5;
841
+ /**
842
+ * Represents the available background filter processing engines.
843
+ */
844
+ var FilterEngine;
845
+ (function (FilterEngine) {
846
+ FilterEngine[FilterEngine["TF"] = 0] = "TF";
847
+ FilterEngine[FilterEngine["MEDIA_PIPE"] = 1] = "MEDIA_PIPE";
848
+ FilterEngine[FilterEngine["NONE"] = 2] = "NONE";
849
+ })(FilterEngine || (FilterEngine = {}));
850
+ /**
851
+ * Represents the possible reasons for background filter performance degradation.
852
+ */
853
+ var PerformanceDegradationReason;
854
+ (function (PerformanceDegradationReason) {
855
+ PerformanceDegradationReason["FRAME_DROP"] = "frame-drop";
856
+ PerformanceDegradationReason["CPU_THROTTLING"] = "cpu-throttling";
857
+ })(PerformanceDegradationReason || (PerformanceDegradationReason = {}));
830
858
  /**
831
859
  * The context for the background filters.
832
860
  */
@@ -841,6 +869,26 @@ const useBackgroundFilters = () => {
841
869
  }
842
870
  return context;
843
871
  };
872
+ /**
873
+ * Determines which filter engine is available.
874
+ * MEDIA_PIPE is the default unless legacy filters are requested or MediaPipe is unsupported.
875
+ *
876
+ * Returns NONE if neither is supported.
877
+ */
878
+ const determineEngine = async (useLegacyFilter, forceSafariSupport, forceMobileSupport) => {
879
+ const isTfPlatformSupported = await isPlatformSupported({
880
+ forceSafariSupport,
881
+ forceMobileSupport,
882
+ });
883
+ if (useLegacyFilter) {
884
+ return isTfPlatformSupported ? FilterEngine.TF : FilterEngine.NONE;
885
+ }
886
+ const isMediaPipeSupported = await isMediaPipePlatformSupported({
887
+ forceSafariSupport,
888
+ forceMobileSupport,
889
+ });
890
+ return isMediaPipeSupported ? FilterEngine.MEDIA_PIPE : FilterEngine.NONE;
891
+ };
844
892
  /**
845
893
  * A provider component that enables the use of background filters in your app.
846
894
  *
@@ -848,10 +896,88 @@ const useBackgroundFilters = () => {
848
896
  * in your project before using this component.
849
897
  */
850
898
  const BackgroundFiltersProvider = (props) => {
851
- const { children, backgroundImages = [], backgroundFilter: bgFilterFromProps = undefined, backgroundImage: bgImageFromProps = undefined, backgroundBlurLevel: bgBlurLevelFromProps = undefined, tfFilePath, modelFilePath, basePath, onError, forceSafariSupport, forceMobileSupport, } = props;
899
+ const { children, backgroundImages = [], backgroundFilter: bgFilterFromProps = undefined, backgroundImage: bgImageFromProps = undefined, backgroundBlurLevel: bgBlurLevelFromProps = undefined, tfFilePath, modelFilePath, useLegacyFilter, basePath, onError, performanceThresholds, forceSafariSupport, forceMobileSupport, } = props;
900
+ const call = useCall();
901
+ const { useCallStatsReport } = useCallStateHooks();
902
+ const callStatsReport = useCallStatsReport();
852
903
  const [backgroundFilter, setBackgroundFilter] = useState(bgFilterFromProps);
853
904
  const [backgroundImage, setBackgroundImage] = useState(bgImageFromProps);
854
905
  const [backgroundBlurLevel, setBackgroundBlurLevel] = useState(bgBlurLevelFromProps);
906
+ const [showLowFpsWarning, setShowLowFpsWarning] = useState(false);
907
+ const fpsWarningThresholdLower = performanceThresholds?.fpsWarningThresholdLower ??
908
+ FPS_WARNING_THRESHOLD_LOWER;
909
+ const fpsWarningThresholdUpper = performanceThresholds?.fpsWarningThresholdUpper ??
910
+ FPS_WARNING_THRESHOLD_UPPER;
911
+ const defaultFps = performanceThresholds?.defaultFps ?? DEFAULT_FPS;
912
+ const emaRef = useRef(defaultFps);
913
+ const outlierStreakRef = useRef(0);
914
+ const handleStats = useCallback((stats) => {
915
+ const fps = stats?.fps;
916
+ if (fps === undefined || fps === null) {
917
+ emaRef.current = defaultFps;
918
+ outlierStreakRef.current = 0;
919
+ setShowLowFpsWarning(false);
920
+ return;
921
+ }
922
+ const prevEma = emaRef.current;
923
+ const deviation = Math.abs(fps - prevEma) / prevEma;
924
+ const isOutlier = fps < prevEma && deviation > DEVIATION_LIMIT;
925
+ outlierStreakRef.current = isOutlier ? outlierStreakRef.current + 1 : 0;
926
+ if (isOutlier && outlierStreakRef.current < OUTLIER_PERSISTENCE)
927
+ return;
928
+ emaRef.current = ALPHA * fps + (1 - ALPHA) * prevEma;
929
+ setShowLowFpsWarning((prev) => {
930
+ if (prev && emaRef.current > fpsWarningThresholdUpper)
931
+ return false;
932
+ if (!prev && emaRef.current < fpsWarningThresholdLower)
933
+ return true;
934
+ return prev;
935
+ });
936
+ }, [fpsWarningThresholdLower, fpsWarningThresholdUpper, defaultFps]);
937
+ const performance = useMemo(() => {
938
+ if (!backgroundFilter) {
939
+ return { degraded: false };
940
+ }
941
+ const reasons = [];
942
+ if (showLowFpsWarning) {
943
+ reasons.push(PerformanceDegradationReason.FRAME_DROP);
944
+ }
945
+ const qualityLimitationReasons = callStatsReport?.publisherStats?.qualityLimitationReasons;
946
+ if (showLowFpsWarning &&
947
+ qualityLimitationReasons &&
948
+ qualityLimitationReasons?.includes('cpu')) {
949
+ reasons.push(PerformanceDegradationReason.CPU_THROTTLING);
950
+ }
951
+ return {
952
+ degraded: reasons.length > 0,
953
+ reason: reasons.length > 0 ? reasons : undefined,
954
+ };
955
+ }, [
956
+ showLowFpsWarning,
957
+ callStatsReport?.publisherStats?.qualityLimitationReasons,
958
+ backgroundFilter,
959
+ ]);
960
+ const prevDegradedRef = useRef(undefined);
961
+ useEffect(() => {
962
+ const currentDegraded = performance.degraded;
963
+ const prevDegraded = prevDegradedRef.current;
964
+ if (!!backgroundFilter &&
965
+ prevDegraded !== undefined &&
966
+ prevDegraded !== currentDegraded) {
967
+ call?.tracer.trace('backgroundFilters.performance', {
968
+ degraded: currentDegraded,
969
+ reason: performance?.reason,
970
+ fps: emaRef.current,
971
+ });
972
+ }
973
+ prevDegradedRef.current = currentDegraded;
974
+ }, [
975
+ performanceThresholds,
976
+ performance.degraded,
977
+ performance.reason,
978
+ backgroundFilter,
979
+ call?.tracer,
980
+ ]);
855
981
  const applyBackgroundImageFilter = useCallback((imageUrl) => {
856
982
  setBackgroundFilter('image');
857
983
  setBackgroundImage(imageUrl);
@@ -864,31 +990,47 @@ const BackgroundFiltersProvider = (props) => {
864
990
  setBackgroundFilter(undefined);
865
991
  setBackgroundImage(undefined);
866
992
  setBackgroundBlurLevel(undefined);
867
- }, []);
993
+ emaRef.current = defaultFps;
994
+ outlierStreakRef.current = 0;
995
+ setShowLowFpsWarning(false);
996
+ }, [defaultFps]);
997
+ const [engine, setEngine] = useState(FilterEngine.NONE);
868
998
  const [isSupported, setIsSupported] = useState(false);
869
999
  useEffect(() => {
870
- isPlatformSupported({
871
- forceSafariSupport,
872
- forceMobileSupport,
873
- }).then(setIsSupported);
874
- }, [forceMobileSupport, forceSafariSupport]);
1000
+ determineEngine(useLegacyFilter, forceSafariSupport, forceMobileSupport).then((determinedEngine) => {
1001
+ setEngine(determinedEngine);
1002
+ setIsSupported(determinedEngine !== FilterEngine.NONE);
1003
+ });
1004
+ }, [forceMobileSupport, forceSafariSupport, useLegacyFilter]);
875
1005
  const [tfLite, setTfLite] = useState();
876
1006
  useEffect(() => {
877
- // don't try to load TFLite if the platform is not supported
878
- if (!isSupported)
1007
+ if (engine !== FilterEngine.TF)
879
1008
  return;
880
1009
  loadTFLite({ basePath, modelFilePath, tfFilePath })
881
1010
  .then(setTfLite)
882
1011
  .catch((err) => console.error('Failed to load TFLite', err));
883
- }, [basePath, isSupported, modelFilePath, tfFilePath]);
1012
+ }, [basePath, engine, modelFilePath, tfFilePath]);
1013
+ const [mediaPipe, setMediaPipe] = useState();
1014
+ useEffect(() => {
1015
+ if (engine !== FilterEngine.MEDIA_PIPE)
1016
+ return;
1017
+ loadMediaPipe({
1018
+ basePath: basePath,
1019
+ modelPath: modelFilePath,
1020
+ })
1021
+ .then(setMediaPipe)
1022
+ .catch((err) => console.error('Failed to preload MediaPipe', err));
1023
+ }, [engine, modelFilePath, basePath]);
884
1024
  const handleError = useCallback((error) => {
885
1025
  console.warn('[filters] Filter encountered an error and will be disabled');
886
1026
  disableBackgroundFilter();
887
1027
  onError?.(error);
888
1028
  }, [disableBackgroundFilter, onError]);
1029
+ const isReady = useLegacyFilter ? !!tfLite : !!mediaPipe;
889
1030
  return (jsxs(BackgroundFiltersContext.Provider, { value: {
890
1031
  isSupported,
891
- isReady: !!tfLite,
1032
+ performance,
1033
+ isReady,
892
1034
  backgroundImage,
893
1035
  backgroundBlurLevel,
894
1036
  backgroundFilter,
@@ -900,26 +1042,30 @@ const BackgroundFiltersProvider = (props) => {
900
1042
  modelFilePath,
901
1043
  basePath,
902
1044
  onError: handleError,
903
- }, children: [children, tfLite && jsx(BackgroundFilters, { tfLite: tfLite })] }));
1045
+ }, children: [children, isReady && (jsx(BackgroundFilters, { tfLite: tfLite, engine: engine, onStats: handleStats }))] }));
904
1046
  };
905
1047
  const BackgroundFilters = (props) => {
906
1048
  const call = useCall();
907
- const { children, start } = useRenderer(props.tfLite, call);
908
- const { backgroundFilter, onError } = useBackgroundFilters();
1049
+ const { children, start } = useRenderer(props.tfLite, call, props.engine);
1050
+ const { onError, backgroundFilter } = useBackgroundFilters();
909
1051
  const handleErrorRef = useRef(undefined);
910
1052
  handleErrorRef.current = onError;
1053
+ const handleStatsRef = useRef(undefined);
1054
+ handleStatsRef.current = props.onStats;
911
1055
  useEffect(() => {
912
1056
  if (!call || !backgroundFilter)
913
1057
  return;
914
- const { unregister } = call.camera.registerFilter((ms) => start(ms, (error) => handleErrorRef.current?.(error)));
1058
+ const { unregister } = call.camera.registerFilter((ms) => {
1059
+ return start(ms, (error) => handleErrorRef.current?.(error), (stats) => handleStatsRef.current?.(stats));
1060
+ });
915
1061
  return () => {
916
1062
  unregister().catch((err) => console.warn(`Can't unregister filter`, err));
917
1063
  };
918
- }, [backgroundFilter, call, start]);
1064
+ }, [call, start, backgroundFilter]);
919
1065
  return children;
920
1066
  };
921
- const useRenderer = (tfLite, call) => {
922
- const { backgroundFilter, backgroundBlurLevel, backgroundImage } = useBackgroundFilters();
1067
+ const useRenderer = (tfLite, call, engine) => {
1068
+ const { backgroundFilter, backgroundBlurLevel, backgroundImage, modelFilePath, basePath, } = useBackgroundFilters();
923
1069
  const videoRef = useRef(null);
924
1070
  const canvasRef = useRef(null);
925
1071
  const bgImageRef = useRef(null);
@@ -927,8 +1073,9 @@ const useRenderer = (tfLite, call) => {
927
1073
  width: 1920,
928
1074
  height: 1080,
929
1075
  });
930
- const start = useCallback((ms, onError) => {
1076
+ const start = useCallback((ms, onError, onStats) => {
931
1077
  let outputStream;
1078
+ let processor;
932
1079
  let renderer;
933
1080
  const output = new Promise((resolve, reject) => {
934
1081
  if (!backgroundFilter) {
@@ -938,16 +1085,20 @@ const useRenderer = (tfLite, call) => {
938
1085
  const videoEl = videoRef.current;
939
1086
  const canvasEl = canvasRef.current;
940
1087
  const bgImageEl = bgImageRef.current;
941
- if (!videoEl || !canvasEl || (backgroundImage && !bgImageEl)) {
942
- // You should start renderer in effect or event handlers
943
- reject(new Error('Renderer started before elements are ready'));
1088
+ const [track] = ms.getVideoTracks();
1089
+ if (!track) {
1090
+ reject(new Error('No video tracks in input media stream'));
944
1091
  return;
945
1092
  }
946
- videoEl.srcObject = ms;
947
- videoEl.play().then(() => {
948
- const [track] = ms.getVideoTracks();
949
- if (!track) {
950
- reject(new Error('No video tracks in input media stream'));
1093
+ if (engine === FilterEngine.MEDIA_PIPE) {
1094
+ call?.tracer.trace('backgroundFilters.enable', {
1095
+ backgroundFilter,
1096
+ backgroundBlurLevel,
1097
+ backgroundImage,
1098
+ engine,
1099
+ });
1100
+ if (!videoEl) {
1101
+ reject(new Error('Renderer started before elements are ready'));
951
1102
  return;
952
1103
  }
953
1104
  const trackSettings = track.getSettings();
@@ -955,26 +1106,65 @@ const useRenderer = (tfLite, call) => {
955
1106
  width: trackSettings.width ?? 0,
956
1107
  height: trackSettings.height ?? 0,
957
1108
  }));
958
- call?.tracer.trace('backgroundFilters.enable', {
959
- backgroundFilter,
1109
+ processor = new VirtualBackground(track, {
1110
+ basePath: basePath,
1111
+ modelPath: modelFilePath,
960
1112
  backgroundBlurLevel,
961
1113
  backgroundImage,
962
- });
963
- renderer = createRenderer(tfLite, videoEl, canvasEl, {
964
1114
  backgroundFilter,
965
- backgroundBlurLevel,
966
- backgroundImage: bgImageEl ?? undefined,
967
- }, onError);
968
- outputStream = canvasEl.captureStream();
969
- resolve(outputStream);
970
- }, () => {
971
- reject(new Error('Could not play the source video stream'));
972
- });
1115
+ }, { onError, onStats });
1116
+ processor
1117
+ .start()
1118
+ .then((processedTrack) => {
1119
+ outputStream = new MediaStream([processedTrack]);
1120
+ resolve(outputStream);
1121
+ })
1122
+ .catch((error) => {
1123
+ reject(error);
1124
+ });
1125
+ return;
1126
+ }
1127
+ if (engine === FilterEngine.TF) {
1128
+ if (!videoEl || !canvasEl || (backgroundImage && !bgImageEl)) {
1129
+ reject(new Error('Renderer started before elements are ready'));
1130
+ return;
1131
+ }
1132
+ videoEl.srcObject = ms;
1133
+ videoEl.play().then(() => {
1134
+ const trackSettings = track.getSettings();
1135
+ flushSync(() => setVideoSize({
1136
+ width: trackSettings.width ?? 0,
1137
+ height: trackSettings.height ?? 0,
1138
+ }));
1139
+ call?.tracer.trace('backgroundFilters.enable', {
1140
+ backgroundFilter,
1141
+ backgroundBlurLevel,
1142
+ backgroundImage,
1143
+ engine,
1144
+ });
1145
+ if (!tfLite) {
1146
+ reject(new Error('TensorFlow Lite not loaded'));
1147
+ return;
1148
+ }
1149
+ renderer = createRenderer(tfLite, videoEl, canvasEl, {
1150
+ backgroundFilter,
1151
+ backgroundBlurLevel,
1152
+ backgroundImage: bgImageEl ?? undefined,
1153
+ }, onError);
1154
+ outputStream = canvasEl.captureStream();
1155
+ resolve(outputStream);
1156
+ }, () => {
1157
+ reject(new Error('Could not play the source video stream'));
1158
+ });
1159
+ return;
1160
+ }
1161
+ reject(new Error('No supported engine available'));
973
1162
  });
974
1163
  return {
975
1164
  output,
976
1165
  stop: () => {
977
1166
  call?.tracer.trace('backgroundFilters.disable', null);
1167
+ processor?.stop();
978
1168
  renderer?.dispose();
979
1169
  if (videoRef.current)
980
1170
  videoRef.current.srcObject = null;
@@ -988,13 +1178,13 @@ const useRenderer = (tfLite, call) => {
988
1178
  backgroundImage,
989
1179
  call?.tracer,
990
1180
  tfLite,
1181
+ engine,
1182
+ modelFilePath,
1183
+ basePath,
991
1184
  ]);
992
1185
  const children = (jsxs("div", { className: "str-video__background-filters", children: [jsx("video", { className: clsx('str-video__background-filters__video', videoSize.height > videoSize.width &&
993
1186
  'str-video__background-filters__video--tall'), ref: videoRef, playsInline: true, muted: true, controls: false, ...videoSize }), backgroundImage && (jsx("img", { className: "str-video__background-filters__background-image", alt: "Background", ref: bgImageRef, crossOrigin: "anonymous", src: backgroundImage, ...videoSize })), jsx("canvas", { className: "str-video__background-filters__target-canvas", ...videoSize, ref: canvasRef })] }));
994
- return {
995
- start,
996
- children,
997
- };
1187
+ return { start, children };
998
1188
  };
999
1189
 
1000
1190
  const IconButton = forwardRef(function IconButton(props, ref) {
@@ -1047,7 +1237,7 @@ const AcceptCallButton = ({ disabled, onAccept, onClick, }) => {
1047
1237
  };
1048
1238
 
1049
1239
  const Notification = (props) => {
1050
- const { isVisible, message, children, visibilityTimeout, resetIsVisible, placement = 'top', iconClassName = 'str-video__notification__icon', close, } = props;
1240
+ const { isVisible, message, children, visibilityTimeout, resetIsVisible, placement = 'top', className, iconClassName = 'str-video__notification__icon', close, } = props;
1051
1241
  const { refs, x, y, strategy } = useFloatingUIPreset({
1052
1242
  placement,
1053
1243
  strategy: 'absolute',
@@ -1060,7 +1250,7 @@ const Notification = (props) => {
1060
1250
  }, visibilityTimeout);
1061
1251
  return () => clearTimeout(timeout);
1062
1252
  }, [isVisible, resetIsVisible, visibilityTimeout]);
1063
- return (jsxs("div", { ref: refs.setReference, children: [isVisible && (jsxs("div", { className: "str-video__notification", ref: refs.setFloating, style: {
1253
+ return (jsxs("div", { ref: refs.setReference, children: [isVisible && (jsxs("div", { className: clsx('str-video__notification', className), ref: refs.setFloating, style: {
1064
1254
  position: strategy,
1065
1255
  top: y ?? 0,
1066
1256
  left: x ?? 0,
@@ -1456,6 +1646,54 @@ const DeviceSelector = (props) => {
1456
1646
  return jsx(DeviceSelectorDropdown, { ...rest, icon: icon });
1457
1647
  };
1458
1648
 
1649
+ /**
1650
+ * SpeakerTest component that plays a test audio through the selected speaker.
1651
+ * This allows users to verify their audio output device is working correctly.
1652
+ */
1653
+ const SpeakerTest = (props) => {
1654
+ const { useSpeakerState } = useCallStateHooks();
1655
+ const { selectedDevice } = useSpeakerState();
1656
+ const audioElementRef = useRef(null);
1657
+ const [isPlaying, setIsPlaying] = useState(false);
1658
+ const { t } = useI18n();
1659
+ const { audioUrl = `https://unpkg.com/${"@stream-io/video-react-sdk"}@${"1.27.0"}/assets/piano.mp3`, } = props;
1660
+ // Update audio output device when selection changes
1661
+ useEffect(() => {
1662
+ const audio = audioElementRef.current;
1663
+ if (!audio || !selectedDevice)
1664
+ return;
1665
+ // Set the sinkId to route audio to the selected speaker
1666
+ if ('setSinkId' in audio) {
1667
+ audio.setSinkId(selectedDevice).catch((err) => {
1668
+ console.error('Failed to set audio output device:', err);
1669
+ });
1670
+ }
1671
+ }, [selectedDevice]);
1672
+ const handleStartTest = useCallback(async () => {
1673
+ const audio = audioElementRef.current;
1674
+ if (!audio)
1675
+ return;
1676
+ audio.src = audioUrl;
1677
+ try {
1678
+ if (isPlaying) {
1679
+ audio.pause();
1680
+ audio.currentTime = 0;
1681
+ setIsPlaying(false);
1682
+ }
1683
+ else {
1684
+ await audio.play();
1685
+ setIsPlaying(true);
1686
+ }
1687
+ }
1688
+ catch (err) {
1689
+ console.error('Failed to play test audio:', err);
1690
+ setIsPlaying(false);
1691
+ }
1692
+ }, [isPlaying, audioUrl]);
1693
+ const handleAudioEnded = useCallback(() => setIsPlaying(false), []);
1694
+ return (jsxs("div", { className: "str-video__speaker-test", children: [jsx("audio", { ref: audioElementRef, onEnded: handleAudioEnded, onPause: handleAudioEnded }), jsx(CompositeButton, { className: "str-video__speaker-test__button", onClick: handleStartTest, type: "button", children: jsxs("div", { className: "str-video__speaker-test__button-content", children: [jsx(Icon, { icon: "speaker" }), isPlaying ? t('Stop test') : t('Test speaker')] }) })] }));
1695
+ };
1696
+
1459
1697
  const DeviceSelectorAudioInput = ({ title, visualType, volumeIndicatorVisible = true, }) => {
1460
1698
  const { useMicrophoneState } = useCallStateHooks();
1461
1699
  const { microphone, selectedDevice, devices } = useMicrophoneState();
@@ -1463,14 +1701,14 @@ const DeviceSelectorAudioInput = ({ title, visualType, volumeIndicatorVisible =
1463
1701
  await microphone.select(deviceId);
1464
1702
  }, title: title, visualType: visualType, icon: "mic", children: volumeIndicatorVisible && (jsxs(Fragment, { children: [jsx("hr", { className: "str-video__device-settings__separator" }), jsx(AudioVolumeIndicator, {})] })) }));
1465
1703
  };
1466
- const DeviceSelectorAudioOutput = ({ title, visualType, }) => {
1704
+ const DeviceSelectorAudioOutput = ({ title, visualType, speakerTestVisible = true, speakerTestAudioUrl, }) => {
1467
1705
  const { useSpeakerState } = useCallStateHooks();
1468
1706
  const { speaker, selectedDevice, devices, isDeviceSelectionSupported } = useSpeakerState();
1469
1707
  if (!isDeviceSelectionSupported)
1470
1708
  return null;
1471
1709
  return (jsx(DeviceSelector, { devices: devices, type: "audiooutput", selectedDeviceId: selectedDevice, onChange: (deviceId) => {
1472
1710
  speaker.select(deviceId);
1473
- }, title: title, visualType: visualType, icon: "speaker" }));
1711
+ }, title: title, visualType: visualType, icon: "speaker", children: speakerTestVisible && (jsxs(Fragment, { children: [jsx("hr", { className: "str-video__device-settings__separator" }), jsx(SpeakerTest, { audioUrl: speakerTestAudioUrl })] })) }));
1474
1712
  };
1475
1713
 
1476
1714
  const DeviceSelectorVideo = ({ title, visualType, }) => {
@@ -2385,6 +2623,7 @@ var en = {
2385
2623
  Speakers: Speakers,
2386
2624
  Video: Video,
2387
2625
  "You are muted. Unmute to speak.": "You are muted. Unmute to speak.",
2626
+ "Background filters performance is degraded. Consider disabling filters for better performance.": "Background filters performance is degraded. Consider disabling filters for better performance.",
2388
2627
  Live: Live,
2389
2628
  "Livestream starts soon": "Livestream starts soon",
2390
2629
  "Livestream starts at {{ startsAt }}": "Livestream starts at {{ startsAt, datetime }}",
@@ -3026,7 +3265,7 @@ const checkCanJoinEarly = (startsAt, joinAheadTimeSeconds) => {
3026
3265
  return Date.now() >= +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
3027
3266
  };
3028
3267
 
3029
- const [major, minor, patch] = ("1.26.1").split('.');
3268
+ const [major, minor, patch] = ("1.27.0").split('.');
3030
3269
  setSdkInfo({
3031
3270
  type: SfuModels.SdkType.REACT,
3032
3271
  major,
@@ -3034,5 +3273,5 @@ setSdkInfo({
3034
3273
  patch,
3035
3274
  });
3036
3275
 
3037
- export { AcceptCallButton, Audio, AudioVolumeIndicator, Avatar, AvatarFallback, BackgroundFiltersProvider, BackstageLayout, BaseVideo, CallControls, CallParticipantListing, CallParticipantListingItem, CallParticipantsList, CallPreview, CallRecordingList, CallRecordingListHeader, CallRecordingListItem, CallStats, CallStatsButton, CancelCallButton, CancelCallConfirmButton, CompositeButton, DefaultParticipantViewUI, DefaultReactionsMenu, DefaultScreenShareOverlay, DefaultVideoPlaceholder, DeviceSelector, DeviceSelectorAudioInput, DeviceSelectorAudioOutput, DeviceSelectorVideo, DeviceSettings, DropDownSelect, DropDownSelectOption, EmptyCallRecordingListing, GenericMenu, GenericMenuButtonItem, Icon, IconButton, LivestreamLayout, LivestreamPlayer, LoadingCallRecordingListing, LoadingIndicator, MenuToggle, MenuVisualType, NoiseCancellationProvider, Notification, PaginatedGridLayout, ParticipantActionsContextMenu, ParticipantDetails, ParticipantView, ParticipantViewContext, ParticipantsAudio, PermissionNotification, PermissionRequestList, PermissionRequests, PipLayout, Reaction, ReactionsButton, RecordCallButton, RecordCallConfirmationButton, RecordingInProgressNotification, RingingCall, RingingCallControls, ScreenShareButton, SearchInput, SearchResults, SpeakerLayout, SpeakingWhileMutedNotification, SpeechIndicator, StatCard, StreamCall, StreamTheme, StreamVideo, TextButton, ToggleAudioOutputButton, ToggleAudioPreviewButton, ToggleAudioPublishingButton, ToggleVideoPreviewButton, ToggleVideoPublishingButton, Tooltip, Video$1 as Video, VideoPreview, WithTooltip, applyFilter, defaultEmojiReactionMap, defaultReactions, translations, useBackgroundFilters, useDeviceList, useFilteredParticipants, useHorizontalScrollPosition, useMenuContext, useNoiseCancellation, useParticipantViewContext, usePersistedDevicePreferences, useRequestPermission, useTrackElementVisibility, useVerticalScrollPosition };
3276
+ export { AcceptCallButton, Audio, AudioVolumeIndicator, Avatar, AvatarFallback, BackgroundFiltersProvider, BackstageLayout, BaseVideo, CallControls, CallParticipantListing, CallParticipantListingItem, CallParticipantsList, CallPreview, CallRecordingList, CallRecordingListHeader, CallRecordingListItem, CallStats, CallStatsButton, CancelCallButton, CancelCallConfirmButton, CompositeButton, DefaultParticipantViewUI, DefaultReactionsMenu, DefaultScreenShareOverlay, DefaultVideoPlaceholder, DeviceSelector, DeviceSelectorAudioInput, DeviceSelectorAudioOutput, DeviceSelectorVideo, DeviceSettings, DropDownSelect, DropDownSelectOption, EmptyCallRecordingListing, GenericMenu, GenericMenuButtonItem, Icon, IconButton, LivestreamLayout, LivestreamPlayer, LoadingCallRecordingListing, LoadingIndicator, MenuToggle, MenuVisualType, NoiseCancellationProvider, Notification, PaginatedGridLayout, ParticipantActionsContextMenu, ParticipantDetails, ParticipantView, ParticipantViewContext, ParticipantsAudio, PerformanceDegradationReason, PermissionNotification, PermissionRequestList, PermissionRequests, PipLayout, Reaction, ReactionsButton, RecordCallButton, RecordCallConfirmationButton, RecordingInProgressNotification, RingingCall, RingingCallControls, ScreenShareButton, SearchInput, SearchResults, SpeakerLayout, SpeakerTest, SpeakingWhileMutedNotification, SpeechIndicator, StatCard, StreamCall, StreamTheme, StreamVideo, TextButton, ToggleAudioOutputButton, ToggleAudioPreviewButton, ToggleAudioPublishingButton, ToggleVideoPreviewButton, ToggleVideoPublishingButton, Tooltip, Video$1 as Video, VideoPreview, WithTooltip, applyFilter, defaultEmojiReactionMap, defaultReactions, translations, useBackgroundFilters, useDeviceList, useFilteredParticipants, useHorizontalScrollPosition, useMenuContext, useNoiseCancellation, useParticipantViewContext, usePersistedDevicePreferences, useRequestPermission, useTrackElementVisibility, useVerticalScrollPosition };
3038
3277
  //# sourceMappingURL=index.es.js.map