@stream-io/video-react-sdk 1.26.0 → 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 +34 -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 +313 -66
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +313 -68
- 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 +2 -0
- package/dist/src/components/Notification/Notification.d.ts +1 -0
- package/dist/src/core/components/CallLayout/LivestreamLayout.d.ts +5 -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 +2 -0
- package/src/components/Notification/Notification.tsx +4 -2
- package/src/core/components/CallLayout/LivestreamLayout.tsx +68 -45
- package/src/translations/en.json +1 -0
package/dist/index.cjs.js
CHANGED
|
@@ -827,6 +827,34 @@ const AvatarFallback = ({ className, names, style, }) => {
|
|
|
827
827
|
return (jsxRuntime.jsx("div", { className: clsx('str-video__avatar--initials-fallback', className), style: style, children: jsxRuntime.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
|
+
exports.PerformanceDegradationReason = void 0;
|
|
854
|
+
(function (PerformanceDegradationReason) {
|
|
855
|
+
PerformanceDegradationReason["FRAME_DROP"] = "frame-drop";
|
|
856
|
+
PerformanceDegradationReason["CPU_THROTTLING"] = "cpu-throttling";
|
|
857
|
+
})(exports.PerformanceDegradationReason || (exports.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 videoFiltersWeb.isPlatformSupported({
|
|
880
|
+
forceSafariSupport,
|
|
881
|
+
forceMobileSupport,
|
|
882
|
+
});
|
|
883
|
+
if (useLegacyFilter) {
|
|
884
|
+
return isTfPlatformSupported ? FilterEngine.TF : FilterEngine.NONE;
|
|
885
|
+
}
|
|
886
|
+
const isMediaPipeSupported = await videoFiltersWeb.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 = videoReactBindings.useCall();
|
|
901
|
+
const { useCallStatsReport } = videoReactBindings.useCallStateHooks();
|
|
902
|
+
const callStatsReport = useCallStatsReport();
|
|
852
903
|
const [backgroundFilter, setBackgroundFilter] = react.useState(bgFilterFromProps);
|
|
853
904
|
const [backgroundImage, setBackgroundImage] = react.useState(bgImageFromProps);
|
|
854
905
|
const [backgroundBlurLevel, setBackgroundBlurLevel] = react.useState(bgBlurLevelFromProps);
|
|
906
|
+
const [showLowFpsWarning, setShowLowFpsWarning] = react.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 = react.useRef(defaultFps);
|
|
913
|
+
const outlierStreakRef = react.useRef(0);
|
|
914
|
+
const handleStats = react.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 = react.useMemo(() => {
|
|
938
|
+
if (!backgroundFilter) {
|
|
939
|
+
return { degraded: false };
|
|
940
|
+
}
|
|
941
|
+
const reasons = [];
|
|
942
|
+
if (showLowFpsWarning) {
|
|
943
|
+
reasons.push(exports.PerformanceDegradationReason.FRAME_DROP);
|
|
944
|
+
}
|
|
945
|
+
const qualityLimitationReasons = callStatsReport?.publisherStats?.qualityLimitationReasons;
|
|
946
|
+
if (showLowFpsWarning &&
|
|
947
|
+
qualityLimitationReasons &&
|
|
948
|
+
qualityLimitationReasons?.includes('cpu')) {
|
|
949
|
+
reasons.push(exports.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 = react.useRef(undefined);
|
|
961
|
+
react.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 = react.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] = react.useState(FilterEngine.NONE);
|
|
868
998
|
const [isSupported, setIsSupported] = react.useState(false);
|
|
869
999
|
react.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] = react.useState();
|
|
876
1006
|
react.useEffect(() => {
|
|
877
|
-
|
|
878
|
-
if (!isSupported)
|
|
1007
|
+
if (engine !== FilterEngine.TF)
|
|
879
1008
|
return;
|
|
880
1009
|
videoFiltersWeb.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] = react.useState();
|
|
1014
|
+
react.useEffect(() => {
|
|
1015
|
+
if (engine !== FilterEngine.MEDIA_PIPE)
|
|
1016
|
+
return;
|
|
1017
|
+
videoFiltersWeb.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 = react.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 (jsxRuntime.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 && (jsxRuntime.jsx(BackgroundFilters, { tfLite: tfLite, engine: engine, onStats: handleStats }))] }));
|
|
904
1046
|
};
|
|
905
1047
|
const BackgroundFilters = (props) => {
|
|
906
1048
|
const call = videoReactBindings.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 = react.useRef(undefined);
|
|
910
1052
|
handleErrorRef.current = onError;
|
|
1053
|
+
const handleStatsRef = react.useRef(undefined);
|
|
1054
|
+
handleStatsRef.current = props.onStats;
|
|
911
1055
|
react.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 = react.useRef(null);
|
|
924
1070
|
const canvasRef = react.useRef(null);
|
|
925
1071
|
const bgImageRef = react.useRef(null);
|
|
@@ -927,8 +1073,9 @@ const useRenderer = (tfLite, call) => {
|
|
|
927
1073
|
width: 1920,
|
|
928
1074
|
height: 1080,
|
|
929
1075
|
});
|
|
930
|
-
const start = react.useCallback((ms, onError) => {
|
|
1076
|
+
const start = react.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 videoFiltersWeb.VirtualBackground(track, {
|
|
1110
|
+
basePath: basePath,
|
|
1111
|
+
modelPath: modelFilePath,
|
|
960
1112
|
backgroundBlurLevel,
|
|
961
1113
|
backgroundImage,
|
|
962
|
-
});
|
|
963
|
-
renderer = videoFiltersWeb.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
|
+
reactDom.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 = videoFiltersWeb.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 = (jsxRuntime.jsxs("div", { className: "str-video__background-filters", children: [jsxRuntime.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 && (jsxRuntime.jsx("img", { className: "str-video__background-filters__background-image", alt: "Background", ref: bgImageRef, crossOrigin: "anonymous", src: backgroundImage, ...videoSize })), jsxRuntime.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 = react.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 (jsxRuntime.jsxs("div", { ref: refs.setReference, children: [isVisible && (jsxRuntime.jsxs("div", { className:
|
|
1253
|
+
return (jsxRuntime.jsxs("div", { ref: refs.setReference, children: [isVisible && (jsxRuntime.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,
|
|
@@ -1331,6 +1521,21 @@ const ScreenShareButton = (props) => {
|
|
|
1331
1521
|
: 'screen-share-start-button', disabled: disableScreenShareButton, onClick: handleClick, children: jsxRuntime.jsx(Icon, { icon: isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off' }) }) }) }) }));
|
|
1332
1522
|
};
|
|
1333
1523
|
|
|
1524
|
+
const AudioVolumeIndicator = () => {
|
|
1525
|
+
const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
1526
|
+
const { isEnabled, mediaStream } = useMicrophoneState();
|
|
1527
|
+
const [audioLevel, setAudioLevel] = react.useState(0);
|
|
1528
|
+
react.useEffect(() => {
|
|
1529
|
+
if (!isEnabled || !mediaStream)
|
|
1530
|
+
return;
|
|
1531
|
+
const disposeSoundDetector = videoClient.createSoundDetector(mediaStream, ({ audioLevel: al }) => setAudioLevel(al), { detectionFrequencyInMs: 80, destroyStreamOnStop: false });
|
|
1532
|
+
return () => {
|
|
1533
|
+
disposeSoundDetector().catch(console.error);
|
|
1534
|
+
};
|
|
1535
|
+
}, [isEnabled, mediaStream]);
|
|
1536
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__audio-volume-indicator", children: [jsxRuntime.jsx(Icon, { icon: isEnabled ? 'mic' : 'mic-off' }), jsxRuntime.jsx("div", { className: "str-video__audio-volume-indicator__bar", children: jsxRuntime.jsx("div", { className: "str-video__audio-volume-indicator__bar-value", style: { transform: `scaleX(${audioLevel / 100})` } }) })] }));
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1334
1539
|
const SelectContext = react.createContext({});
|
|
1335
1540
|
const Select = (props) => {
|
|
1336
1541
|
const { children, icon, defaultSelectedLabel, defaultSelectedIndex, handleSelect: handleSelectProp, } = props;
|
|
@@ -1441,19 +1646,52 @@ const DeviceSelector = (props) => {
|
|
|
1441
1646
|
return jsxRuntime.jsx(DeviceSelectorDropdown, { ...rest, icon: icon });
|
|
1442
1647
|
};
|
|
1443
1648
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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 } = videoReactBindings.useCallStateHooks();
|
|
1655
|
+
const { selectedDevice } = useSpeakerState();
|
|
1656
|
+
const audioElementRef = react.useRef(null);
|
|
1657
|
+
const [isPlaying, setIsPlaying] = react.useState(false);
|
|
1658
|
+
const { t } = videoReactBindings.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
|
|
1448
1661
|
react.useEffect(() => {
|
|
1449
|
-
|
|
1662
|
+
const audio = audioElementRef.current;
|
|
1663
|
+
if (!audio || !selectedDevice)
|
|
1450
1664
|
return;
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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 = react.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 = react.useCallback(() => setIsPlaying(false), []);
|
|
1694
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__speaker-test", children: [jsxRuntime.jsx("audio", { ref: audioElementRef, onEnded: handleAudioEnded, onPause: handleAudioEnded }), jsxRuntime.jsx(CompositeButton, { className: "str-video__speaker-test__button", onClick: handleStartTest, type: "button", children: jsxRuntime.jsxs("div", { className: "str-video__speaker-test__button-content", children: [jsxRuntime.jsx(Icon, { icon: "speaker" }), isPlaying ? t('Stop test') : t('Test speaker')] }) })] }));
|
|
1457
1695
|
};
|
|
1458
1696
|
|
|
1459
1697
|
const DeviceSelectorAudioInput = ({ title, visualType, volumeIndicatorVisible = true, }) => {
|
|
@@ -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 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("hr", { className: "str-video__device-settings__separator" }), jsxRuntime.jsx(AudioVolumeIndicator, {})] })) }));
|
|
1465
1703
|
};
|
|
1466
|
-
const DeviceSelectorAudioOutput = ({ title, visualType, }) => {
|
|
1704
|
+
const DeviceSelectorAudioOutput = ({ title, visualType, speakerTestVisible = true, speakerTestAudioUrl, }) => {
|
|
1467
1705
|
const { useSpeakerState } = videoReactBindings.useCallStateHooks();
|
|
1468
1706
|
const { speaker, selectedDevice, devices, isDeviceSelectionSupported } = useSpeakerState();
|
|
1469
1707
|
if (!isDeviceSelectionSupported)
|
|
1470
1708
|
return null;
|
|
1471
1709
|
return (jsxRuntime.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 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("hr", { className: "str-video__device-settings__separator" }), jsxRuntime.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 }}",
|
|
@@ -2657,13 +2896,14 @@ const LivestreamLayout = (props) => {
|
|
|
2657
2896
|
? participants.find(videoClient.hasScreenShare)
|
|
2658
2897
|
: undefined;
|
|
2659
2898
|
usePaginatedLayoutSortPreset(call);
|
|
2660
|
-
const
|
|
2661
|
-
const {
|
|
2662
|
-
const floatingParticipantOverlay = hasOngoingScreenShare &&
|
|
2663
|
-
|
|
2664
|
-
, {
|
|
2899
|
+
const { floatingParticipantProps, muted, ParticipantViewUI } = props;
|
|
2900
|
+
const overlay = ParticipantViewUI ?? (jsxRuntime.jsx(ParticipantOverlay, { showParticipantCount: props.showParticipantCount, showDuration: props.showDuration, showLiveBadge: props.showLiveBadge, showSpeakerName: props.showSpeakerName, enableFullScreen: props.enableFullScreen }));
|
|
2901
|
+
const floatingParticipantOverlay = hasOngoingScreenShare &&
|
|
2902
|
+
(ParticipantViewUI ?? (jsxRuntime.jsx(ParticipantOverlay
|
|
2665
2903
|
// these elements aren't needed for the video feed
|
|
2666
|
-
|
|
2904
|
+
, {
|
|
2905
|
+
// these elements aren't needed for the video feed
|
|
2906
|
+
showParticipantCount: floatingParticipantProps?.showParticipantCount ?? false, showDuration: floatingParticipantProps?.showDuration ?? false, showLiveBadge: floatingParticipantProps?.showLiveBadge ?? false, showSpeakerName: floatingParticipantProps?.showSpeakerName ?? true, enableFullScreen: floatingParticipantProps?.enableFullScreen ?? true })));
|
|
2667
2907
|
return (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__wrapper", children: [!muted && jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), hasOngoingScreenShare && presenter && (jsxRuntime.jsx(ParticipantView, { className: "str-video__livestream-layout__screen-share", participant: presenter, ParticipantViewUI: overlay, trackType: "screenShareTrack", muteAudio // audio is rendered by ParticipantsAudio
|
|
2668
2908
|
: true })), currentSpeaker && (jsxRuntime.jsx(ParticipantView, { className: clsx(hasOngoingScreenShare &&
|
|
2669
2909
|
clsx('str-video__livestream-layout__floating-participant', `str-video__livestream-layout__floating-participant--${floatingParticipantProps?.position ?? 'top-right'}`)), participant: currentSpeaker, ParticipantViewUI: floatingParticipantOverlay || overlay, mirror: props.mirrorLocalParticipantVideo !== false ? undefined : false, muteAudio // audio is rendered by ParticipantsAudio
|
|
@@ -2685,13 +2925,18 @@ const BackstageLayout = (props) => {
|
|
|
2685
2925
|
BackstageLayout.displayName = 'BackstageLayout';
|
|
2686
2926
|
const ParticipantOverlay = (props) => {
|
|
2687
2927
|
const { enableFullScreen = true, showParticipantCount = true, showDuration = true, showLiveBadge = true, showSpeakerName = false, } = props;
|
|
2928
|
+
const overlayBarVisible = enableFullScreen ||
|
|
2929
|
+
showParticipantCount ||
|
|
2930
|
+
showDuration ||
|
|
2931
|
+
showLiveBadge ||
|
|
2932
|
+
showSpeakerName;
|
|
2688
2933
|
const { participant } = useParticipantViewContext();
|
|
2689
2934
|
const { useParticipantCount } = videoReactBindings.useCallStateHooks();
|
|
2690
2935
|
const participantCount = useParticipantCount();
|
|
2691
2936
|
const duration = useUpdateCallDuration();
|
|
2692
2937
|
const toggleFullScreen = useToggleFullScreen();
|
|
2693
2938
|
const { t } = videoReactBindings.useI18n();
|
|
2694
|
-
return (jsxRuntime.jsx("div", { className: "str-video__livestream-layout__overlay", children: 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 }))] }) }));
|
|
2939
|
+
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 }))] })) }));
|
|
2695
2940
|
};
|
|
2696
2941
|
const useUpdateCallDuration = () => {
|
|
2697
2942
|
const { useIsCallLive, useCallSession } = videoReactBindings.useCallStateHooks();
|
|
@@ -3020,7 +3265,7 @@ const checkCanJoinEarly = (startsAt, joinAheadTimeSeconds) => {
|
|
|
3020
3265
|
return Date.now() >= +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
|
|
3021
3266
|
};
|
|
3022
3267
|
|
|
3023
|
-
const [major, minor, patch] = ("1.
|
|
3268
|
+
const [major, minor, patch] = ("1.27.0").split('.');
|
|
3024
3269
|
videoClient.setSdkInfo({
|
|
3025
3270
|
type: videoClient.SfuModels.SdkType.REACT,
|
|
3026
3271
|
major,
|
|
@@ -3030,6 +3275,7 @@ videoClient.setSdkInfo({
|
|
|
3030
3275
|
|
|
3031
3276
|
exports.AcceptCallButton = AcceptCallButton;
|
|
3032
3277
|
exports.Audio = Audio;
|
|
3278
|
+
exports.AudioVolumeIndicator = AudioVolumeIndicator;
|
|
3033
3279
|
exports.Avatar = Avatar;
|
|
3034
3280
|
exports.AvatarFallback = AvatarFallback;
|
|
3035
3281
|
exports.BackgroundFiltersProvider = BackgroundFiltersProvider;
|
|
@@ -3092,6 +3338,7 @@ exports.ScreenShareButton = ScreenShareButton;
|
|
|
3092
3338
|
exports.SearchInput = SearchInput;
|
|
3093
3339
|
exports.SearchResults = SearchResults;
|
|
3094
3340
|
exports.SpeakerLayout = SpeakerLayout;
|
|
3341
|
+
exports.SpeakerTest = SpeakerTest;
|
|
3095
3342
|
exports.SpeakingWhileMutedNotification = SpeakingWhileMutedNotification;
|
|
3096
3343
|
exports.SpeechIndicator = SpeechIndicator;
|
|
3097
3344
|
exports.StatCard = StatCard;
|