@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/CHANGELOG.md +21 -0
- package/assets/piano.mp3 +0 -0
- package/dist/css/styles.css +19 -4
- package/dist/css/styles.css.map +1 -1
- package/dist/index.cjs.js +288 -48
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +289 -50
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/BackgroundFilters/BackgroundFilters.d.ts +66 -4
- package/dist/src/components/DeviceSettings/DeviceSelectorAudio.d.ts +3 -1
- package/dist/src/components/DeviceSettings/SpeakerTest.d.ts +7 -0
- package/dist/src/components/DeviceSettings/index.d.ts +1 -0
- package/dist/src/components/Notification/Notification.d.ts +1 -0
- package/dist/src/translations/index.d.ts +1 -0
- package/package.json +6 -5
- package/src/components/BackgroundFilters/BackgroundFilters.tsx +413 -68
- package/src/components/DeviceSettings/DeviceSelectorAudio.tsx +13 -1
- package/src/components/DeviceSettings/SpeakerTest.tsx +75 -0
- package/src/components/DeviceSettings/index.ts +1 -0
- package/src/components/Notification/Notification.tsx +4 -2
- package/src/translations/en.json +1 -0
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,
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
})
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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) =>
|
|
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
|
-
}, [
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
reject(new Error('
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
959
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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:
|
|
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.
|
|
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
|