Show tiles for members we're trying to connect to
This should help give more context on what's going wrong in splitbrain scenarios. If users leave calls uncleanly, their tile will remain in until their member event times out, which will be an hour from when they joined the call. See https://github.com/vector-im/element-call/issues/639. Part of https://github.com/vector-im/element-call/issues/616
This commit is contained in:
parent
54fe2aa7a3
commit
1ea9432769
11 changed files with 85 additions and 61 deletions
|
|
@ -76,7 +76,6 @@ export function GroupCallView({
|
||||||
toggleScreensharing,
|
toggleScreensharing,
|
||||||
requestingScreenshare,
|
requestingScreenshare,
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
localScreenshareFeed,
|
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
participants,
|
participants,
|
||||||
unencryptedEventsFromUsers,
|
unencryptedEventsFromUsers,
|
||||||
|
|
@ -221,6 +220,7 @@ export function GroupCallView({
|
||||||
client={client}
|
client={client}
|
||||||
roomName={groupCall.room.name}
|
roomName={groupCall.room.name}
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
|
participants={participants}
|
||||||
microphoneMuted={microphoneMuted}
|
microphoneMuted={microphoneMuted}
|
||||||
localVideoMuted={localVideoMuted}
|
localVideoMuted={localVideoMuted}
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
|
|
@ -230,7 +230,6 @@ export function GroupCallView({
|
||||||
onLeave={onLeave}
|
onLeave={onLeave}
|
||||||
toggleScreensharing={toggleScreensharing}
|
toggleScreensharing={toggleScreensharing}
|
||||||
isScreensharing={isScreensharing}
|
isScreensharing={isScreensharing}
|
||||||
localScreenshareFeed={localScreenshareFeed}
|
|
||||||
screenshareFeeds={screenshareFeeds}
|
screenshareFeeds={screenshareFeeds}
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
groupCall: GroupCall;
|
groupCall: GroupCall;
|
||||||
|
participants: RoomMember[];
|
||||||
roomName: string;
|
roomName: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
microphoneMuted: boolean;
|
microphoneMuted: boolean;
|
||||||
|
|
@ -82,14 +83,16 @@ interface Props {
|
||||||
onLeave: () => void;
|
onLeave: () => void;
|
||||||
isScreensharing: boolean;
|
isScreensharing: boolean;
|
||||||
screenshareFeeds: CallFeed[];
|
screenshareFeeds: CallFeed[];
|
||||||
localScreenshareFeed: CallFeed;
|
|
||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
unencryptedEventsFromUsers: Set<string>;
|
unencryptedEventsFromUsers: Set<string>;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Participant {
|
// Represents something that should get a tile on the layout,
|
||||||
|
// ie. a user's video feed or a screen share feed.
|
||||||
|
export interface TileDescriptor {
|
||||||
id: string;
|
id: string;
|
||||||
|
member: RoomMember;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
presenter: boolean;
|
presenter: boolean;
|
||||||
callFeed?: CallFeed;
|
callFeed?: CallFeed;
|
||||||
|
|
@ -99,6 +102,7 @@ export interface Participant {
|
||||||
export function InCallView({
|
export function InCallView({
|
||||||
client,
|
client,
|
||||||
groupCall,
|
groupCall,
|
||||||
|
participants,
|
||||||
roomName,
|
roomName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
microphoneMuted,
|
microphoneMuted,
|
||||||
|
|
@ -111,7 +115,6 @@ export function InCallView({
|
||||||
toggleScreensharing,
|
toggleScreensharing,
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
localScreenshareFeed,
|
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
unencryptedEventsFromUsers,
|
unencryptedEventsFromUsers,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
|
|
@ -185,39 +188,48 @@ export function InCallView({
|
||||||
}, [setLayout]);
|
}, [setLayout]);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const participants: Participant[] = [];
|
const tileDescriptors: TileDescriptor[] = [];
|
||||||
|
|
||||||
for (const callFeed of userMediaFeeds) {
|
// one tile for each participants, to start with (we want a tile for everyone we
|
||||||
participants.push({
|
// think should be in the call, even if we don't have a media feed for them yet)
|
||||||
id: callFeed.stream.id,
|
for (const p of participants) {
|
||||||
callFeed,
|
const userMediaFeed = userMediaFeeds.find((f) => f.userId === p.userId);
|
||||||
focused:
|
|
||||||
screenshareFeeds.length === 0 && callFeed.userId === activeSpeaker,
|
// NB. this assumes that the same user can't join more than once from multiple
|
||||||
isLocal: callFeed.isLocal(),
|
// devices, but the participants are just RoomMembers, so this assumption is baked
|
||||||
|
// into GroupCall itself.
|
||||||
|
tileDescriptors.push({
|
||||||
|
id: p.userId,
|
||||||
|
member: p,
|
||||||
|
callFeed: userMediaFeed,
|
||||||
|
focused: screenshareFeeds.length === 0 && p.userId === activeSpeaker,
|
||||||
|
isLocal: p.userId === client.getUserId(),
|
||||||
presenter: false,
|
presenter: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const callFeed of screenshareFeeds) {
|
// add the screenshares too
|
||||||
const userMediaItem = participants.find(
|
for (const screenshareFeed of screenshareFeeds) {
|
||||||
(item) => item.callFeed.userId === callFeed.userId
|
const userMediaItem = tileDescriptors.find(
|
||||||
|
(item) => item.member.userId === screenshareFeed.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userMediaItem) {
|
if (userMediaItem) {
|
||||||
userMediaItem.presenter = true;
|
userMediaItem.presenter = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
participants.push({
|
tileDescriptors.push({
|
||||||
id: callFeed.stream.id,
|
id: screenshareFeed.stream.id,
|
||||||
callFeed,
|
member: userMediaItem?.member,
|
||||||
|
callFeed: screenshareFeed,
|
||||||
focused: true,
|
focused: true,
|
||||||
isLocal: callFeed.isLocal(),
|
isLocal: screenshareFeed.isLocal(),
|
||||||
presenter: false,
|
presenter: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return participants;
|
return tileDescriptors;
|
||||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
}, [client, participants, userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
||||||
|
|
||||||
// The maximised participant: either the participant that the user has
|
// The maximised participant: either the participant that the user has
|
||||||
// manually put in fullscreen, or the focused (active) participant if the
|
// manually put in fullscreen, or the focused (active) participant if the
|
||||||
|
|
@ -281,7 +293,13 @@ export function InCallView({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
|
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
|
||||||
{({ item, ...rest }: { item: Participant; [x: string]: unknown }) => (
|
{({
|
||||||
|
item,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
item: TileDescriptor;
|
||||||
|
[x: string]: unknown;
|
||||||
|
}) => (
|
||||||
<VideoTileContainer
|
<VideoTileContainer
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ export interface UseGroupCallReturnType {
|
||||||
requestingScreenshare: boolean;
|
requestingScreenshare: boolean;
|
||||||
isScreensharing: boolean;
|
isScreensharing: boolean;
|
||||||
screenshareFeeds: CallFeed[];
|
screenshareFeeds: CallFeed[];
|
||||||
localScreenshareFeed: CallFeed;
|
|
||||||
localDesktopCapturerSourceId: string;
|
localDesktopCapturerSourceId: string;
|
||||||
participants: RoomMember[];
|
participants: RoomMember[];
|
||||||
hasLocalParticipant: boolean;
|
hasLocalParticipant: boolean;
|
||||||
|
|
@ -66,7 +65,6 @@ interface State {
|
||||||
microphoneMuted: boolean;
|
microphoneMuted: boolean;
|
||||||
localVideoMuted: boolean;
|
localVideoMuted: boolean;
|
||||||
screenshareFeeds: CallFeed[];
|
screenshareFeeds: CallFeed[];
|
||||||
localScreenshareFeed: CallFeed;
|
|
||||||
localDesktopCapturerSourceId: string;
|
localDesktopCapturerSourceId: string;
|
||||||
isScreensharing: boolean;
|
isScreensharing: boolean;
|
||||||
requestingScreenshare: boolean;
|
requestingScreenshare: boolean;
|
||||||
|
|
@ -87,7 +85,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
localVideoMuted,
|
localVideoMuted,
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
localScreenshareFeed,
|
|
||||||
localDesktopCapturerSourceId,
|
localDesktopCapturerSourceId,
|
||||||
participants,
|
participants,
|
||||||
hasLocalParticipant,
|
hasLocalParticipant,
|
||||||
|
|
@ -105,7 +102,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
localVideoMuted: false,
|
localVideoMuted: false,
|
||||||
isScreensharing: false,
|
isScreensharing: false,
|
||||||
screenshareFeeds: [],
|
screenshareFeeds: [],
|
||||||
localScreenshareFeed: null,
|
|
||||||
localDesktopCapturerSourceId: null,
|
localDesktopCapturerSourceId: null,
|
||||||
requestingScreenshare: false,
|
requestingScreenshare: false,
|
||||||
participants: [],
|
participants: [],
|
||||||
|
|
@ -133,7 +129,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||||
isScreensharing: groupCall.isScreensharing(),
|
isScreensharing: groupCall.isScreensharing(),
|
||||||
localScreenshareFeed: groupCall.localScreenshareFeed,
|
|
||||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||||
participants: [...groupCall.participants],
|
participants: [...groupCall.participants],
|
||||||
|
|
@ -170,12 +165,11 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
|
|
||||||
function onLocalScreenshareStateChanged(
|
function onLocalScreenshareStateChanged(
|
||||||
isScreensharing: boolean,
|
isScreensharing: boolean,
|
||||||
localScreenshareFeed: CallFeed,
|
_localScreenshareFeed: CallFeed,
|
||||||
localDesktopCapturerSourceId: string
|
localDesktopCapturerSourceId: string
|
||||||
): void {
|
): void {
|
||||||
updateState({
|
updateState({
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
localScreenshareFeed,
|
|
||||||
localDesktopCapturerSourceId,
|
localDesktopCapturerSourceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +220,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||||
isScreensharing: groupCall.isScreensharing(),
|
isScreensharing: groupCall.isScreensharing(),
|
||||||
localScreenshareFeed: groupCall.localScreenshareFeed,
|
|
||||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||||
participants: [...groupCall.participants],
|
participants: [...groupCall.participants],
|
||||||
|
|
@ -342,7 +335,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
requestingScreenshare,
|
requestingScreenshare,
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
localScreenshareFeed,
|
|
||||||
localDesktopCapturerSourceId,
|
localDesktopCapturerSourceId,
|
||||||
participants,
|
participants,
|
||||||
hasLocalParticipant,
|
hasLocalParticipant,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { FC, useEffect, useRef } from "react";
|
import React, { FC, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { Participant } from "../room/InCallView";
|
import { TileDescriptor } from "../room/InCallView";
|
||||||
import { useCallFeed } from "./useCallFeed";
|
import { useCallFeed } from "./useCallFeed";
|
||||||
import { useMediaStreamTrackCount } from "./useMediaStream";
|
import { useMediaStreamTrackCount } from "./useMediaStream";
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { useMediaStreamTrackCount } from "./useMediaStream";
|
||||||
// only way to a hook on an array
|
// only way to a hook on an array
|
||||||
|
|
||||||
interface AudioForParticipantProps {
|
interface AudioForParticipantProps {
|
||||||
item: Participant;
|
item: TileDescriptor;
|
||||||
audioContext: AudioContext;
|
audioContext: AudioContext;
|
||||||
audioDestination: AudioNode;
|
audioDestination: AudioNode;
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +78,7 @@ export const AudioForParticipant: FC<AudioForParticipantProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AudioContainerProps {
|
interface AudioContainerProps {
|
||||||
items: Participant[];
|
items: TileDescriptor[];
|
||||||
audioContext: AudioContext;
|
audioContext: AudioContext;
|
||||||
audioDestination: AudioNode;
|
audioDestination: AudioNode;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { RoomMember } from "matrix-js-sdk";
|
||||||
|
|
||||||
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
|
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
|
||||||
import { VideoTile } from "./VideoTile";
|
import { VideoTile } from "./VideoTile";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Participant } from "../room/InCallView";
|
import { TileDescriptor } from "../room/InCallView";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "VideoGrid",
|
title: "VideoGrid",
|
||||||
|
|
@ -33,10 +34,11 @@ export const ParticipantsTest = () => {
|
||||||
const { layout, setLayout } = useVideoGridLayout(false);
|
const { layout, setLayout } = useVideoGridLayout(false);
|
||||||
const [participantCount, setParticipantCount] = useState(1);
|
const [participantCount, setParticipantCount] = useState(1);
|
||||||
|
|
||||||
const items: Participant[] = useMemo(
|
const items: TileDescriptor[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Array(participantCount).fill(undefined).map((_, i) => ({
|
new Array(participantCount).fill(undefined).map((_, i) => ({
|
||||||
id: (i + 1).toString(),
|
id: (i + 1).toString(),
|
||||||
|
member: new RoomMember("!fake:room.id", `@user${i}:fake.dummy`),
|
||||||
focused: false,
|
focused: false,
|
||||||
presenter: false,
|
presenter: false,
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/typ
|
||||||
|
|
||||||
import styles from "./VideoGrid.module.css";
|
import styles from "./VideoGrid.module.css";
|
||||||
import { Layout } from "../room/GridLayoutMenu";
|
import { Layout } from "../room/GridLayoutMenu";
|
||||||
import { Participant } from "../room/InCallView";
|
import { TileDescriptor } from "../room/InCallView";
|
||||||
|
|
||||||
interface TilePosition {
|
interface TilePosition {
|
||||||
x: number;
|
x: number;
|
||||||
|
|
@ -36,7 +36,7 @@ interface TilePosition {
|
||||||
interface Tile {
|
interface Tile {
|
||||||
key: Key;
|
key: Key;
|
||||||
order: number;
|
order: number;
|
||||||
item: Participant;
|
item: TileDescriptor;
|
||||||
remove: boolean;
|
remove: boolean;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
presenter: boolean;
|
presenter: boolean;
|
||||||
|
|
@ -693,12 +693,12 @@ interface ChildrenProperties extends ReactDOMAttributes {
|
||||||
};
|
};
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
item: Participant;
|
item: TileDescriptor;
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoGridProps {
|
interface VideoGridProps {
|
||||||
items: Participant[];
|
items: TileDescriptor[];
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
disableAnimations?: boolean;
|
disableAnimations?: boolean;
|
||||||
children: (props: ChildrenProperties) => React.ReactNode;
|
children: (props: ChildrenProperties) => React.ReactNode;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { AudioButton, FullscreenButton } from "../button/Button";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
|
hasFeed: Boolean;
|
||||||
speaking?: boolean;
|
speaking?: boolean;
|
||||||
audioMuted?: boolean;
|
audioMuted?: boolean;
|
||||||
videoMuted?: boolean;
|
videoMuted?: boolean;
|
||||||
|
|
@ -47,6 +48,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
|
hasFeed,
|
||||||
speaking,
|
speaking,
|
||||||
audioMuted,
|
audioMuted,
|
||||||
videoMuted,
|
videoMuted,
|
||||||
|
|
@ -90,6 +92,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const caption = hasFeed ? name : t("{{name}} (Connecting...)", { name });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<animated.div
|
<animated.div
|
||||||
className={classNames(styles.videoTile, className, {
|
className={classNames(styles.videoTile, className, {
|
||||||
|
|
@ -120,7 +124,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||||
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
||||||
{audioMuted && !videoMuted && <MicMutedIcon />}
|
{audioMuted && !videoMuted && <MicMutedIcon />}
|
||||||
{videoMuted && <VideoMutedIcon />}
|
{videoMuted && <VideoMutedIcon />}
|
||||||
<span title={name}>{name}</span>
|
<span title={caption}>{caption}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<video ref={mediaRef} playsInline disablePictureInPicture />
|
<video ref={mediaRef} playsInline disablePictureInPicture />
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ import { useRoomMemberName } from "./useRoomMemberName";
|
||||||
import { VideoTile } from "./VideoTile";
|
import { VideoTile } from "./VideoTile";
|
||||||
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
import { Participant } from "../room/InCallView";
|
import { TileDescriptor } from "../room/InCallView";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: Participant;
|
item: TileDescriptor;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
getAvatar: (
|
getAvatar: (
|
||||||
|
|
@ -41,7 +41,7 @@ interface Props {
|
||||||
disableSpeakingIndicator: boolean;
|
disableSpeakingIndicator: boolean;
|
||||||
maximised: boolean;
|
maximised: boolean;
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
onFullscreen: (item: Participant) => void;
|
onFullscreen: (item: TileDescriptor) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoTileContainer({
|
export function VideoTileContainer({
|
||||||
|
|
@ -65,9 +65,8 @@ export function VideoTileContainer({
|
||||||
speaking,
|
speaking,
|
||||||
stream,
|
stream,
|
||||||
purpose,
|
purpose,
|
||||||
member,
|
|
||||||
} = useCallFeed(item.callFeed);
|
} = useCallFeed(item.callFeed);
|
||||||
const { rawDisplayName } = useRoomMemberName(member);
|
const { rawDisplayName } = useRoomMemberName(item.member);
|
||||||
const [tileRef, mediaRef] = useSpatialMediaStream(
|
const [tileRef, mediaRef] = useSpatialMediaStream(
|
||||||
stream,
|
stream,
|
||||||
audioContext,
|
audioContext,
|
||||||
|
|
@ -99,9 +98,10 @@ export function VideoTileContainer({
|
||||||
videoMuted={videoMuted}
|
videoMuted={videoMuted}
|
||||||
screenshare={purpose === SDPStreamMetadataPurpose.Screenshare}
|
screenshare={purpose === SDPStreamMetadataPurpose.Screenshare}
|
||||||
name={rawDisplayName}
|
name={rawDisplayName}
|
||||||
|
hasFeed={Boolean(item.callFeed)}
|
||||||
ref={tileRef}
|
ref={tileRef}
|
||||||
mediaRef={mediaRef}
|
mediaRef={mediaRef}
|
||||||
avatar={getAvatar && getAvatar(member, width, height)}
|
avatar={getAvatar && getAvatar(item.member, width, height)}
|
||||||
onOptionsPress={onOptionsPress}
|
onOptionsPress={onOptionsPress}
|
||||||
localVolume={localVolume}
|
localVolume={localVolume}
|
||||||
maximised={maximised}
|
maximised={maximised}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|
||||||
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
|
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
|
||||||
|
|
||||||
interface CallFeedState {
|
interface CallFeedState {
|
||||||
member: RoomMember;
|
callFeed: CallFeed;
|
||||||
isLocal: boolean;
|
isLocal: boolean;
|
||||||
speaking: boolean;
|
speaking: boolean;
|
||||||
videoMuted: boolean;
|
videoMuted: boolean;
|
||||||
|
|
@ -32,7 +31,7 @@ interface CallFeedState {
|
||||||
}
|
}
|
||||||
function getCallFeedState(callFeed: CallFeed): CallFeedState {
|
function getCallFeedState(callFeed: CallFeed): CallFeedState {
|
||||||
return {
|
return {
|
||||||
member: callFeed ? callFeed.getMember() : null,
|
callFeed,
|
||||||
isLocal: callFeed ? callFeed.isLocal() : false,
|
isLocal: callFeed ? callFeed.isLocal() : false,
|
||||||
speaking: callFeed ? callFeed.isSpeaking() : false,
|
speaking: callFeed ? callFeed.isSpeaking() : false,
|
||||||
videoMuted: callFeed ? callFeed.isVideoMuted() : true,
|
videoMuted: callFeed ? callFeed.isVideoMuted() : true,
|
||||||
|
|
|
||||||
|
|
@ -17,27 +17,27 @@ limitations under the License.
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Participant } from "../room/InCallView";
|
import { TileDescriptor } from "../room/InCallView";
|
||||||
import { useEventTarget } from "../useEvents";
|
import { useEventTarget } from "../useEvents";
|
||||||
import { useCallFeed } from "./useCallFeed";
|
import { useCallFeed } from "./useCallFeed";
|
||||||
|
|
||||||
export function useFullscreen(ref: React.RefObject<HTMLElement>): {
|
export function useFullscreen(ref: React.RefObject<HTMLElement>): {
|
||||||
toggleFullscreen: (participant: Participant) => void;
|
toggleFullscreen: (participant: TileDescriptor) => void;
|
||||||
fullscreenParticipant: Participant | null;
|
fullscreenParticipant: TileDescriptor | null;
|
||||||
} {
|
} {
|
||||||
const [fullscreenParticipant, setFullscreenParticipant] =
|
const [fullscreenParticipant, setFullscreenParticipant] =
|
||||||
useState<Participant | null>(null);
|
useState<TileDescriptor | null>(null);
|
||||||
const { disposed } = useCallFeed(fullscreenParticipant?.callFeed);
|
const { disposed } = useCallFeed(fullscreenParticipant?.callFeed);
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(
|
const toggleFullscreen = useCallback(
|
||||||
(participant: Participant) => {
|
(tileDes: TileDescriptor) => {
|
||||||
if (fullscreenParticipant) {
|
if (fullscreenParticipant) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
setFullscreenParticipant(null);
|
setFullscreenParticipant(null);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
ref.current.requestFullscreen();
|
ref.current.requestFullscreen();
|
||||||
setFullscreenParticipant(participant);
|
setFullscreenParticipant(tileDes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to fullscreen:", error);
|
console.warn("Failed to fullscreen:", error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,21 +35,31 @@ declare global {
|
||||||
export const useMediaStreamTrackCount = (
|
export const useMediaStreamTrackCount = (
|
||||||
stream: MediaStream
|
stream: MediaStream
|
||||||
): [number, number] => {
|
): [number, number] => {
|
||||||
|
const latestAudioTrackCount = stream ? stream.getAudioTracks().length : 0;
|
||||||
|
const latestVideoTrackCount = stream ? stream.getVideoTracks().length : 0;
|
||||||
|
|
||||||
const [audioTrackCount, setAudioTrackCount] = useState(
|
const [audioTrackCount, setAudioTrackCount] = useState(
|
||||||
stream.getAudioTracks().length
|
stream ? stream.getAudioTracks().length : 0
|
||||||
);
|
);
|
||||||
const [videoTrackCount, setVideoTrackCount] = useState(
|
const [videoTrackCount, setVideoTrackCount] = useState(
|
||||||
stream.getVideoTracks().length
|
stream ? stream.getVideoTracks().length : 0
|
||||||
);
|
);
|
||||||
|
|
||||||
const tracksChanged = useCallback(() => {
|
const tracksChanged = useCallback(() => {
|
||||||
setAudioTrackCount(stream.getAudioTracks().length);
|
setAudioTrackCount(stream ? stream.getAudioTracks().length : 0);
|
||||||
setVideoTrackCount(stream.getVideoTracks().length);
|
setVideoTrackCount(stream ? stream.getVideoTracks().length : 0);
|
||||||
}, [stream]);
|
}, [stream]);
|
||||||
|
|
||||||
useEventTarget(stream, "addtrack", tracksChanged);
|
useEventTarget(stream, "addtrack", tracksChanged);
|
||||||
useEventTarget(stream, "removetrack", tracksChanged);
|
useEventTarget(stream, "removetrack", tracksChanged);
|
||||||
|
|
||||||
|
if (
|
||||||
|
latestAudioTrackCount !== audioTrackCount ||
|
||||||
|
latestVideoTrackCount !== videoTrackCount
|
||||||
|
) {
|
||||||
|
tracksChanged();
|
||||||
|
}
|
||||||
|
|
||||||
return [audioTrackCount, videoTrackCount];
|
return [audioTrackCount, videoTrackCount];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue