@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.
@@ -0,0 +1,75 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
3
+ import { CompositeButton } from '../Button';
4
+ import { Icon } from '../Icon';
5
+
6
+ /**
7
+ * SpeakerTest component that plays a test audio through the selected speaker.
8
+ * This allows users to verify their audio output device is working correctly.
9
+ */
10
+ export const SpeakerTest = (props: { audioUrl?: string }) => {
11
+ const { useSpeakerState } = useCallStateHooks();
12
+ const { selectedDevice } = useSpeakerState();
13
+ const audioElementRef = useRef<HTMLAudioElement | null>(null);
14
+ const [isPlaying, setIsPlaying] = useState(false);
15
+ const { t } = useI18n();
16
+
17
+ const {
18
+ audioUrl = `https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}/assets/piano.mp3`,
19
+ } = props;
20
+
21
+ // Update audio output device when selection changes
22
+ useEffect(() => {
23
+ const audio = audioElementRef.current;
24
+ if (!audio || !selectedDevice) return;
25
+
26
+ // Set the sinkId to route audio to the selected speaker
27
+ if ('setSinkId' in audio) {
28
+ audio.setSinkId(selectedDevice).catch((err) => {
29
+ console.error('Failed to set audio output device:', err);
30
+ });
31
+ }
32
+ }, [selectedDevice]);
33
+
34
+ const handleStartTest = useCallback(async () => {
35
+ const audio = audioElementRef.current;
36
+ if (!audio) return;
37
+
38
+ audio.src = audioUrl;
39
+
40
+ try {
41
+ if (isPlaying) {
42
+ audio.pause();
43
+ audio.currentTime = 0;
44
+ setIsPlaying(false);
45
+ } else {
46
+ await audio.play();
47
+ setIsPlaying(true);
48
+ }
49
+ } catch (err) {
50
+ console.error('Failed to play test audio:', err);
51
+ setIsPlaying(false);
52
+ }
53
+ }, [isPlaying, audioUrl]);
54
+
55
+ const handleAudioEnded = useCallback(() => setIsPlaying(false), []);
56
+ return (
57
+ <div className="str-video__speaker-test">
58
+ <audio
59
+ ref={audioElementRef}
60
+ onEnded={handleAudioEnded}
61
+ onPause={handleAudioEnded}
62
+ />
63
+ <CompositeButton
64
+ className="str-video__speaker-test__button"
65
+ onClick={handleStartTest}
66
+ type="button"
67
+ >
68
+ <div className="str-video__speaker-test__button-content">
69
+ <Icon icon="speaker" />
70
+ {isPlaying ? t('Stop test') : t('Test speaker')}
71
+ </div>
72
+ </CompositeButton>
73
+ </div>
74
+ );
75
+ };
@@ -1,4 +1,6 @@
1
+ export * from './AudioVolumeIndicator';
1
2
  export * from './DeviceSettings';
2
3
  export * from './DeviceSelector';
3
4
  export * from './DeviceSelectorAudio';
4
5
  export * from './DeviceSelectorVideo';
6
+ export * from './SpeakerTest';
@@ -1,6 +1,6 @@
1
1
  import { PropsWithChildren, ReactNode, useEffect } from 'react';
2
+ import clsx from 'clsx';
2
3
  import { Placement } from '@floating-ui/react';
3
-
4
4
  import { useFloatingUIPreset } from '../../hooks';
5
5
 
