@stream-io/video-react-sdk 1.34.1 → 1.35.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,6 @@
1
+ import { DeviceListItem } from '../../hooks';
2
+ export type DeviceAudioPreviewItemProps = {
3
+ device: DeviceListItem;
4
+ onSelect: (deviceId: string) => void;
5
+ };
6
+ export declare const DeviceAudioPreviewItem: ({ device, onSelect, }: DeviceAudioPreviewItemProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -1,5 +1,10 @@
1
- import { PropsWithChildren } from 'react';
1
+ import { ComponentType, PropsWithChildren } from 'react';
2
+ import { DeviceListItem } from '../../hooks';
2
3
  export type DeviceSelectorType = 'audioinput' | 'audiooutput' | 'videoinput';
4
+ export type PreviewItemProps = {
5
+ device: DeviceListItem;
6
+ onSelect: (deviceId: string) => void;
7
+ };
3
8
  export declare const DeviceSelector: (props: PropsWithChildren<{
4
9
  devices: MediaDeviceInfo[];
5
10
  icon: string;
@@ -7,5 +12,9 @@ export declare const DeviceSelector: (props: PropsWithChildren<{
7
12
  selectedDeviceId?: string;
8
13
  title?: string;
9
14
  onChange?: (deviceId: string) => void;
15
+ }> & ({
10
16
  visualType?: "list" | "dropdown";
11
- }>) => import("react/jsx-runtime").JSX.Element;
17
+ } | {
18
+ visualType: "preview";
19
+ PreviewItem: ComponentType<PreviewItemProps>;
20
+ })) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  export type DeviceSelectorAudioInputProps = {
2
2
  title?: string;
3
- visualType?: 'list' | 'dropdown';
3
+ visualType?: 'list' | 'dropdown' | 'preview';
4
4
  volumeIndicatorVisible?: boolean;
5
5
  };
6
6
  export declare const DeviceSelectorAudioInput: ({ title, visualType, volumeIndicatorVisible, }: DeviceSelectorAudioInputProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  export type DeviceSelectorVideoProps = {
2
2
  title?: string;
3
- visualType?: 'list' | 'dropdown';
3
+ visualType?: 'list' | 'dropdown' | 'preview';
4
4
  };
5
5
  export declare const DeviceSelectorVideo: ({ title, visualType, }: DeviceSelectorVideoProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { DeviceListItem } from '../../hooks';
2
+ export type DeviceVideoPreviewItemProps = {
3
+ device: DeviceListItem;
4
+ onSelect: (deviceId: string) => void;
5
+ };
6
+ export declare const DeviceVideoPreviewItem: ({ device, onSelect, }: DeviceVideoPreviewItemProps) => import("react/jsx-runtime").JSX.Element | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.34.1",
3
+ "version": "1.35.0",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "types": "./dist/index.d.ts",
@@ -45,9 +45,9 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@floating-ui/react": "^0.27.6",
48
- "@stream-io/video-client": "1.44.5",
49
- "@stream-io/video-filters-web": "0.7.3",
50
- "@stream-io/video-react-bindings": "1.13.14",
48
+ "@stream-io/video-client": "1.46.0",
49
+ "@stream-io/video-filters-web": "0.7.4",
50
+ "@stream-io/video-react-bindings": "1.14.0",
51
51
  "chart.js": "^4.4.4",
52
52
  "clsx": "^2.0.0",
53
53
  "react-chartjs-2": "^5.3.0"
@@ -61,7 +61,7 @@
61
61
  "@rollup/plugin-replace": "^6.0.2",
62
62
  "@rollup/plugin-typescript": "^12.1.4",
63
63
  "@stream-io/audio-filters-web": "^0.7.3",
64
- "@stream-io/video-styling": "^1.12.0",
64
+ "@stream-io/video-styling": "^1.13.0",
65
65
  "@types/react": "~19.1.17",
66
66
  "@types/react-dom": "~19.1.11",
67
67
  "react": "19.1.0",
@@ -0,0 +1,83 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { createSoundDetector } from '@stream-io/video-client';
3
+ import clsx from 'clsx';
4
+ import { DeviceListItem } from '../../hooks';
5
+
6
+ const LEVEL_BARS = 5;
7
+
8
+ const DeviceLevelIndicator = ({ deviceId }: { deviceId: string }) => {
9
+ const [audioLevel, setAudioLevel] = useState(0);
10
+
11
+ useEffect(() => {
12
+ let cancelled = false;
13
+ let dispose: (() => Promise<void>) | undefined;
14
+
15
+ navigator.mediaDevices
16
+ .getUserMedia({
17
+ audio: { deviceId: { exact: deviceId } },
18
+ video: false,
19
+ })
20
+ .then((mediaStream) => {
21
+ if (cancelled) {
22
+ mediaStream.getTracks().forEach((t) => t.stop());
23
+ return;
24
+ }
25
+ dispose = createSoundDetector(
26
+ mediaStream,
27
+ ({ audioLevel: al }) => setAudioLevel(al),
28
+ { detectionFrequencyInMs: 80 },
29
+ );
30
+ })
31
+ .catch(console.error);
32
+
33
+ return () => {
34
+ cancelled = true;
35
+ dispose?.().catch(console.error);
36
+ };
37
+ }, [deviceId]);
38
+
39
+ const activeBars = Math.round((audioLevel / 100) * LEVEL_BARS);
40
+
41
+ return (
42
+ <div className="str-video__device-level-indicator" aria-label="Audio level">
43
+ {Array.from({ length: LEVEL_BARS }, (_, i) => (
44
+ <div
45
+ key={i}
46
+ className={clsx('str-video__device-level-indicator__bar', {
47
+ 'str-video__device-level-indicator__bar--active': i < activeBars,
48
+ })}
49
+ />
50
+ ))}
51
+ </div>
52
+ );
53
+ };
54
+
55
+ export type DeviceAudioPreviewItemProps = {
56
+ device: DeviceListItem;
57
+ onSelect: (deviceId: string) => void;
58
+ };
59
+
60
+ export const DeviceAudioPreviewItem = ({
61
+ device,
62
+ onSelect,
63
+ }: DeviceAudioPreviewItemProps) => {
64
+ if (device.deviceId === 'default') return null;
65
+
66
+ return (
67
+ <label
68
+ className={`str-video__device-settings__option${device.isSelected ? ' str-video__device-settings__option--selected' : ''}`}
69
+ htmlFor={`audioinput--${device.deviceId}`}
70
+ >
71
+ <input
72
+ type="radio"
73
+ name="audioinput"
74
+ value={device.deviceId}
75
+ id={`audioinput--${device.deviceId}`}
76
+ checked={device.isSelected}
77
+ onChange={(e) => onSelect(e.target.value)}
78
+ />
79
+ {device.label}
80
+ <DeviceLevelIndicator deviceId={device.deviceId} />
81
+ </label>
82
+ );
83
+ };
@@ -1,7 +1,12 @@
1
1
  import clsx from 'clsx';
2
- import { ChangeEventHandler, PropsWithChildren, useCallback } from 'react';
2
+ import {
3
+ ChangeEventHandler,
4
+ ComponentType,
5
+ PropsWithChildren,
6
+ useCallback,
7
+ } from 'react';
3
8
 
4
- import { useDeviceList } from '../../hooks';
9
+ import { DeviceListItem, useDeviceList } from '../../hooks';
5
10
  import { DropDownSelect, DropDownSelectOption } from '../DropdownSelect';
6
11
  import { useMenuContext } from '../Menu';
7
12
 
@@ -102,6 +107,59 @@ const DeviceSelectorList = (
102
107
  );
103
108
  };
104
109
 
110
+ export type PreviewItemProps = {
111
+ device: DeviceListItem;
112
+ onSelect: (deviceId: string) => void;
113
+ };
114
+
115
+ const DeviceSelectorPreview = (
116
+ props: PropsWithChildren<{
117
+ devices: MediaDeviceInfo[];
118
+ selectedDeviceId?: string;
119
+ title?: string;
120
+ onChange?: (deviceId: string) => void;
121
+ PreviewItem: ComponentType<PreviewItemProps>;
122
+ }>,
123
+ ) => {
124
+ const {
125
+ devices = [],
126
+ selectedDeviceId,
127
+ title,
128
+ onChange,
129
+ children,
130
+ PreviewItem,
131
+ } = props;
132
+ const { close } = useMenuContext();
133
+ const { deviceList } = useDeviceList(devices, selectedDeviceId);
134
+
135
+ const onSelect = useCallback(
136
+ (deviceId: string) => {
137
+ if (deviceId === 'default') return;
138
+ onChange?.(deviceId);
139
+ close?.();
140
+ },
141
+ [onChange, close],
142
+ );
143
+
144
+ return (
145
+ <div className="str-video__device-settings__device-kind">
146
+ {title && (
147
+ <div className="str-video__device-settings__device-selector-title">
148
+ {title}
149
+ </div>
150
+ )}
151
+ {deviceList.map((device) => (
152
+ <PreviewItem
153
+ key={device.deviceId}
154
+ device={device}
155
+ onSelect={onSelect}
156
+ />
157
+ ))}
158
+ {children}
159
+ </div>
160
+ );
161
+ };
162
+
105
163
  const DeviceSelectorDropdown = (props: {
106
164
  devices: MediaDeviceInfo[];
107
165
  selectedDeviceId?: string;
@@ -157,11 +215,18 @@ export const DeviceSelector = (
157
215
  selectedDeviceId?: string;
158
216
  title?: string;
159
217
  onChange?: (deviceId: string) => void;
160
- visualType?: 'list' | 'dropdown';
161
- }>,
218
+ }> &
219
+ (
220
+ | { visualType?: 'list' | 'dropdown' }
221
+ | { visualType: 'preview'; PreviewItem: ComponentType<PreviewItemProps> }
222
+ ),
162
223
  ) => {
163
- const { visualType = 'list', icon, ...rest } = props;
224
+ if (props.visualType === 'preview') {
225
+ const { PreviewItem, ...rest } = props;
226
+ return <DeviceSelectorPreview {...rest} PreviewItem={PreviewItem} />;
227
+ }
164
228
 
229
+ const { visualType = 'list', icon, ...rest } = props;
165
230
  if (visualType === 'list') {
166
231
  return <DeviceSelectorList {...rest} />;
167
232
  }
@@ -1,11 +1,12 @@
1
1
  import { useCallStateHooks } from '@stream-io/video-react-bindings';
2
+ import { DeviceAudioPreviewItem } from './DeviceAudioPreviewItem';
2
3
  import { DeviceSelector } from './DeviceSelector';
3
4
  import { AudioVolumeIndicator } from './AudioVolumeIndicator';
4
5
  import { SpeakerTest } from './SpeakerTest';
5
6
 
6
7
  export type DeviceSelectorAudioInputProps = {
7
8
  title?: string;
8
- visualType?: 'list' | 'dropdown';
9
+ visualType?: 'list' | 'dropdown' | 'preview';
9
10
  volumeIndicatorVisible?: boolean;
10
11
  };
11
12
 
@@ -28,6 +29,7 @@ export const DeviceSelectorAudioInput = ({
28
29
  title={title}
29
30
  visualType={visualType}
30
31
  icon="mic"
32
+ PreviewItem={DeviceAudioPreviewItem}
31
33
  >
32
34
  {volumeIndicatorVisible && (
33
35
  <>
@@ -1,9 +1,11 @@
1
- import { DeviceSelector } from './DeviceSelector';
2
1
  import { useCallStateHooks } from '@stream-io/video-react-bindings';
3
2
 
3
+ import { DeviceSelector } from './DeviceSelector';
4
+ import { DeviceVideoPreviewItem } from './DeviceVideoPreviewItem';
5
+
4
6
  export type DeviceSelectorVideoProps = {
5
7
  title?: string;
6
- visualType?: 'list' | 'dropdown';
8
+ visualType?: 'list' | 'dropdown' | 'preview';
7
9
  };
8
10
 
9
11
  export const DeviceSelectorVideo = ({
@@ -24,6 +26,7 @@ export const DeviceSelectorVideo = ({
24
26
  title={title}
25
27
  visualType={visualType}
26
28
  icon="camera"
29
+ PreviewItem={DeviceVideoPreviewItem}
27
30
  />
28
31
  );
29
32
  };
@@ -0,0 +1,73 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import clsx from 'clsx';
3
+ import { DeviceListItem } from '../../hooks';
4
+
5
+ const DeviceVideoPreview = ({ deviceId }: { deviceId: string }) => {
6
+ const videoRef = useRef<HTMLVideoElement>(null);
7
+
8
+ useEffect(() => {
9
+ let cancelled = false;
10
+ let stream: MediaStream | undefined;
11
+
12
+ navigator.mediaDevices
13
+ .getUserMedia({
14
+ video: { deviceId: { exact: deviceId } },
15
+ audio: false,
16
+ })
17
+ .then((mediaStream) => {
18
+ if (cancelled) {
19
+ mediaStream.getTracks().forEach((t) => t.stop());
20
+ return;
21
+ }
22
+
23
+ stream = mediaStream;
24
+ if (videoRef.current) {
25
+ videoRef.current.srcObject = mediaStream;
26
+ }
27
+ })
28
+ .catch(console.error);
29
+
30
+ return () => {
31
+ cancelled = true;
32
+ stream?.getTracks().forEach((t) => t.stop());
33
+ };
34
+ }, [deviceId]);
35
+
36
+ return (
37
+ <div className="str-video__device-video-preview">
38
+ <video
39
+ ref={videoRef}
40
+ autoPlay
41
+ playsInline
42
+ muted
43
+ className="str-video__device-video-preview__video"
44
+ />
45
+ </div>
46
+ );
47
+ };
48
+
49
+ export type DeviceVideoPreviewItemProps = {
50
+ device: DeviceListItem;
51
+ onSelect: (deviceId: string) => void;
52
+ };
53
+
54
+ export const DeviceVideoPreviewItem = ({
55
+ device,
56
+ onSelect,
57
+ }: DeviceVideoPreviewItemProps) => {
58
+ if (device.deviceId === 'default') return null;
59
+
60
+ return (
61
+ <button
62
+ type="button"
63
+ className={clsx('str-video__device-preview', {
64
+ 'str-video__device-preview--selected': device.isSelected,
65
+ })}
66
+ onClick={() => onSelect(device.deviceId)}
67
+ aria-pressed={device.isSelected}
68
+ >
69
+ <DeviceVideoPreview deviceId={device.deviceId} />
70
+ <span className="str-video__device-preview__label">{device.label}</span>
71
+ </button>
72
+ );
73
+ };
@@ -165,11 +165,10 @@ export const ParticipantDetails = ({
165
165
  className="str-video__participant-details__name--track-paused"
166
166
  />
167
167
  )}
168
- {indicatorsVisible && canUnpin && (
169
- // TODO: remove this monstrosity once we have a proper design
168
+ {indicatorsVisible && pin && (
170
169
  <span
171
- title={t('Unpin')}
172
- onClick={() => call?.unpin(sessionId)}
170
+ title={canUnpin ? t('Unpin') : t('Pinned')}
171
+ onClick={canUnpin ? () => call?.unpin(sessionId) : undefined}
173
172
  className="str-video__participant-details__name--pinned"
174
173
  />
175
174
  )}