@stream-io/video-react-sdk 1.38.0 → 1.39.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +26 -6
  2. package/dist/css/embedded.css +1 -0
  3. package/dist/css/embedded.css.map +1 -1
  4. package/dist/css/styles.css +1 -0
  5. package/dist/css/styles.css.map +1 -1
  6. package/dist/embedded.cjs.js +57 -38
  7. package/dist/embedded.cjs.js.map +1 -1
  8. package/dist/embedded.es.js +59 -40
  9. package/dist/embedded.es.js.map +1 -1
  10. package/dist/index.cjs.js +57 -39
  11. package/dist/index.cjs.js.map +1 -1
  12. package/dist/index.es.js +59 -41
  13. package/dist/index.es.js.map +1 -1
  14. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.d.ts +1 -1
  15. package/dist/src/core/components/Audio/Audio.d.ts +1 -4
  16. package/dist/src/core/components/ParticipantView/ParticipantView.d.ts +1 -1
  17. package/package.json +9 -7
  18. package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +50 -46
  19. package/src/components/CallParticipantsList/CallParticipantsList.tsx +17 -6
  20. package/src/components/DeviceSettings/AudioVolumeIndicator.tsx +8 -4
  21. package/src/components/Search/SearchInput.tsx +4 -2
  22. package/src/core/components/Audio/Audio.tsx +4 -4
  23. package/src/core/components/ParticipantView/ParticipantView.tsx +4 -3
  24. package/src/core/components/Video/BaseVideo.tsx +3 -1
  25. package/src/core/components/Video/Video.tsx +5 -2
  26. package/src/embedded/hooks/useWakeLock.ts +1 -0
  27. package/src/embedded/livestream/viewer/ViewerLobby.tsx +5 -6
  28. package/src/hooks/useDragToScroll.ts +9 -4
  29. package/src/hooks/useScrollPosition.ts +2 -2
  30. package/src/wrappers/LivestreamPlayer/LivestreamPlayer.tsx +5 -6
@@ -8,5 +8,5 @@ type CallParticipantListingItemProps = {
8
8
  participant: StreamVideoParticipant;
9
9
  }>;
10
10
  };
11
- export declare const CallParticipantListingItem: ({ participant, DisplayName, }: CallParticipantListingItemProps) => import("react/jsx-runtime").JSX.Element;
11
+ export declare const CallParticipantListingItem: import("react").MemoExoticComponent<({ participant, DisplayName, }: CallParticipantListingItemProps) => import("react/jsx-runtime").JSX.Element>;
12
12
  export {};
@@ -11,7 +11,4 @@ export type AudioProps = ComponentPropsWithoutRef<'audio'> & {
11
11
  */
12
12
  trackType?: AudioTrackType;
13
13
  };
14
- export declare const Audio: {
15
- ({ participant, trackType, ...rest }: AudioProps): import("react/jsx-runtime").JSX.Element;
16
- displayName: string;
17
- };
14
+ export declare const Audio: import("react").MemoExoticComponent<({ participant, trackType, ...rest }: AudioProps) => import("react/jsx-runtime").JSX.Element>;
@@ -42,7 +42,7 @@ export type ParticipantViewProps = {
42
42
  */
43
43
  className?: string;
44
44
  } & Pick<VideoProps, 'VideoPlaceholder' | 'PictureInPicturePlaceholder'>;