6
6
  export type NotificationProps = {
@@ -9,6 +9,7 @@ export type NotificationProps = {
9
9
  visibilityTimeout?: number;
10
10
  resetIsVisible?: () => void;
11
11
  placement?: Placement;
12
+ className?: string;
12
13
  iconClassName?: string | null;
13
14
  close?: () => void;
14
15
  };
@@ -21,6 +22,7 @@ export const Notification = (props: PropsWithChildren<NotificationProps>) => {
21
22
  visibilityTimeout,
22
23
  resetIsVisible,
23
24
  placement = 'top',
25
+ className,
24
26
  iconClassName = 'str-video__notification__icon',
25
27
  close,
26
28
  } = props;
@@ -44,7 +46,7 @@ export const Notification = (props: PropsWithChildren<NotificationProps>) => {
44
46
  <div ref={refs.setReference}>
45
47
  {isVisible && (
46
48
  <div
47
- className="str-video__notification"
49
+ className={clsx('str-video__notification', className)}
48
50
  ref={refs.setFloating}
49
51
  style={{
50
52
  position: strategy,
@@ -1,5 +1,11 @@
1
1
  import clsx from 'clsx';
2
- import { useCallback, useEffect, useState } from 'react';
2
+ import {
3
+ ComponentType,
4
+ ReactElement,
5
+ useCallback,
6
+ useEffect,
7
+ useState,
8
+ } from 'react';
3
9
  import {
4
10
  useCall,
5
11
  useCallStateHooks,
@@ -62,6 +68,11 @@ export type LivestreamLayoutProps = {
62
68
  */
63
69
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
64
70
  };
71
+
72
+ /**
73
+ * Override the default participant view overlay UI.
74
+ */
75
+ ParticipantViewUI?: ComponentType | ReactElement | null;
65
76
  };
66
77
 
67
78
  export const LivestreamLayout = (props: LivestreamLayoutProps) => {
@@ -77,27 +88,31 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
77
88
 
78
89
  usePaginatedLayoutSortPreset(call);
79
90
 
80
- const overlay = (
91
+ const { floatingParticipantProps, muted, ParticipantViewUI } = props;
92
+ const overlay = ParticipantViewUI ?? (
81
93
  <ParticipantOverlay
82
94
  showParticipantCount={props.showParticipantCount}
83
95
  showDuration={props.showDuration}
84
96
  showLiveBadge={props.showLiveBadge}
85
97
  showSpeakerName={props.showSpeakerName}
98
+ enableFullScreen={props.enableFullScreen}
86
99
  />
87
100
  );
88
101
 
89
- const { floatingParticipantProps, muted } = props;
90
- const floatingParticipantOverlay = hasOngoingScreenShare && (
91
- <ParticipantOverlay
92
- // these elements aren't needed for the video feed
93
- showParticipantCount={
94
- floatingParticipantProps?.showParticipantCount ?? false
95
- }
96
- showDuration={floatingParticipantProps?.showDuration ?? false}
97
- showLiveBadge={floatingParticipantProps?.showLiveBadge ?? false}
98
- showSpeakerName={floatingParticipantProps?.showSpeakerName ?? true}
99
- />
100
- );
102
+ const floatingParticipantOverlay =
103
+ hasOngoingScreenShare &&
104
+ (ParticipantViewUI ?? (
105
+ <ParticipantOverlay
106
+ // these elements aren't needed for the video feed
107
+ showParticipantCount={
108
+ floatingParticipantProps?.showParticipantCount ?? false
109
+ }
110
+ showDuration={floatingParticipantProps?.showDuration ?? false}
111
+ showLiveBadge={floatingParticipantProps?.showLiveBadge ?? false}
112
+ showSpeakerName={floatingParticipantProps?.showSpeakerName ?? true}
113
+ enableFullScreen={floatingParticipantProps?.enableFullScreen ?? true}
114
+ />
115
+ ));
101
116
 
102
117
  return (
103
118
  <div className="str-video__livestream-layout__wrapper">
@@ -192,6 +207,12 @@ const ParticipantOverlay = (props: {
192
207
  showLiveBadge = true,
193
208
  showSpeakerName = false,
194
209
  } = props;
210
+ const overlayBarVisible =
211
+ enableFullScreen ||
212
+ showParticipantCount ||
213
+ showDuration ||
214
+ showLiveBadge ||
215
+ showSpeakerName;
195
216
  const { participant } = useParticipantViewContext();
196
217
  const { useParticipantCount } = useCallStateHooks();
197
218
  const participantCount = useParticipantCount();
@@ -200,37 +221,39 @@ const ParticipantOverlay = (props: {
200
221
  const { t } = useI18n();
201
222
  return (
202
223
  <div className="str-video__livestream-layout__overlay">
203
- <div className="str-video__livestream-layout__overlay__bar">
204
- {showLiveBadge && (
205
- <span className="str-video__livestream-layout__live-badge">
206
- {t('Live')}
207
- </span>
208
- )}
209
- {showParticipantCount && (
210
- <span className="str-video__livestream-layout__viewers-count">
211
- {participantCount}
212
- </span>
213
- )}
214
- {showSpeakerName && (
215
- <span
216
- className="str-video__livestream-layout__speaker-name"
217
- title={participant.name || participant.userId || ''}
218
- >
219
- {participant.name || participant.userId || ''}
220
- </span>
221
- )}
222
- {showDuration && (
223
- <span className="str-video__livestream-layout__duration">
224
- {formatDuration(duration)}
225
- </span>
226
- )}
227
- {enableFullScreen && (
228
- <span
229
- className="str-video__livestream-layout__go-fullscreen"
230
- onClick={toggleFullScreen}
231
- />
232
- )}
233
- </div>
224
+ {overlayBarVisible && (
225
+ <div className="str-video__livestream-layout__overlay__bar">
226
+ {showLiveBadge && (
227
+ <span className="str-video__livestream-layout__live-badge">
228
+ {t('Live')}
229
+ </span>
230
+ )}
231
+ {showParticipantCount && (
232
+ <span className="str-video__livestream-layout__viewers-count">
233
+ {participantCount}
234
+ </span>
235
+ )}
236
+ {showSpeakerName && (
237
+ <span
238
+ className="str-video__livestream-layout__speaker-name"
239
+ title={participant.name || participant.userId || ''}
240
+ >
241
+ {participant.name || participant.userId || ''}
242
+ </span>
243
+ )}
244
+ {showDuration && (
245
+ <span className="str-video__livestream-layout__duration">
246
+ {formatDuration(duration)}
247
+ </span>
248
+ )}
249
+ {enableFullScreen && (
250
+ <span
251
+ className="str-video__livestream-layout__go-fullscreen"
252
+ onClick={toggleFullScreen}
253
+ />
254
+ )}
255
+ </div>
256
+ )}
234
257
  </div>
235
258
  );
236
259
  };
@@ -11,6 +11,7 @@
11
11
  "Speakers": "Speakers",
12
12
  "Video": "Video",
13
13
  "You are muted. Unmute to speak.": "You are muted. Unmute to speak.",
14
+ "Background filters performance is degraded. Consider disabling filters for better performance.": "Background filters performance is degraded. Consider disabling filters for better performance.",
14
15
 
15
16
  "Live": "Live",
16
17
  "Livestream starts soon": "Livestream starts soon",