@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.
- package/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/dist/css/styles.css +1 -1
- package/dist/css/styles.css.map +1 -1
- package/dist/index.cjs.js +45 -30
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +45 -30
- package/dist/index.es.js.map +1 -1
- package/dist/src/core/components/CallLayout/SpeakerLayout.d.ts +2 -2
- package/dist/src/core/components/ParticipantView/ParticipantView.d.ts +2 -2
- package/dist/src/core/components/Video/BaseVideoPlaceholder.d.ts +6 -0
- package/dist/src/core/components/Video/DefaultPictureInPicturePlaceholder.d.ts +3 -0
- package/dist/src/core/components/Video/DefaultVideoPlaceholder.d.ts +3 -6
- package/dist/src/core/components/Video/Video.d.ts +12 -1
- package/dist/src/core/hooks/usePictureInPictureState.d.ts +1 -0
- package/package.json +4 -4
- package/src/core/components/CallLayout/PaginatedGridLayout.tsx +6 -1
- package/src/core/components/CallLayout/SpeakerLayout.tsx +8 -1
- package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +4 -24
- package/src/core/components/ParticipantView/ParticipantView.tsx +3 -1
- package/src/core/components/Video/BaseVideoPlaceholder.tsx +49 -0
- package/src/core/components/Video/DefaultPictureInPicturePlaceholder.tsx +20 -0
- package/src/core/components/Video/DefaultVideoPlaceholder.tsx +10 -41
- package/src/core/components/Video/Video.tsx +25 -0
- package/src/core/hooks/usePictureInPictureState.ts +27 -0
|
@@ -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 {
|
|
2
|
-
|
|
3
|
-
export
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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<
|
|
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<
|
|
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 &&
|
|
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 {
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
2
|
import { useI18n } from '@stream-io/video-react-bindings';
|
|
3
|
-
import
|
|
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(
|
|
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
|
-
<
|
|
19
|
-
{(
|
|
20
|
-
|
|
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
|
+
}
|