@stream-io/video-react-sdk 1.6.6 → 1.7.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.
@@ -33,8 +33,8 @@ export type SpeakerLayoutProps = {
33
33
  * @default true
34
34
  */
35
35
  pageArrowsVisible?: boolean;
36
- } & Pick<ParticipantViewProps, 'VideoPlaceholder'>;
36
+ } & Pick<ParticipantViewProps, 'VideoPlaceholder' | 'PictureInPicturePlaceholder'>;
37
37
  export declare const SpeakerLayout: {
38
- ({ ParticipantViewUIBar, ParticipantViewUISpotlight, VideoPlaceholder, participantsBarPosition, participantsBarLimit, mirrorLocalParticipantVideo, excludeLocalParticipant, pageArrowsVisible, }: SpeakerLayoutProps): import("react/jsx-runtime").JSX.Element | null;
38
+ ({ ParticipantViewUIBar, ParticipantViewUISpotlight, VideoPlaceholder, PictureInPicturePlaceholder, participantsBarPosition, participantsBarLimit, mirrorLocalParticipantVideo, excludeLocalParticipant, pageArrowsVisible, }: SpeakerLayoutProps): import("react/jsx-runtime").JSX.Element | null;
39
39
  displayName: string;
40
40
  };
@@ -41,7 +41,7 @@ export type ParticipantViewProps = {
41
41
  * Custom class applied to the root DOM element.
42
42
  */
43
43
  className?: string;
44
- } & Pick<VideoProps, 'VideoPlaceholder'>;
44
+ } & Pick<VideoProps, 'VideoPlaceholder' | 'PictureInPicturePlaceholder'>;
45
45
  export declare const ParticipantView: import("react").ForwardRefExoticComponent<{
46
46
  /**
47
47
  * The participant whose video/audio stream we want to play.
@@ -82,4 +82,4 @@ export declare const ParticipantView: import("react").ForwardRefExoticComponent<
82
82
  * Custom class applied to the root DOM element.
83
83
  */
84
84
  className?: string;
85
- } & Pick<VideoProps, "VideoPlaceholder"> & import("react").RefAttributes<HTMLDivElement>>;
85
+ } & Pick<VideoProps, "VideoPlaceholder" | "PictureInPicturePlaceholder"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,6 @@
1
+ import { ComponentProps, RefAttributes } from 'react';
2
+ import type { StreamVideoParticipant } from '@stream-io/video-client';
3
+ export type BaseVideoPlaceholderProps = {
4
+ participant: StreamVideoParticipant;
5
+ } & RefAttributes<HTMLDivElement> & ComponentProps<'div'>;
6
+ export declare const BaseVideoPlaceholder: import("react").ForwardRefExoticComponent<Omit<BaseVideoPlaceholderProps, "ref"> & RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,3 @@
1
+ import { type BaseVideoPlaceholderProps } from './BaseVideoPlaceholder';
2
+ export type PictureInPicturePlaceholderProps = BaseVideoPlaceholderProps;
3
+ export declare const DefaultPictureInPicturePlaceholder: import("react").ForwardRefExoticComponent<Omit<BaseVideoPlaceholderProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,6 +1,3 @@
1
- import { ComponentProps, RefAttributes } from 'react';
2
- import type { StreamVideoParticipant } from '@stream-io/video-client';
3
- export type VideoPlaceholderProps = {
4
- participant: StreamVideoParticipant;
5
- } & RefAttributes<HTMLDivElement> & ComponentProps<'div'>;
6
- export declare const DefaultVideoPlaceholder: import("react").ForwardRefExoticComponent<Omit<VideoPlaceholderProps, "ref"> & RefAttributes<HTMLDivElement>>;
1
+ import { type BaseVideoPlaceholderProps } from './BaseVideoPlaceholder';
2
+ export type VideoPlaceholderProps = BaseVideoPlaceholderProps;
3
+ export declare const DefaultVideoPlaceholder: import("react").ForwardRefExoticComponent<Omit<BaseVideoPlaceholderProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,6 +1,7 @@
1
1
  import { ComponentPropsWithoutRef, ComponentType } from 'react';