45
- export declare const ParticipantView: import("react").ForwardRefExoticComponent<{
45
+ export declare const ParticipantView: import("react").NamedExoticComponent<{
46
46
  /**
47
47
  * The participant whose video/audio stream we want to play.
48
48
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.38.0",
3
+ "version": "1.39.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.19",
48
- "@stream-io/video-client": "1.54.0",
49
- "@stream-io/video-filters-web": "0.7.5",
50
- "@stream-io/video-react-bindings": "1.17.0",
48
+ "@stream-io/video-client": "1.55.0",
49
+ "@stream-io/video-filters-web": "0.8.0",
50
+ "@stream-io/video-react-bindings": "1.18.0",
51
51
  "chart.js": "^4.5.1",
52
52
  "clsx": "^2.1.1",
53
53
  "react-chartjs-2": "^5.3.1"
@@ -60,14 +60,16 @@
60
60
  "@rollup/plugin-json": "^6.1.0",
61
61
  "@rollup/plugin-replace": "^6.0.3",
62
62
  "@rollup/plugin-typescript": "^12.3.0",
63
- "@stream-io/audio-filters-web": "^0.8.2",
64
- "@stream-io/video-styling": "^1.14.1",
63
+ "@stream-io/audio-filters-web": "^0.9.0",
64
+ "@stream-io/typescript-config": "^0.0.0",
65
+ "@stream-io/video-styling": "^1.14.2",
66
+ "@types/node": "^25.9.3",
65
67
  "@types/react": "~19.2.15",
66
68
  "@types/react-dom": "~19.2.3",
67
69
  "react": "19.2.3",
68
70
  "react-dom": "19.2.3",
69
71
  "rimraf": "^6.1.3",
70
72
  "rollup": "^4.60.4",
71
- "typescript": "^5.9.3"
73
+ "typescript": "^6.0.3"
72
74
  }
73
75
  }
@@ -1,5 +1,5 @@
1
1
  import clsx from 'clsx';
2
- import { ComponentProps, ComponentType, forwardRef } from 'react';
2
+ import { ComponentProps, ComponentType, forwardRef, memo } from 'react';
3
3
  import { useConnectedUser, useI18n } from '@stream-io/video-react-bindings';
4
4
  import {
5
5
  hasAudio,
@@ -20,60 +20,64 @@ type CallParticipantListingItemProps = {
20
20
  /** Custom component used to display participant's name */
21
21
  DisplayName?: ComponentType<{ participant: StreamVideoParticipant }>;
22
22
  };
23
- export const CallParticipantListingItem = ({
24
- participant,
25
- DisplayName = DefaultDisplayName,
26
- }: CallParticipantListingItemProps) => {
27
- const isAudioOn = hasAudio(participant);
28
- const isVideoOn = hasVideo(participant);
29
- const isPinnedOn = isPinned(participant);
23
+ export const CallParticipantListingItem = memo(
24
+ function CallParticipantListingItem({
25
+ participant,
26
+ DisplayName = DefaultDisplayName,
27
+ }: CallParticipantListingItemProps) {
28
+ const isAudioOn = hasAudio(participant);
29
+ const isVideoOn = hasVideo(participant);
30
+ const isPinnedOn = isPinned(participant);
30
31
 
31
- const { t } = useI18n();
32
+ const { t } = useI18n();
32
33
 
33
- return (
34
- <div className="str-video__participant-listing-item">
35
- <Avatar name={participant.name} imageSrc={participant.image} />
36
- <DisplayName participant={participant} />
37
- <div className="str-video__participant-listing-item__media-indicator-group">
38
- <MediaIndicator
39
- title={isAudioOn ? t('Microphone on') : t('Microphone off')}
40
- className={clsx(
41
- 'str-video__participant-listing-item__icon',
42
- `str-video__participant-listing-item__icon-${
43
- isAudioOn ? 'mic' : 'mic-off'
44
- }`,
45
- )}
46
- />
47
- <MediaIndicator
48
- title={isVideoOn ? t('Camera on') : t('Camera off')}
49
- className={clsx(
50
- 'str-video__participant-listing-item__icon',
51
- `str-video__participant-listing-item__icon-${
52
- isVideoOn ? 'camera' : 'camera-off'
53
- }`,
54
- )}
55
- />
56
- {isPinnedOn && (
34
+ return (
35
+ <div className="str-video__participant-listing-item">
36
+ <Avatar name={participant.name} imageSrc={participant.image} />
37
+ <DisplayName participant={participant} />
38
+ <div className="str-video__participant-listing-item__media-indicator-group">
57
39
  <MediaIndicator
58
- title={t('Pinned')}
40
+ title={isAudioOn ? t('Microphone on') : t('Microphone off')}
59
41
  className={clsx(
60
42
  'str-video__participant-listing-item__icon',
61
- 'str-video__participant-listing-item__icon-pinned',
43
+ `str-video__participant-listing-item__icon-${
44
+ isAudioOn ? 'mic' : 'mic-off'
45
+ }`,
62
46
  )}
63
47
  />
64
- )}
48
+ <MediaIndicator
49
+ title={isVideoOn ? t('Camera on') : t('Camera off')}
50
+ className={clsx(
51
+ 'str-video__participant-listing-item__icon',
52
+ `str-video__participant-listing-item__icon-${
53
+ isVideoOn ? 'camera' : 'camera-off'
54
+ }`,
55
+ )}
56
+ />
57
+ {isPinnedOn && (
58
+ <MediaIndicator
59
+ title={t('Pinned')}
60
+ className={clsx(
61
+ 'str-video__participant-listing-item__icon',
62
+ 'str-video__participant-listing-item__icon-pinned',
63
+ )}
64
+ />
65
+ )}
65
66
 
66
- <MenuToggle placement="bottom-end" ToggleButton={ToggleButton}>
67
- <ParticipantViewContext.Provider
68
- value={{ participant, trackType: 'none' }}
69
- >
70
- <ParticipantActionsContextMenu />
71
- </ParticipantViewContext.Provider>
72
- </MenuToggle>
67
+ <MenuToggle placement="bottom-end" ToggleButton={ToggleButton}>
68
+ <ParticipantViewContext.Provider
69
+ value={{ participant, trackType: 'none' }}
70
+ >
71
+ <ParticipantActionsContextMenu />
72
+ </ParticipantViewContext.Provider>
73
+ </MenuToggle>
74
+ </div>
73
75
  </div>
74
- </div>
75
- );
76
- };
76
+ );
77
+ },
78
+ );
79
+
80
+ CallParticipantListingItem.displayName = 'CallParticipantListingItem';
77
81
 
78
82
  const MediaIndicator = (props: ComponentProps<'div'>) => (
79
83
  <WithTooltip {...props} />
@@ -3,6 +3,7 @@ import {
3
3
  forwardRef,
4
4
  SetStateAction,
5
5
  useCallback,
6
+ useRef,
6
7
  useState,
7
8
  } from 'react';
8
9
  import {
@@ -150,13 +151,21 @@ const ActiveUsersSearchResults = ({
150
151
  const { useParticipants } = useCallStateHooks();
151
152
  const participants = useParticipants({ sortBy: name });
152
153
 
154
+ // cache: each distinct name is normalized once
155
+ const normalizedNameCache = useRef<Map<string, string> | null>(null);
156
+
153
157
  const activeUsersSearchFn = useCallback(
154
158
  async (queryString: string) => {
155
159
  const normalizedQuery = normalizeString(queryString);
156
- const queryRegExp = new RegExp(normalizedQuery, 'i');
157
- return participants.filter((p) =>
158
- normalizeString(p.name).match(queryRegExp),
159
- );
160
+ const cache = (normalizedNameCache.current ??= new Map<string, string>());
161
+ return participants.filter((p) => {
162
+ let normalizedName = cache.get(p.name);
163
+ if (normalizedName === undefined) {
164
+ normalizedName = normalizeString(p.name);
165
+ cache.set(p.name, normalizedName);
166
+ }
167
+ return normalizedName.includes(normalizedQuery);
168
+ });
160
169
  },
161
170
  [participants],
162
171
  );
@@ -191,8 +200,10 @@ const BlockedUsersSearchResults = ({
191
200
 
192
201
  const blockedUsersSearchFn = useCallback(
193
202
  async (queryString: string) => {
194
- const queryRegExp = new RegExp(queryString, 'i');
195
- return blockedUsers.filter((userId) => userId.match(queryRegExp));
203
+ const normalizedQuery = queryString.toLowerCase();
204
+ return blockedUsers.filter((userId) =>
205
+ userId.toLowerCase().includes(normalizedQuery),
206
+ );
196
207
  },
197
208
  [blockedUsers],
198
209
  );
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { createSoundDetector } from '@stream-io/video-client';
3
3
  import { useCallStateHooks } from '@stream-io/video-react-bindings';
4
4
  import { Icon } from '../Icon';
@@ -6,13 +6,17 @@ import { Icon } from '../Icon';
6
6
  export const AudioVolumeIndicator = () => {
7
7
  const { useMicrophoneState } = useCallStateHooks();
8
8
  const { isEnabled, mediaStream } = useMicrophoneState();
9
- const [audioLevel, setAudioLevel] = useState(0);
9
+ const barRef = useRef<HTMLDivElement>(null);
10
10
 
11
11
  useEffect(() => {
12
12
  if (!isEnabled || !mediaStream) return;
13
13
  const disposeSoundDetector = createSoundDetector(
14
14
  mediaStream,
15
- ({ audioLevel: al }) => setAudioLevel(al),
15
+ ({ audioLevel: al }) => {
16
+ if (barRef.current) {
17
+ barRef.current.style.transform = `scaleX(${al / 100})`;
18
+ }
19
+ },
16
20
  { detectionFrequencyInMs: 80, destroyStreamOnStop: false },
17
21
  );
18
22
  return () => {
@@ -25,8 +29,8 @@ export const AudioVolumeIndicator = () => {
25
29
  <Icon icon={isEnabled ? 'mic' : 'mic-off'} />
26
30
  <div className="str-video__audio-volume-indicator__bar">
27
31
  <div
32
+ ref={barRef}
28
33
  className="str-video__audio-volume-indicator__bar-value"
29
- style={{ transform: `scaleX(${audioLevel / 100})` }}
30
34
  />
31
35
  </div>
32
36
  </div>
@@ -1,4 +1,5 @@
1
1
  import { ComponentProps, useEffect, useState } from 'react';
2
+ import { useEffectEvent } from '@stream-io/video-react-bindings';
2
3
  import clsx from 'clsx';
3
4
 
4
5
  type SearchInputProps = ComponentProps<'input'> & {
@@ -20,11 +21,12 @@ export const SearchInput = ({
20
21
  const [inputElement, setInputElement] = useState<HTMLInputElement | null>(
21
22
  null,
22
23
  );
24
+ const onExitSearch = useEffectEvent(exitSearch);
23
25
  useEffect(() => {
24
26
  if (!inputElement) return;
25
27
 
26
28
  const handleKeyDown = (e: KeyboardEvent) => {
27
- if (e.key.toLowerCase() === 'escape') exitSearch();
29
+ if (e.key.toLowerCase() === 'escape') onExitSearch();
28
30
  };
29
31
 
30
32
  inputElement.addEventListener('keydown', handleKeyDown);
@@ -32,7 +34,7 @@ export const SearchInput = ({
32
34
  return () => {
33
35
  inputElement.removeEventListener('keydown', handleKeyDown);
34
36
  };
35
- }, [exitSearch, inputElement]);
37
+ }, [inputElement]);
36
38
 
37
39
  return (
38
40
  <div
@@ -1,4 +1,4 @@
1
- import { ComponentPropsWithoutRef, useEffect, useState } from 'react';
1
+ import { ComponentPropsWithoutRef, memo, useEffect, useState } from 'react';
2
2
  import {
3
3
  AudioTrackType,
4
4
  StreamVideoParticipant,
@@ -18,11 +18,11 @@ export type AudioProps = ComponentPropsWithoutRef<'audio'> & {
18
18
  trackType?: AudioTrackType;
19
19
  };
20
20
 
21
- export const Audio = ({
21
+ export const Audio = memo(function Audio({
22
22
  participant,
23
23
  trackType = 'audioTrack',
24
24
  ...rest
25
- }: AudioProps) => {
25
+ }: AudioProps) {
26
26
  const call = useCall();
27
27
  const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
28
28
  null,
@@ -47,6 +47,6 @@ export const Audio = ({
47
47
  data-track-type={trackType}
48
48
  />
49
49
  );
50
- };
50
+ });
51
51
 
52
52
  Audio.displayName = 'Audio';
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ComponentType,
3
3
  forwardRef,
4
+ memo,
4
5
  ReactElement,
5
6
  useMemo,
6
7
  useState,
@@ -70,8 +71,8 @@ export type ParticipantViewProps = {
70
71
  className?: string;
71
72
  } & Pick<VideoProps, 'VideoPlaceholder' | 'PictureInPicturePlaceholder'>;
72
73
 
73
- export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
74
- function ParticipantView(
74
+ export const ParticipantView = memo(
75
+ forwardRef<HTMLDivElement, ParticipantViewProps>(function ParticipantView(
75
76
  {
76
77
  participant,
77
78
  trackType = 'videoTrack',
@@ -195,7 +196,7 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
195
196
  </ParticipantViewContext.Provider>
196
197
  </div>
197
198
  );
198
- },
199
+ }),
199
200
  );
200
201
 
201
202
  ParticipantView.displayName = 'ParticipantView';
@@ -22,9 +22,10 @@ export const BaseVideo = forwardRef<HTMLVideoElement, BaseVideoProps>(
22
22
  if (stream === videoElement.srcObject) return;
23
23
 
24
24
  videoElement.srcObject = stream;
25
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
25
26
  if (Browsers.isSafari() || Browsers.isFirefox()) {
26
27
  // Firefox and Safari have some timing issue
27
- setTimeout(() => {
28
+ timeoutId = setTimeout(() => {
28
29
  videoElement.srcObject = stream;
29
30
  videoElement.play().catch((e) => {
30
31
  console.error(`Failed to play stream`, e);
@@ -33,6 +34,7 @@ export const BaseVideo = forwardRef<HTMLVideoElement, BaseVideoProps>(
33
34
  }
34
35
 
35
36
  return () => {
37
+ clearTimeout(timeoutId);
36
38
  videoElement.pause();
37
39
  videoElement.srcObject = null;
38
40
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ComponentPropsWithoutRef,
3
3
  ComponentType,
4
+ CSSProperties,
4
5
  useEffect,
5
6
  useLayoutEffect,
6
7
  useState,
@@ -25,6 +26,8 @@ import {
25
26
  PictureInPicturePlaceholderProps,
26
27
  } from './DefaultPictureInPicturePlaceholder';
27
28
 
29
+ const ABSOLUTE_POSITION_STYLE: CSSProperties = { position: 'absolute' };
30
+
28
31
  export type VideoProps = ComponentPropsWithoutRef<'video'> & {
29
32
  /**
30
33
  * Pass false to disable rendering video and render fallback
@@ -203,14 +206,14 @@ export const Video = ({
203
206
  )}
204
207
  {isPiP && PictureInPicturePlaceholder && (
205
208
  <PictureInPicturePlaceholder
206
- style={{ position: 'absolute' }}
209
+ style={ABSOLUTE_POSITION_STYLE}
207
210
  participant={participant}
208
211
  />
209
212
  )}
210
213
  {/* TODO: add condition to "hold" the placeholder until track unmutes as well */}
211
214
  {(hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (
212
215
  <VideoPlaceholder
213
- style={{ position: 'absolute' }}
216
+ style={ABSOLUTE_POSITION_STYLE}
214
217
  participant={participant}
215
218
  ref={refs?.setVideoPlaceholderElement}
216
219
  />
@@ -20,6 +20,7 @@ export const useWakeLock = () => {
20
20
  .then((wls: WakeLockSentinel) => {
21
21
  if (interrupted) return wls.release();
22
22
  wakeLockSentinel = wls;
23
+ return undefined;
23
24
  })
24
25
  .catch((error: unknown) =>
25
26
  console.debug(`Couldn't setup WakeLock due to: ${error}`),
@@ -55,13 +55,12 @@ export const ViewerLobby = ({ onJoin }: ViewerLobbyProps) => {
55
55
  }, [canJoin, autoJoin, onJoin]);
56
56
 
57
57
  useEffect(() => {
58
- if (!canJoinEarly) {
59
- const handle = setInterval(() => {
60
- setCanJoinEarly(checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
61
- }, 1000);
58
+ if (canJoinEarly) return;
59
+ const handle = setInterval(() => {
60
+ setCanJoinEarly(checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
61
+ }, 1000);
62
62
 
63
- return () => clearInterval(handle);
64
- }
63
+ return () => clearInterval(handle);
65
64
  }, [canJoinEarly, startsAt, joinAheadTimeSeconds]);
66
65
 
67
66
  useEffect(() => {
@@ -33,6 +33,13 @@ export function useDragToScroll(
33
33
  element: HTMLElement | null,
34
34
  options: DragToScrollOptions = {},
35
35
  ) {
36
+ const {
37
+ enabled,
38
+ decay = 0.95,
39
+ minVelocity = 0.5,
40
+ dragThreshold = 5,
41
+ } = options;
42
+
36
43
  const stateRef = useRef<DragScrollState>({
37
44
  isDragging: false,
38
45
  isPointerActive: false,
@@ -46,9 +53,7 @@ export function useDragToScroll(
46
53
  });
47
54
 
48
55
  useEffect(() => {
49
- if (!element || !options.enabled) return;
50
-
51
- const { decay = 0.95, minVelocity = 0.5, dragThreshold = 5 } = options;
56
+ if (!element || !enabled) return;
52
57
 
53
58
  const state = stateRef.current;
54
59
 
@@ -158,5 +163,5 @@ export function useDragToScroll(
158
163
 
159
164
  stopMomentum();
160
165
  };
161
- }, [element, options]);
166
+ }, [element, enabled, decay, minVelocity, dragThreshold]);
162
167
  }
@@ -40,7 +40,7 @@ export const useVerticalScrollPosition = (
40
40
  const resizeObserver = new ResizeObserver(scrollHandler);
41
41
  resizeObserver.observe(scrollElement);
42
42
 
43
- scrollElement.addEventListener('scroll', scrollHandler);
43
+ scrollElement.addEventListener('scroll', scrollHandler, { passive: true });
44
44
  return () => {
45
45
  scrollElement.removeEventListener('scroll', scrollHandler);
46
46
  resizeObserver.disconnect();
@@ -84,7 +84,7 @@ export const useHorizontalScrollPosition = (
84
84
  const resizeObserver = new ResizeObserver(scrollHandler);
85
85
  resizeObserver.observe(scrollElement);
86
86
 
87
- scrollElement.addEventListener('scroll', scrollHandler);
87
+ scrollElement.addEventListener('scroll', scrollHandler, { passive: true });
88
88
  return () => {
89
89
  scrollElement.removeEventListener('scroll', scrollHandler);
90
90
  resizeObserver.disconnect();
@@ -143,13 +143,12 @@ const useCanJoinEarly = () => {
143
143
  );
144
144
 
145
145
  useEffect(() => {
146
- if (!canJoinEarly) {
147
- const handle = setInterval(() => {
148
- setCanJoinEarly(checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
149
- }, 1000);
146
+ if (canJoinEarly) return;
147
+ const handle = setInterval(() => {
148
+ setCanJoinEarly(checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
149
+ }, 1000);
150
150
 
151
- return () => clearInterval(handle);
152
- }
151
+ return () => clearInterval(handle);
153
152
  }, [canJoinEarly, startsAt, joinAheadTimeSeconds]);
154
153
 
155
154
  return canJoinEarly;