@stream-io/video-react-sdk 0.0.1-alpha.33 → 0.0.1-alpha.35
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 +8 -0
- package/dist/css/styles.css +134 -145
- package/dist/css/styles.css.map +1 -1
- package/dist/src/components/Button/CompositeButton.js +2 -4
- package/dist/src/components/Button/CompositeButton.js.map +1 -1
- package/dist/src/components/StreamCall/CallParticipantsScreenView.js +3 -3
- package/dist/src/components/StreamCall/CallParticipantsScreenView.js.map +1 -1
- package/dist/src/components/StreamCall/CallParticipantsView.js +2 -3
- package/dist/src/components/StreamCall/CallParticipantsView.js.map +1 -1
- package/dist/src/core/components/CallLayout/PaginatedGridLayout.d.ts +3 -7
- package/dist/src/core/components/CallLayout/PaginatedGridLayout.js +7 -11
- package/dist/src/core/components/CallLayout/PaginatedGridLayout.js.map +1 -1
- package/dist/src/core/components/CallLayout/SpeakerLayout.d.ts +6 -1
- package/dist/src/core/components/CallLayout/SpeakerLayout.js +5 -3
- package/dist/src/core/components/CallLayout/SpeakerLayout.js.map +1 -1
- package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.d.ts +20 -0
- package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.js +33 -0
- package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.js.map +1 -0
- package/dist/src/core/components/ParticipantView/ParticipantView.d.ts +82 -0
- package/dist/src/core/components/ParticipantView/ParticipantView.js +28 -0
- package/dist/src/core/components/ParticipantView/ParticipantView.js.map +1 -0
- package/dist/src/core/components/ParticipantView/index.d.ts +2 -0
- package/dist/src/core/components/ParticipantView/index.js +3 -0
- package/dist/src/core/components/ParticipantView/index.js.map +1 -0
- package/dist/src/core/components/Video/BaseVideo.d.ts +3 -3
- package/dist/src/core/components/Video/BaseVideo.js +6 -12
- package/dist/src/core/components/Video/BaseVideo.js.map +1 -1
- package/dist/src/core/components/Video/Video.d.ts +8 -6
- package/dist/src/core/components/Video/Video.js +27 -25
- package/dist/src/core/components/Video/Video.js.map +1 -1
- package/dist/src/core/components/Video/VideoPlaceholder.d.ts +3 -3
- package/dist/src/core/components/Video/VideoPlaceholder.js +2 -5
- package/dist/src/core/components/Video/VideoPlaceholder.js.map +1 -1
- package/dist/src/core/components/index.d.ts +2 -2
- package/dist/src/core/components/index.js +1 -1
- package/dist/src/core/components/index.js.map +1 -1
- package/dist/src/core/hooks/index.d.ts +1 -0
- package/dist/src/core/hooks/index.js +1 -0
- package/dist/src/core/hooks/index.js.map +1 -1
- package/dist/src/core/hooks/useTrackElementVisibility.d.ts +6 -0
- package/dist/src/core/hooks/useTrackElementVisibility.js +24 -0
- package/dist/src/core/hooks/useTrackElementVisibility.js.map +1 -0
- package/dist/src/utilities/applyElementRef.d.ts +2 -0
- package/dist/src/utilities/applyElementRef.js +8 -0
- package/dist/src/utilities/applyElementRef.js.map +1 -0
- package/dist/src/utilities/chunk.d.ts +1 -0
- package/dist/src/utilities/chunk.js +5 -0
- package/dist/src/utilities/chunk.js.map +1 -0
- package/dist/src/utilities/index.d.ts +3 -0
- package/dist/src/utilities/index.js +4 -0
- package/dist/src/utilities/index.js.map +1 -0
- package/dist/src/utilities/isComponentType.d.ts +2 -0
- package/dist/src/utilities/isComponentType.js +7 -0
- package/dist/src/utilities/isComponentType.js.map +1 -0
- package/package.json +5 -5
- package/src/components/Button/CompositeButton.tsx +4 -13
- package/src/components/StreamCall/CallParticipantsScreenView.tsx +3 -4
- package/src/components/StreamCall/CallParticipantsView.tsx +3 -4
- package/src/core/components/CallLayout/PaginatedGridLayout.tsx +21 -40
- package/src/core/components/CallLayout/SpeakerLayout.tsx +46 -19
- package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +165 -0
- package/src/core/components/ParticipantView/ParticipantView.tsx +136 -0
- package/src/core/components/ParticipantView/index.ts +2 -0
- package/src/core/components/Video/BaseVideo.tsx +9 -24
- package/src/core/components/Video/Video.tsx +55 -44
- package/src/core/components/Video/VideoPlaceholder.tsx +7 -11
- package/src/core/components/index.ts +2 -2
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/useTrackElementVisibility.ts +41 -0
- package/src/utilities/applyElementRef.ts +12 -0
- package/src/utilities/chunk.ts +8 -0
- package/src/utilities/index.ts +3 -0
- package/src/utilities/isComponentType.ts +9 -0
- package/dist/src/core/components/ParticipantBox/ParticipantBox.d.ts +0 -48
- package/dist/src/core/components/ParticipantBox/ParticipantBox.js +0 -58
- package/dist/src/core/components/ParticipantBox/ParticipantBox.js.map +0 -1
- package/dist/src/core/components/ParticipantBox/index.d.ts +0 -1
- package/dist/src/core/components/ParticipantBox/index.js +0 -2
- package/dist/src/core/components/ParticipantBox/index.js.map +0 -1
- package/src/core/components/ParticipantBox/ParticipantBox.tsx +0 -248
- package/src/core/components/ParticipantBox/index.ts +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { Placement } from '@floating-ui/react';
|
|
3
|
+
import { SfuModels } from '@stream-io/video-client';
|
|
4
|
+
import { useCall } from '@stream-io/video-react-bindings';
|
|
5
|
+
import { clsx } from 'clsx';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
IconButton,
|
|
9
|
+
MenuToggle,
|
|
10
|
+
Notification,
|
|
11
|
+
ParticipantActionsContextMenu,
|
|
12
|
+
ToggleMenuButtonProps,
|
|
13
|
+
} from '../../../components';
|
|
14
|
+
import { Reaction } from '../../../components/Reaction';
|
|
15
|
+
import { ParticipantViewProps } from './ParticipantView';
|
|
16
|
+
|
|
17
|
+
import { DebugParticipantPublishQuality } from '../../../components/Debug/DebugParticipantPublishQuality';
|
|
18
|
+
import { DebugStatsView } from '../../../components/Debug/DebugStatsView';
|
|
19
|
+
import { useIsDebugMode } from '../../../components/Debug/useIsDebugMode';
|
|
20
|
+
|
|
21
|
+
export type ParticipantViewUIProps = Pick<ParticipantViewProps, 'participant'>;
|
|
22
|
+
|
|
23
|
+
export type DefaultParticipantViewUIProps = {
|
|
24
|
+
/**
|
|
25
|
+
* Turns on/off the status indicator icons (mute, connection quality, etc...).
|
|
26
|
+
*/
|
|
27
|
+
indicatorsVisible?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Placement of the context menu component when opened
|
|
30
|
+
*/
|
|
31
|
+
menuPlacement?: Placement;
|
|
32
|
+
/**
|
|
33
|
+
* Option to show/hide menu button component
|
|
34
|
+
*/
|
|
35
|
+
showMenuButton?: boolean;
|
|
36
|
+
} & ParticipantViewUIProps;
|
|
37
|
+
|
|
38
|
+
const ToggleButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
|
|
39
|
+
(props, ref) => {
|
|
40
|
+
return <IconButton enabled={props.menuShown} icon="ellipsis" ref={ref} />;
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export const DefaultParticipantViewUI = ({
|
|
45
|
+
participant,
|
|
46
|
+
indicatorsVisible = true,
|
|
47
|
+
menuPlacement = 'bottom-end',
|
|
48
|
+
showMenuButton = true,
|
|
49
|
+
}: DefaultParticipantViewUIProps) => {
|
|
50
|
+
const call = useCall()!;
|
|
51
|
+
const { reaction, sessionId } = participant;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
{showMenuButton && (
|
|
56
|
+
<MenuToggle
|
|
57
|
+
strategy="fixed"
|
|
58
|
+
placement={menuPlacement}
|
|
59
|
+
ToggleButton={ToggleButton}
|
|
60
|
+
>
|
|
61
|
+
<ParticipantActionsContextMenu participant={participant} />
|
|
62
|
+
</MenuToggle>
|
|
63
|
+
)}
|
|
64
|
+
{reaction && (
|
|
65
|
+
<Reaction reaction={reaction} sessionId={sessionId} call={call} />
|
|
66
|
+
)}
|
|
67
|
+
<ParticipantDetails
|
|
68
|
+
participant={participant}
|
|
69
|
+
indicatorsVisible={indicatorsVisible}
|
|
70
|
+
/>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const ParticipantDetails = ({
|
|
76
|
+
participant,
|
|
77
|
+
indicatorsVisible = true,
|
|
78
|
+
}: Pick<
|
|
79
|
+
DefaultParticipantViewUIProps,
|
|
80
|
+
'participant' | 'indicatorsVisible'
|
|
81
|
+
>) => {
|
|
82
|
+
const {
|
|
83
|
+
isDominantSpeaker,
|
|
84
|
+
isLoggedInUser,
|
|
85
|
+
connectionQuality,
|
|
86
|
+
publishedTracks,
|
|
87
|
+
pinnedAt,
|
|
88
|
+
sessionId,
|
|
89
|
+
name,
|
|
90
|
+
userId,
|
|
91
|
+
videoStream,
|
|
92
|
+
} = participant;
|
|
93
|
+
const call = useCall()!;
|
|
94
|
+
|
|
95
|
+
const connectionQualityAsString =
|
|
96
|
+
!!connectionQuality &&
|
|
97
|
+
String(SfuModels.ConnectionQuality[connectionQuality]).toLowerCase();
|
|
98
|
+
|
|
99
|
+
const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
|
|
100
|
+
const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
|
|
101
|
+
const isPinned = !!pinnedAt;
|
|
102
|
+
|
|
103
|
+
const isDebugMode = useIsDebugMode();
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="str-video__participant-details">
|
|
107
|
+
<span className="str-video__participant-details__name">
|
|
108
|
+
{name || userId}
|
|
109
|
+
{indicatorsVisible && isDominantSpeaker && (
|
|
110
|
+
<span
|
|
111
|
+
className="str-video__participant-details__name--dominant_speaker"
|
|
112
|
+
title="Dominant speaker"
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
{indicatorsVisible && (
|
|
116
|
+
<Notification
|
|
117
|
+
isVisible={
|
|
118
|
+
isLoggedInUser &&
|
|
119
|
+
connectionQuality === SfuModels.ConnectionQuality.POOR
|
|
120
|
+
}
|
|
121
|
+
message="Poor connection quality. Please check your internet connection."
|
|
122
|
+
>
|
|
123
|
+
{connectionQualityAsString && (
|
|
124
|
+
<span
|
|
125
|
+
className={clsx(
|
|
126
|
+
'str-video__participant-details__connection-quality',
|
|
127
|
+
`str-video__participant-details__connection-quality--${connectionQualityAsString}`,
|
|
128
|
+
)}
|
|
129
|
+
title={connectionQualityAsString}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</Notification>
|
|
133
|
+
)}
|
|
134
|
+
{indicatorsVisible && !hasAudio && (
|
|
135
|
+
<span className="str-video__participant-details__name--audio-muted" />
|
|
136
|
+
)}
|
|
137
|
+
{indicatorsVisible && !hasVideo && (
|
|
138
|
+
<span className="str-video__participant-details__name--video-muted" />
|
|
139
|
+
)}
|
|
140
|
+
{indicatorsVisible && isPinned && (
|
|
141
|
+
// TODO: remove this monstrosity once we have a proper design
|
|
142
|
+
<span
|
|
143
|
+
title="Unpin"
|
|
144
|
+
onClick={() => call?.setParticipantPinnedAt(sessionId)}
|
|
145
|
+
style={{ cursor: 'pointer' }}
|
|
146
|
+
className="str-video__participant-details__name--pinned"
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</span>
|
|
150
|
+
{isDebugMode && (
|
|
151
|
+
<>
|
|
152
|
+
<DebugParticipantPublishQuality
|
|
153
|
+
participant={participant}
|
|
154
|
+
call={call}
|
|
155
|
+
/>
|
|
156
|
+
<DebugStatsView
|
|
157
|
+
call={call}
|
|
158
|
+
kind={isLoggedInUser ? 'publisher' : 'subscriber'}
|
|
159
|
+
mediaStream={videoStream}
|
|
160
|
+
/>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { forwardRef, ComponentType, useState, ReactElement } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import {
|
|
4
|
+
SfuModels,
|
|
5
|
+
StreamVideoLocalParticipant,
|
|
6
|
+
StreamVideoParticipant,
|
|
7
|
+
} from '@stream-io/video-client';
|
|
8
|
+
|
|
9
|
+
import { Audio } from '../Audio';
|
|
10
|
+
import { Video, VideoProps } from '../Video';
|
|
11
|
+
import { useTrackElementVisibility } from '../../hooks';
|
|
12
|
+
import {
|
|
13
|
+
DefaultParticipantViewUI,
|
|
14
|
+
ParticipantViewUIProps,
|
|
15
|
+
} from './DefaultParticipantViewUI';
|
|
16
|
+
import { isComponentType, applyElementRef } from '../../../utilities';
|
|
17
|
+
|
|
18
|
+
export type ParticipantViewProps = {
|
|
19
|
+
/**
|
|
20
|
+
* The participant bound to this component.
|
|
21
|
+
*/
|
|
22
|
+
participant: StreamVideoParticipant | StreamVideoLocalParticipant;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Component used to render user interface elements (details, network status...),
|
|
26
|
+
* pass `null` if you wish to not render anything
|
|
27
|
+
* @default DefaultParticipantViewUI
|
|
28
|
+
*/
|
|
29
|
+
ParticipantViewUI?:
|
|
30
|
+
| ComponentType<ParticipantViewUIProps>
|
|
31
|
+
| ReactElement
|
|
32
|
+
| null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* In supported browsers, this sets the default audio output.
|
|
36
|
+
* The value of this prop should be a valid Audio Output `deviceId`.
|
|
37
|
+
*/
|
|
38
|
+
sinkId?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The kind of video stream to play for the given participant.
|
|
42
|
+
*/
|
|
43
|
+
videoKind?: 'video' | 'screen';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Turns on/off the audio for the participant.
|
|
47
|
+
*/
|
|
48
|
+
muteAudio?: boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A function meant for exposing the "native" element ref to the integrators.
|
|
52
|
+
* The element can either be:
|
|
53
|
+
* - `<video />` for participants with enabled video.
|
|
54
|
+
* - `<div />` for participants with disabled video. This ref would point to
|
|
55
|
+
* the VideoPlaceholder component.
|
|
56
|
+
*
|
|
57
|
+
* @param element the element ref.
|
|
58
|
+
*/
|
|
59
|
+
setVideoElementRef?: (element: HTMLElement | null) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Custom class applied to the root DOM element.
|
|
63
|
+
*/
|
|
64
|
+
className?: string;
|
|
65
|
+
} & Pick<VideoProps, 'VideoPlaceholder'>;
|
|
66
|
+
|
|
67
|
+
export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
|
|
68
|
+
(
|
|
69
|
+
{
|
|
70
|
+
participant,
|
|
71
|
+
sinkId,
|
|
72
|
+
videoKind = 'video',
|
|
73
|
+
muteAudio,
|
|
74
|
+
setVideoElementRef,
|
|
75
|
+
className,
|
|
76
|
+
VideoPlaceholder,
|
|
77
|
+
ParticipantViewUI = DefaultParticipantViewUI as ComponentType<ParticipantViewUIProps>,
|
|
78
|
+
},
|
|
79
|
+
ref,
|
|
80
|
+
) => {
|
|
81
|
+
const {
|
|
82
|
+
audioStream,
|
|
83
|
+
isLoggedInUser,
|
|
84
|
+
isSpeaking,
|
|
85
|
+
publishedTracks,
|
|
86
|
+
sessionId,
|
|
87
|
+
} = participant;
|
|
88
|
+
|
|
89
|
+
const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
|
|
90
|
+
const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
|
|
91
|
+
|
|
92
|
+
const [trackedElement, setTrackedElement] = useState<HTMLDivElement | null>(
|
|
93
|
+
null,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
useTrackElementVisibility({
|
|
97
|
+
sessionId,
|
|
98
|
+
trackedElement,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
ref={(element) => {
|
|
104
|
+
applyElementRef(ref, element);
|
|
105
|
+
setTrackedElement(element);
|
|
106
|
+
}}
|
|
107
|
+
className={clsx(
|
|
108
|
+
'str-video__participant-view',
|
|
109
|
+
isSpeaking && 'str-video__participant-view--speaking',
|
|
110
|
+
!hasVideo && 'str-video__participant-view--no-video',
|
|
111
|
+
!hasAudio && 'str-video__participant-view--no-audio',
|
|
112
|
+
className,
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
<Audio
|
|
116
|
+
// mute the local participant, as we don't want to hear ourselves
|
|
117
|
+
muted={isLoggedInUser || muteAudio}
|
|
118
|
+
sinkId={sinkId}
|
|
119
|
+
audioStream={audioStream}
|
|
120
|
+
/>
|
|
121
|
+
<Video
|
|
122
|
+
VideoPlaceholder={VideoPlaceholder}
|
|
123
|
+
participant={participant}
|
|
124
|
+
kind={videoKind}
|
|
125
|
+
setVideoElementRef={setVideoElementRef}
|
|
126
|
+
autoPlay
|
|
127
|
+
/>
|
|
128
|
+
{isComponentType(ParticipantViewUI) ? (
|
|
129
|
+
<ParticipantViewUI participant={participant} />
|
|
130
|
+
) : (
|
|
131
|
+
ParticipantViewUI
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DetailedHTMLProps,
|
|
3
|
-
ForwardedRef,
|
|
4
|
-
forwardRef,
|
|
5
|
-
useEffect,
|
|
6
|
-
useState,
|
|
7
|
-
VideoHTMLAttributes,
|
|
8
|
-
} from 'react';
|
|
9
|
-
import clsx from 'clsx';
|
|
1
|
+
import { ComponentPropsWithRef, forwardRef, useEffect, useState } from 'react';
|
|
10
2
|
import { Browsers } from '@stream-io/video-client';
|
|
11
3
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
> & {
|
|
4
|
+
import { applyElementRef } from '../../../utilities';
|
|
5
|
+
|
|
6
|
+
export type BaseVideoProps = ComponentPropsWithRef<'video'> & {
|
|
16
7
|
stream?: MediaStream;
|
|
17
8
|
};
|
|
18
9
|
|
|
@@ -20,19 +11,11 @@ export type VideoProps = DetailedHTMLProps<
|
|
|
20
11
|
* @description Extends video element with `stream` property
|
|
21
12
|
* (`srcObject`) to reactively handle stream changes
|
|
22
13
|
*/
|
|
23
|
-
export const BaseVideo = forwardRef<HTMLVideoElement,
|
|
14
|
+
export const BaseVideo = forwardRef<HTMLVideoElement, BaseVideoProps>(
|
|
24
15
|
({ stream, ...rest }, ref) => {
|
|
25
16
|
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
|
|
26
17
|
null,
|
|
27
18
|
);
|
|
28
|
-
const setRef: ForwardedRef<HTMLVideoElement> = (instance) => {
|
|
29
|
-
setVideoElement(instance);
|
|
30
|
-
if (typeof ref === 'function') {
|
|
31
|
-
(ref as (instance: HTMLVideoElement | null) => void)(instance);
|
|
32
|
-
} else if (ref) {
|
|
33
|
-
ref.current = instance;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
19
|
|
|
37
20
|
useEffect(() => {
|
|
38
21
|
if (!videoElement || !stream) return;
|
|
@@ -60,8 +43,10 @@ export const BaseVideo = forwardRef<HTMLVideoElement, VideoProps>(
|
|
|
60
43
|
autoPlay
|
|
61
44
|
playsInline
|
|
62
45
|
{...rest}
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
ref={(element) => {
|
|
47
|
+
applyElementRef(ref, element);
|
|
48
|
+
setVideoElement(element);
|
|
49
|
+
}}
|
|
65
50
|
/>
|
|
66
51
|
);
|
|
67
52
|
},
|
|
@@ -1,45 +1,60 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
ComponentType,
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
5
|
useRef,
|
|
6
6
|
useState,
|
|
7
|
-
|
|
7
|
+
ComponentPropsWithoutRef,
|
|
8
8
|
} from 'react';
|
|
9
9
|
import {
|
|
10
|
-
Call,
|
|
11
10
|
DebounceType,
|
|
12
11
|
SfuModels,
|
|
13
12
|
StreamVideoParticipant,
|
|
14
13
|
VisibilityState,
|
|
15
14
|
} from '@stream-io/video-client';
|
|
16
15
|
import clsx from 'clsx';
|
|
17
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
VideoPlaceholder as DefaultVideoPlaceholder,
|
|
18
|
+
VideoPlaceholderProps,
|
|
19
|
+
} from './VideoPlaceholder';
|
|
18
20
|
import { BaseVideo } from './BaseVideo';
|
|
21
|
+
import { useCall } from '@stream-io/video-react-bindings';
|
|
22
|
+
|
|
23
|
+
export type VideoProps = ComponentPropsWithoutRef<'video'> & {
|
|
24
|
+
kind: 'video' | 'screen';
|
|
25
|
+
participant: StreamVideoParticipant;
|
|
26
|
+
setVideoElementRef?: (element: HTMLElement | null) => void;
|
|
27
|
+
VideoPlaceholder?: ComponentType<VideoPlaceholderProps>;
|
|
28
|
+
};
|
|
19
29
|
|
|
20
|
-
export const Video = (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
export const Video = ({
|
|
31
|
+
kind,
|
|
32
|
+
participant,
|
|
33
|
+
className,
|
|
34
|
+
setVideoElementRef,
|
|
35
|
+
VideoPlaceholder = DefaultVideoPlaceholder,
|
|
36
|
+
...rest
|
|
37
|
+
}: VideoProps) => {
|
|
38
|
+
const {
|
|
39
|
+
sessionId,
|
|
40
|
+
videoStream,
|
|
41
|
+
screenShareStream,
|
|
42
|
+
publishedTracks,
|
|
43
|
+
viewportVisibilityState,
|
|
44
|
+
isLoggedInUser,
|
|
45
|
+
userId,
|
|
46
|
+
} = participant;
|
|
47
|
+
|
|
48
|
+
const call = useCall();
|
|
35
49
|
|
|
36
50
|
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
|
|
37
51
|
null,
|
|
38
52
|
);
|
|
53
|
+
|
|
39
54
|
// const [videoTrackMuted, setVideoTrackMuted] = useState(false);
|
|
40
55
|
const [videoPlaying, setVideoPlaying] = useState(false);
|
|
41
56
|
const viewportVisibilityRef = useRef<VisibilityState | undefined>(
|
|
42
|
-
|
|
57
|
+
viewportVisibilityState,
|
|
43
58
|
);
|
|
44
59
|
|
|
45
60
|
const stream = kind === 'video' ? videoStream : screenShareStream;
|
|
@@ -75,7 +90,7 @@ export const Video = (
|
|
|
75
90
|
|
|
76
91
|
const displayPlaceholder =
|
|
77
92
|
!isPublishingTrack ||
|
|
78
|
-
(
|
|
93
|
+
(viewportVisibilityState === VisibilityState.INVISIBLE &&
|
|
79
94
|
!screenShareStream) ||
|
|
80
95
|
!videoPlaying;
|
|
81
96
|
|
|
@@ -85,6 +100,8 @@ export const Video = (
|
|
|
85
100
|
dimension?: SfuModels.VideoDimension,
|
|
86
101
|
type: DebounceType = DebounceType.SLOW,
|
|
87
102
|
) => {
|
|
103
|
+
if (!call) return;
|
|
104
|
+
|
|
88
105
|
call.updateSubscriptionsPartial(
|
|
89
106
|
kind,
|
|
90
107
|
{
|
|
@@ -98,17 +115,14 @@ export const Video = (
|
|
|
98
115
|
[call, kind, sessionId],
|
|
99
116
|
);
|
|
100
117
|
|
|
101
|
-
// handle generic subscription updates
|
|
102
|
-
|
|
103
118
|
// handle visibility subscription updates
|
|
104
119
|
useEffect(() => {
|
|
105
|
-
viewportVisibilityRef.current =
|
|
120
|
+
viewportVisibilityRef.current = viewportVisibilityState;
|
|
106
121
|
|
|
107
|
-
if (!videoElement || !isPublishingTrack ||
|
|
108
|
-
return;
|
|
122
|
+
if (!videoElement || !isPublishingTrack || isLoggedInUser) return;
|
|
109
123
|
|
|
110
124
|
const isInvisibleVVS =
|
|
111
|
-
|
|
125
|
+
viewportVisibilityState === VisibilityState.INVISIBLE;
|
|
112
126
|
|
|
113
127
|
updateSubscription(
|
|
114
128
|
isInvisibleVVS
|
|
@@ -121,16 +135,15 @@ export const Video = (
|
|
|
121
135
|
);
|
|
122
136
|
}, [
|
|
123
137
|
updateSubscription,
|
|
124
|
-
|
|
138
|
+
viewportVisibilityState,
|
|
125
139
|
videoElement,
|
|
126
140
|
isPublishingTrack,
|
|
127
|
-
|
|
141
|
+
isLoggedInUser,
|
|
128
142
|
]);
|
|
129
143
|
|
|
130
144
|
// handle resize subscription updates
|
|
131
145
|
useEffect(() => {
|
|
132
|
-
if (!videoElement || !isPublishingTrack ||
|
|
133
|
-
return;
|
|
146
|
+
if (!videoElement || !isPublishingTrack || isLoggedInUser) return;
|
|
134
147
|
|
|
135
148
|
const resizeObserver = new ResizeObserver(() => {
|
|
136
149
|
const currentDimensions = `${videoElement.clientWidth},${videoElement.clientHeight}`;
|
|
@@ -163,14 +176,14 @@ export const Video = (
|
|
|
163
176
|
}, [
|
|
164
177
|
updateSubscription,
|
|
165
178
|
videoElement,
|
|
166
|
-
|
|
179
|
+
viewportVisibilityState,
|
|
167
180
|
isPublishingTrack,
|
|
168
|
-
|
|
181
|
+
isLoggedInUser,
|
|
169
182
|
]);
|
|
170
183
|
|
|
184
|
+
// handle generic subscription updates
|
|
171
185
|
useEffect(() => {
|
|
172
|
-
if (!isPublishingTrack || !videoElement ||
|
|
173
|
-
return;
|
|
186
|
+
if (!isPublishingTrack || !videoElement || isLoggedInUser) return;
|
|
174
187
|
|
|
175
188
|
updateSubscription(
|
|
176
189
|
{
|
|
@@ -183,12 +196,7 @@ export const Video = (
|
|
|
183
196
|
return () => {
|
|
184
197
|
updateSubscription(undefined, DebounceType.FAST);
|
|
185
198
|
};
|
|
186
|
-
}, [
|
|
187
|
-
updateSubscription,
|
|
188
|
-
videoElement,
|
|
189
|
-
isPublishingTrack,
|
|
190
|
-
participant.isLoggedInUser,
|
|
191
|
-
]);
|
|
199
|
+
}, [updateSubscription, videoElement, isPublishingTrack, isLoggedInUser]);
|
|
192
200
|
|
|
193
201
|
const [isWideMode, setIsWideMode] = useState(true);
|
|
194
202
|
useEffect(() => {
|
|
@@ -210,16 +218,19 @@ export const Video = (
|
|
|
210
218
|
};
|
|
211
219
|
}, [stream, videoElement]);
|
|
212
220
|
|
|
221
|
+
if (!call) return null;
|
|
222
|
+
|
|
213
223
|
return (
|
|
214
224
|
<>
|
|
215
225
|
<BaseVideo
|
|
216
226
|
{...rest}
|
|
217
227
|
stream={stream}
|
|
218
|
-
className={clsx(className, {
|
|
219
|
-
'str-video__video--wide': isWideMode,
|
|
228
|
+
className={clsx(className, 'str-video__video', {
|
|
220
229
|
'str-video__video--tall': !isWideMode,
|
|
230
|
+
'str-video__video--mirror': isLoggedInUser && kind === 'video',
|
|
231
|
+
'str-video__video--screen-share': kind === 'screen',
|
|
221
232
|
})}
|
|
222
|
-
data-user-id={
|
|
233
|
+
data-user-id={userId}
|
|
223
234
|
data-session-id={sessionId}
|
|
224
235
|
ref={(ref) => {
|
|
225
236
|
setVideoElement(ref);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { StreamVideoParticipant } from '@stream-io/video-client';
|
|
1
|
+
import { ComponentPropsWithRef, forwardRef, useState } from 'react';
|
|
2
|
+
import type { StreamVideoParticipant } from '@stream-io/video-client';
|
|
4
3
|
|
|
5
4
|
export type VideoPlaceholderProps = {
|
|
6
5
|
participant: StreamVideoParticipant;
|
|
7
|
-
} &
|
|
6
|
+
} & ComponentPropsWithRef<'div'>;
|
|
8
7
|
|
|
9
8
|
export const VideoPlaceholder = forwardRef<
|
|
10
9
|
HTMLDivElement,
|
|
@@ -15,10 +14,10 @@ export const VideoPlaceholder = forwardRef<
|
|
|
15
14
|
const name = participant?.name || participant?.userId;
|
|
16
15
|
|
|
17
16
|
return (
|
|
18
|
-
<div className="str-
|
|
17
|
+
<div className="str-video__video-placeholder" style={style} ref={ref}>
|
|
19
18
|
{(!participant.image || error) &&
|
|
20
19
|
(name ? (
|
|
21
|
-
<div className="str-
|
|
20
|
+
<div className="str-video__video-placeholder__initials-fallback">
|
|
22
21
|
<div>{name[0]}</div>
|
|
23
22
|
</div>
|
|
24
23
|
) : (
|
|
@@ -27,11 +26,8 @@ export const VideoPlaceholder = forwardRef<
|
|
|
27
26
|
{participant.image && !error && (
|
|
28
27
|
<img
|
|
29
28
|
onError={() => setError(true)}
|
|
30
|
-
alt="
|
|
31
|
-
className=
|
|
32
|
-
'str-video__participant-placeholder--avatar-speaking':
|
|
33
|
-
participant.isSpeaking,
|
|
34
|
-
})}
|
|
29
|
+
alt="video-placeholder"
|
|
30
|
+
className="str-video__video-placeholder__avatar"
|
|
35
31
|
src={participant.image}
|
|
36
32
|
/>
|
|
37
33
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './Audio';
|
|
2
|
-
export * from './
|
|
2
|
+
export * from './ParticipantView';
|
|
3
3
|
|
|
4
4
|
export { Video } from './Video';
|
|
5
|
-
export type { VideoProps } from './Video';
|
|
5
|
+
export type { BaseVideoProps, VideoProps } from './Video';
|
|
6
6
|
|
|
7
7
|
export * from './CallLayout';
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { ViewportTracker, VisibilityState } from '@stream-io/video-client';
|
|
3
|
+
import { useCall } from '@stream-io/video-react-bindings';
|
|
4
|
+
|
|
5
|
+
export const useTrackElementVisibility = <T extends HTMLElement>({
|
|
6
|
+
trackedElement,
|
|
7
|
+
viewportTracker: propsViewportTracker,
|
|
8
|
+
sessionId,
|
|
9
|
+
}: {
|
|
10
|
+
trackedElement: T | null;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
viewportTracker?: ViewportTracker;
|
|
13
|
+
}) => {
|
|
14
|
+
const call = useCall();
|
|
15
|
+
|
|
16
|
+
const viewportTracker = propsViewportTracker ?? call?.viewportTracker;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!trackedElement || !viewportTracker || !call) return;
|
|
20
|
+
|
|
21
|
+
const unobserve = viewportTracker.observe(trackedElement, (entry) => {
|
|
22
|
+
call.state.updateParticipant(sessionId, (p) => ({
|
|
23
|
+
...p,
|
|
24
|
+
viewportVisibilityState: entry.isIntersecting
|
|
25
|
+
? VisibilityState.VISIBLE
|
|
26
|
+
: VisibilityState.INVISIBLE,
|
|
27
|
+
}));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
unobserve();
|
|
32
|
+
// reset visibility state to UNKNOWN upon cleanup
|
|
33
|
+
// so that the layouts that are not actively observed
|
|
34
|
+
// can still function normally (runtime layout switching)
|
|
35
|
+
call.state.updateParticipant(sessionId, (p) => ({
|
|
36
|
+
...p,
|
|
37
|
+
viewportVisibilityState: VisibilityState.UNKNOWN,
|
|
38
|
+
}));
|
|
39
|
+
};
|
|
40
|
+
}, [trackedElement, viewportTracker, call, sessionId]);
|
|
41
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ForwardedRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export const applyElementRef = <T extends HTMLElement | null>(
|
|
4
|
+
ref: ForwardedRef<T>,
|
|
5
|
+
element: T,
|
|
6
|
+
) => {
|
|
7
|
+
if (!ref) return;
|
|
8
|
+
|
|
9
|
+
if (typeof ref === 'function') return ref(element);
|
|
10
|
+
|
|
11
|
+
ref.current = element;
|
|
12
|
+
};
|