2
2
  import { StreamVideoParticipant, VideoTrackType } from '@stream-io/video-client';
3
3
  import { VideoPlaceholderProps } from './DefaultVideoPlaceholder';
4
+ import { PictureInPicturePlaceholderProps } from './DefaultPictureInPicturePlaceholder';
4
5
  export type VideoProps = ComponentPropsWithoutRef<'video'> & {
5
6
  /**
6
7
  * Pass false to disable rendering video and render fallback
@@ -28,6 +29,15 @@ export type VideoProps = ComponentPropsWithoutRef<'video'> & {
28
29
  * @default DefaultVideoPlaceholder
29
30
  */
30
31
  VideoPlaceholder?: ComponentType<VideoPlaceholderProps> | null;
32
+ /**
33
+ * Override the default UI that's dispayed in place of the video when it's playing
34
+ * in picture-in-picture. Set it to `null` if you wish to display the browser's default
35
+ * placeholder.
36
+ *
37
+ * @default DefaultPictureInPicturePlaceholder
38
+ */
39
+ PictureInPicturePlaceholder?: ComponentType<PictureInPicturePlaceholderProps> | null;
40
+ /**
31
41
  /**
32
42
  * An object with setRef functions
33
43
  * meant for exposing some of the internal elements of this component.
@@ -43,9 +53,10 @@ export type VideoProps = ComponentPropsWithoutRef<'video'> & {
43
53
  * @param element the video placeholder element.
44
54
  */
45
55
  setVideoPlaceholderElement?: (element: HTMLDivElement | null) => void;
56
+ setPictureInPicturePlaceholderElement?: (element: HTMLDivElement | null) => void;
46
57
  };
47
58
  };
48
59
  export declare const Video: {
49
- ({ enabled, mirror, trackType, participant, className, VideoPlaceholder, refs, ...rest }: VideoProps): import("react/jsx-runtime").JSX.Element | null;
60
+ ({ enabled, mirror, trackType, participant, className, VideoPlaceholder, PictureInPicturePlaceholder, refs, ...rest }: VideoProps): import("react/jsx-runtime").JSX.Element | null;
50
61
  displayName: string;
51
62
  };
@@ -0,0 +1 @@
1
+ export declare function usePictureInPictureState(videoElement?: HTMLVideoElement): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.6.6",
3
+ "version": "1.7.0",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "module": "./dist/index.es.js",
@@ -32,9 +32,9 @@
32
32
  ],
33
33
  "dependencies": {
34
34
  "@floating-ui/react": "^0.26.24",
35
- "@stream-io/video-client": "1.8.3",
35
+ "@stream-io/video-client": "1.9.0",
36
36
  "@stream-io/video-filters-web": "0.1.4",
37
- "@stream-io/video-react-bindings": "1.1.3",
37
+ "@stream-io/video-react-bindings": "1.1.5",
38
38
  "chart.js": "^4.4.4",
39
39
  "clsx": "^2.0.0",
40
40
  "react-chartjs-2": "^5.2.0"
@@ -48,7 +48,7 @@
48
48
  "@rollup/plugin-replace": "^5.0.7",
49
49
  "@rollup/plugin-typescript": "^11.1.6",
50
50
  "@stream-io/audio-filters-web": "^0.2.2",
51
- "@stream-io/video-styling": "^1.1.0",
51
+ "@stream-io/video-styling": "^1.1.1",
52
52
  "@types/react": "^18.3.2",
53
53
  "@types/react-dom": "^18.3.0",
54
54
  "react": "^18.3.1",
@@ -20,13 +20,17 @@ type PaginatedGridLayoutGroupProps = {
20
20
  * The group of participants to render.
21
21
  */
22
22
  group: Array<StreamVideoParticipant>;
23
- } & Pick<ParticipantViewProps, 'VideoPlaceholder' | 'mirror'> &
23
+ } & Pick<
24
+ ParticipantViewProps,
25
+ 'VideoPlaceholder' | 'PictureInPicturePlaceholder' | 'mirror'
26
+ > &
24
27
  Required<Pick<ParticipantViewProps, 'ParticipantViewUI'>>;
25
28
 
26
29
  const PaginatedGridLayoutGroup = ({
27
30
  group,
28
31
  mirror,
29
32
  VideoPlaceholder,
33
+ PictureInPicturePlaceholder,
30
34
  ParticipantViewUI,
31
35
  }: PaginatedGridLayoutGroupProps) => {
32
36
  return (
@@ -46,6 +50,7 @@ const PaginatedGridLayoutGroup = ({
46
50
  muteAudio
47
51
  mirror={mirror}
48
52
  VideoPlaceholder={VideoPlaceholder}
53
+ PictureInPicturePlaceholder={PictureInPicturePlaceholder}
49
54
  ParticipantViewUI={ParticipantViewUI}
50
55
  />
51
56
  ))}
@@ -51,7 +51,10 @@ export type SpeakerLayoutProps = {
51
51
  * @default true
52
52
  */
53
53
  pageArrowsVisible?: boolean;
54
- } & Pick<ParticipantViewProps, 'VideoPlaceholder'>;
54
+ } & Pick<
55
+ ParticipantViewProps,
56
+ 'VideoPlaceholder' | 'PictureInPicturePlaceholder'
57
+ >;
55
58
 
56
59
  const DefaultParticipantViewUIBar = () => (
57
60
  <DefaultParticipantViewUI menuPlacement="top-end" />
@@ -61,6 +64,7 @@ export const SpeakerLayout = ({
61
64
  ParticipantViewUIBar = DefaultParticipantViewUIBar,
62
65
  ParticipantViewUISpotlight = DefaultParticipantViewUI,
63
66
  VideoPlaceholder,
67
+ PictureInPicturePlaceholder,
64
68
  participantsBarPosition = 'bottom',
65
69
  participantsBarLimit,
66
70
  mirrorLocalParticipantVideo = true,
@@ -151,6 +155,7 @@ export const SpeakerLayout = ({
151
155
  }
152
156
  ParticipantViewUI={ParticipantViewUISpotlight}
153
157
  VideoPlaceholder={VideoPlaceholder}
158
+ PictureInPicturePlaceholder={PictureInPicturePlaceholder}
154
159
  />
155
160
  )}
156
161
  </div>
@@ -176,6 +181,7 @@ export const SpeakerLayout = ({
176
181
  participant={participantInSpotlight}
177
182
  ParticipantViewUI={ParticipantViewUIBar}
178
183
  VideoPlaceholder={VideoPlaceholder}
184
+ PictureInPicturePlaceholder={PictureInPicturePlaceholder}
179
185
  mirror={mirror}
180
186
  muteAudio={true}
181
187
  />
@@ -190,6 +196,7 @@ export const SpeakerLayout = ({
190
196
  participant={participant}
191
197
  ParticipantViewUI={ParticipantViewUIBar}
192
198
  VideoPlaceholder={VideoPlaceholder}
199
+ PictureInPicturePlaceholder={PictureInPicturePlaceholder}
193
200
  mirror={mirror}
194
201
  muteAudio={true}
195
202
  />
@@ -14,6 +14,7 @@ import {
14
14
  useMenuContext,
15
15
  } from '../../../components/Menu';
16
16
  import { Icon } from '../../../components/Icon';
17
+ import { usePictureInPictureState } from '../../hooks/usePictureInPictureState';
17
18
 
18
19
  export const ParticipantActionsContextMenu = () => {
19
20
  const { participant, participantViewElement, videoElement } =
@@ -21,10 +22,8 @@ export const ParticipantActionsContextMenu = () => {
21
22
  const [fullscreenModeOn, setFullscreenModeOn] = useState(
22
23
  !!document.fullscreenElement,
23
24
  );
24
- const [pictureInPictureElement, setPictureInPictureElement] = useState(
25
- document.pictureInPictureElement,
26
- );
27
25
  const call = useCall();
26
+ const isPiP = usePictureInPictureState(videoElement ?? undefined);
28
27
  const { t } = useI18n();
29
28
 
30
29
  const { pin, sessionId, userId } = participant;
@@ -104,24 +103,8 @@ export const ParticipantActionsContextMenu = () => {
104
103
  };
105
104
  }, []);
106
105
 
107
- useEffect(() => {
108
- if (!videoElement) return;
109
-
110
- const handlePiP = () => {
111
- setPictureInPictureElement(document.pictureInPictureElement);
112
- };
113
-
114
- videoElement.addEventListener('enterpictureinpicture', handlePiP);
115
- videoElement.addEventListener('leavepictureinpicture', handlePiP);
116
-
117
- return () => {
118
- videoElement.removeEventListener('enterpictureinpicture', handlePiP);
119
- videoElement.removeEventListener('leavepictureinpicture', handlePiP);
120
- };
121
- }, [videoElement]);
122
-
123
106
  const togglePictureInPicture = () => {
124
- if (videoElement && pictureInPictureElement !== videoElement) {
107
+ if (videoElement && !isPiP) {
125
108
  return videoElement
126
109
  .requestPictureInPicture()
127
110
  .catch(console.error) as Promise<void>;
@@ -198,10 +181,7 @@ export const ParticipantActionsContextMenu = () => {
198
181
  {videoElement && document.pictureInPictureEnabled && (
199
182
  <GenericMenuButtonItem onClick={togglePictureInPicture}>
200
183
  {t('{{ direction }} picture-in-picture', {
201
- direction:
202
- pictureInPictureElement === videoElement
203
- ? t('Leave')
204
- : t('Enter'),
184
+ direction: isPiP ? t('Leave') : t('Enter'),
205
185
  })}
206
186
  </GenericMenuButtonItem>
207
187
  )}
@@ -68,7 +68,7 @@ export type ParticipantViewProps = {
68
68
  * Custom class applied to the root DOM element.
69
69
  */
70
70
  className?: string;
71
- } & Pick<VideoProps, 'VideoPlaceholder'>;
71
+ } & Pick<VideoProps, 'VideoPlaceholder' | 'PictureInPicturePlaceholder'>;
72
72
 
73
73
  export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
74
74
  function ParticipantView(
@@ -80,6 +80,7 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
80
80
  refs: { setVideoElement, setVideoPlaceholderElement } = {},
81
81
  className,
82
82
  VideoPlaceholder,
83
+ PictureInPicturePlaceholder,
83
84
  ParticipantViewUI = DefaultParticipantViewUI as ComponentType,
84
85
  },
85
86
  ref,
@@ -175,6 +176,7 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
175
176
  )}
176
177
  <Video
177
178
  VideoPlaceholder={VideoPlaceholder}
179
+ PictureInPicturePlaceholder={PictureInPicturePlaceholder}
178
180
  participant={participant}
179
181
  trackType={trackType}
180
182
  refs={videoRefs}
@@ -0,0 +1,49 @@
1
+ import { ComponentProps, RefAttributes, forwardRef, useState } from 'react';
2
+ import type { StreamVideoParticipant } from '@stream-io/video-client';
3
+
4
+ export type BaseVideoPlaceholderProps = {
5
+ participant: StreamVideoParticipant;
6
+ } & RefAttributes<HTMLDivElement> &
7
+ ComponentProps<'div'>;
8
+
9
+ export const BaseVideoPlaceholder = forwardRef<
10
+ HTMLDivElement,
11
+ BaseVideoPlaceholderProps
12
+ >(function DefaultVideoPlaceholder({ participant, style, children }, ref) {
13
+ const [error, setError] = useState(false);
14
+ const name = participant.name || participant.userId;
15
+ return (
16
+ <div className="str-video__video-placeholder" style={style} ref={ref}>
17
+ {(!participant.image || error) &&
18
+ (name ? (
19
+ <InitialsFallback name={name} />
20
+ ) : (
21
+ <div className="str-video__video-placeholder__no-video-label">
22
+ {children}
23
+ </div>
24
+ ))}
25
+ {participant.image && !error && (
26
+ <img
27
+ onError={() => setError(true)}
28
+ alt={name}
29
+ className="str-video__video-placeholder__avatar"
30
+ src={participant.image}
31
+ />
32
+ )}
33
+ </div>
34
+ );
35
+ });
36
+
37
+ const InitialsFallback = (props: { name: string }) => {
38
+ const { name } = props;
39
+ const initials = name
40
+ .split(' ')
41
+ .slice(0, 2)
42
+ .map((n) => n[0])
43
+ .join('');
44
+ return (
45
+ <div className="str-video__video-placeholder__initials-fallback">
46
+ {initials}
47
+ </div>
48
+ );
49
+ };
@@ -0,0 +1,20 @@
1
+ import { forwardRef } from 'react';
2
+ import { useI18n } from '@stream-io/video-react-bindings';
3
+ import {
4
+ BaseVideoPlaceholder,
5
+ type BaseVideoPlaceholderProps,
6
+ } from './BaseVideoPlaceholder';
7
+
8
+ export type PictureInPicturePlaceholderProps = BaseVideoPlaceholderProps;
9
+
10
+ export const DefaultPictureInPicturePlaceholder = forwardRef<
11
+ HTMLDivElement,
12
+ PictureInPicturePlaceholderProps
13
+ >(function DefaultPictureInPicturePlaceholder(props, ref) {
14
+ const { t } = useI18n();
15
+ return (
16
+ <BaseVideoPlaceholder ref={ref} {...props}>
17
+ {t('Video is playing in a popup')}
18
+ </BaseVideoPlaceholder>
19
+ );
20
+ });
@@ -1,51 +1,20 @@
1
- import { ComponentProps, RefAttributes, forwardRef, useState } from 'react';
1
+ import { forwardRef } from 'react';
2
2
  import { useI18n } from '@stream-io/video-react-bindings';
3
- import type { StreamVideoParticipant } from '@stream-io/video-client';
3
+ import {
4
+ BaseVideoPlaceholder,
5
+ type BaseVideoPlaceholderProps,
6
+ } from './BaseVideoPlaceholder';
4
7
 
5
- export type VideoPlaceholderProps = {
6
- participant: StreamVideoParticipant;
7
- } & RefAttributes<HTMLDivElement> &
8
- ComponentProps<'div'>;
8
+ export type VideoPlaceholderProps = BaseVideoPlaceholderProps;
9
9
 
10
10
  export const DefaultVideoPlaceholder = forwardRef<
11
11
  HTMLDivElement,
12
12
  VideoPlaceholderProps
13
- >(function DefaultVideoPlaceholder({ participant, style }, ref) {
13
+ >(function DefaultVideoPlaceholder(props, ref) {
14
14
  const { t } = useI18n();
15
- const [error, setError] = useState(false);
16
- const name = participant.name || participant.userId;
17
15
  return (
18
- <div className="str-video__video-placeholder" style={style} ref={ref}>
19
- {(!participant.image || error) &&
20
- (name ? (
21
- <InitialsFallback name={name} />
22
- ) : (
23
- <div className="str-video__video-placeholder__no-video-label">
24
- {t('Video is disabled')}
25
- </div>
26
- ))}
27
- {participant.image && !error && (
28
- <img
29
- onError={() => setError(true)}
30
- alt="video-placeholder"
31
- className="str-video__video-placeholder__avatar"
32
- src={participant.image}
33
- />
34
- )}
35
- </div>
16
+ <BaseVideoPlaceholder ref={ref} {...props}>
17
+ {t('Video is disabled')}
18
+ </BaseVideoPlaceholder>
36
19
  );
37
20
  });
38
-
39
- const InitialsFallback = (props: { name: string }) => {
40
- const { name } = props;
41
- const initials = name
42
- .split(' ')
43
- .slice(0, 2)
44
- .map((n) => n[0])
45
- .join('');
46
- return (
47
- <div className="str-video__video-placeholder__initials-fallback">
48
- {initials}
49
- </div>
50
- );
51
- };
@@ -18,6 +18,11 @@ import {
18
18
  VideoPlaceholderProps,
19
19
  } from './DefaultVideoPlaceholder';
20
20
  import { useCall } from '@stream-io/video-react-bindings';
21
+ import { usePictureInPictureState } from '../../hooks/usePictureInPictureState';
22
+ import {
23
+ DefaultPictureInPicturePlaceholder,
24
+ PictureInPicturePlaceholderProps,
25
+ } from './DefaultPictureInPicturePlaceholder';
21
26
 
22
27
  export type VideoProps = ComponentPropsWithoutRef<'video'> & {
23
28
  /**
@@ -46,6 +51,15 @@ export type VideoProps = ComponentPropsWithoutRef<'video'> & {
46
51
  * @default DefaultVideoPlaceholder
47
52
  */
48
53
  VideoPlaceholder?: ComponentType<VideoPlaceholderProps> | null;
54
+ /**
55
+ * Override the default UI that's dispayed in place of the video when it's playing
56
+ * in picture-in-picture. Set it to `null` if you wish to display the browser's default
57
+ * placeholder.
58
+ *
59
+ * @default DefaultPictureInPicturePlaceholder
60
+ */
61
+ PictureInPicturePlaceholder?: ComponentType<PictureInPicturePlaceholderProps> | null;
62
+ /**
49
63
  /**
50
64
  * An object with setRef functions
51
65
  * meant for exposing some of the internal elements of this component.
@@ -61,6 +75,9 @@ export type VideoProps = ComponentPropsWithoutRef<'video'> & {
61
75
  * @param element the video placeholder element.
62
76
  */
63
77
  setVideoPlaceholderElement?: (element: HTMLDivElement | null) => void;
78
+ setPictureInPicturePlaceholderElement?: (
79
+ element: HTMLDivElement | null,
80
+ ) => void;
64
81
  };
65
82
  };
66
83
 
@@ -71,6 +88,7 @@ export const Video = ({
71
88
  participant,
72
89
  className,
73
90
  VideoPlaceholder = DefaultVideoPlaceholder,
91
+ PictureInPicturePlaceholder = DefaultPictureInPicturePlaceholder,
74
92
  refs,
75
93
  ...rest
76
94
  }: VideoProps) => {
@@ -90,6 +108,7 @@ export const Video = ({
90
108
  // start with true, will flip once the video starts playing
91
109
  const [isVideoPaused, setIsVideoPaused] = useState(true);
92
110
  const [isWideMode, setIsWideMode] = useState(true);
111
+ const isPiP = usePictureInPictureState(videoElement ?? undefined);
93
112
 
94
113
  const stream =
95
114
  trackType === 'videoTrack'
@@ -177,6 +196,12 @@ export const Video = ({
177
196
  }}
178
197
  />
179
198
  )}
199
+ {isPiP && (
200
+ <DefaultPictureInPicturePlaceholder
201
+ style={{ position: 'absolute' }}
202
+ participant={participant}
203
+ />
204
+ )}
180
205
  {/* TODO: add condition to "hold" the placeholder until track unmutes as well */}
181
206
  {(hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (
182
207
  <VideoPlaceholder
@@ -0,0 +1,27 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export function usePictureInPictureState(videoElement?: HTMLVideoElement) {
4
+ const [isPiP, setIsPiP] = useState(
5
+ document.pictureInPictureElement === videoElement,
6
+ );
7
+
8
+ if (!videoElement && isPiP) setIsPiP(false);
9
+
10
+ useEffect(() => {
11
+ if (!videoElement) return;
12
+
13
+ const handlePiP = () => {
14
+ setIsPiP(document.pictureInPictureElement === videoElement);
15
+ };
16
+
17
+ videoElement.addEventListener('enterpictureinpicture', handlePiP);
18
+ videoElement.addEventListener('leavepictureinpicture', handlePiP);
19
+
20
+ return () => {
21
+ videoElement.removeEventListener('enterpictureinpicture', handlePiP);
22
+ videoElement.removeEventListener('leavepictureinpicture', handlePiP);
23
+ };
24
+ }, [videoElement]);
25
+
26
+ return isPiP;
27
+ }