Merge pull request #752 from vector-im/dbkr/waiting_for_media

Add a 'waiting for video' state to media tiles
This commit is contained in:
David Baker 2022-11-16 16:41:27 +00:00 committed by GitHub
commit b92acd4822
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 102 additions and 12 deletions

View file

@ -45,7 +45,7 @@
"i18next": "^21.10.0", "i18next": "^21.10.0",
"i18next-browser-languagedetector": "^6.1.8", "i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4", "i18next-http-backend": "^1.4.4",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#c6ee258789c9e01d328b5d9158b5b372e3a0da82", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#3f1c3392d45b0fc054c3788cc6c043cd5b4fb730",
"matrix-widget-api": "^1.0.0", "matrix-widget-api": "^1.0.0",
"mermaid": "^8.13.8", "mermaid": "^8.13.8",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",

View file

@ -3,6 +3,7 @@
"{{count}} people connected|other": "{{count}} people connected", "{{count}} people connected|other": "{{count}} people connected",
"{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended", "{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended",
"{{name}} (Connecting...)": "{{name}} (Connecting...)", "{{name}} (Connecting...)": "{{name}} (Connecting...)",
"{{name}} (Waiting for video...)": "{{name}} (Waiting for video...)",
"{{name}} is presenting": "{{name}} is presenting", "{{name}} is presenting": "{{name}} is presenting",
"{{name}} is talking…": "{{name}} is talking…", "{{name}} is talking…": "{{name}} is talking…",
"{{names}}, {{name}}": "{{names}}, {{name}}", "{{names}}, {{name}}": "{{names}}, {{name}}",

View file

@ -79,6 +79,7 @@ export function GroupCallView({
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
participants, participants,
calls,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
} = useGroupCall(groupCall); } = useGroupCall(groupCall);
@ -235,6 +236,7 @@ export function GroupCallView({
roomName={groupCall.room.name} roomName={groupCall.room.name}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
participants={participants} participants={participants}
calls={calls}
microphoneMuted={microphoneMuted} microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted} localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted} toggleLocalVideoMuted={toggleLocalVideoMuted}

View file

@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useEffect, useCallback, useMemo, useRef } from "react"; import React, {
useEffect,
useCallback,
useMemo,
useRef,
useState,
} from "react";
import { usePreventScroll } from "@react-aria/overlays"; import { usePreventScroll } from "@react-aria/overlays";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer"; import { ResizeObserver } from "@juggle/resize-observer";
@ -25,6 +31,11 @@ import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import {
CallEvent,
CallState,
MatrixCall,
} from "matrix-js-sdk/src/webrtc/call";
import type { IWidgetApiRequest } from "matrix-widget-api"; import type { IWidgetApiRequest } from "matrix-widget-api";
import styles from "./InCallView.module.css"; import styles from "./InCallView.module.css";
@ -73,6 +84,7 @@ interface Props {
client: MatrixClient; client: MatrixClient;
groupCall: GroupCall; groupCall: GroupCall;
participants: RoomMember[]; participants: RoomMember[];
calls: MatrixCall[];
roomName: string; roomName: string;
avatarUrl: string; avatarUrl: string;
microphoneMuted: boolean; microphoneMuted: boolean;
@ -90,6 +102,12 @@ interface Props {
hideHeader: boolean; hideHeader: boolean;
} }
export enum ConnectionState {
EstablishingCall = "establishing call", // call hasn't been established yet
WaitMedia = "wait_media", // call is set up, waiting for ICE to connect
Connected = "connected", // media is flowing
}
// Represents something that should get a tile on the layout, // Represents something that should get a tile on the layout,
// ie. a user's video feed or a screen share feed. // ie. a user's video feed or a screen share feed.
export interface TileDescriptor { export interface TileDescriptor {
@ -99,12 +117,14 @@ export interface TileDescriptor {
presenter: boolean; presenter: boolean;
callFeed?: CallFeed; callFeed?: CallFeed;
isLocal?: boolean; isLocal?: boolean;
connectionState: ConnectionState;
} }
export function InCallView({ export function InCallView({
client, client,
groupCall, groupCall,
participants, participants,
calls,
roomName, roomName,
avatarUrl, avatarUrl,
microphoneMuted, microphoneMuted,
@ -154,6 +174,50 @@ export function InCallView({
const { hideScreensharing } = useUrlParams(); const { hideScreensharing } = useUrlParams();
const makeConnectionStatesMap = useCallback(() => {
const newConnStates = new Map<string, ConnectionState>();
for (const participant of participants) {
const userCall = groupCall.getCallByUserId(participant.userId);
const feed = userMediaFeeds.find((f) => f.userId === participant.userId);
let connectionState = ConnectionState.EstablishingCall;
if (feed && feed.isLocal()) {
connectionState = ConnectionState.Connected;
} else if (userCall) {
if (userCall.state === CallState.Connected) {
connectionState = ConnectionState.Connected;
} else if (userCall.state === CallState.Connecting) {
connectionState = ConnectionState.WaitMedia;
}
}
newConnStates.set(participant.userId, connectionState);
}
return newConnStates;
}, [groupCall, participants, userMediaFeeds]);
const [connStates, setConnStates] = useState(
new Map<string, ConnectionState>()
);
const updateConnectionStates = useCallback(() => {
setConnStates(makeConnectionStatesMap());
}, [setConnStates, makeConnectionStatesMap]);
useEffect(() => {
for (const call of calls) {
call.on(CallEvent.State, updateConnectionStates);
}
return () => {
for (const call of calls) {
call.off(CallEvent.State, updateConnectionStates);
}
};
}, [calls, updateConnectionStates]);
useEffect(() => {
updateConnectionStates();
}, [participants, updateConnectionStates]);
useEffect(() => { useEffect(() => {
widget?.api.transport.send( widget?.api.transport.send(
layout === "freedom" layout === "freedom"
@ -208,6 +272,7 @@ export function InCallView({
focused: screenshareFeeds.length === 0 && p.userId === activeSpeaker, focused: screenshareFeeds.length === 0 && p.userId === activeSpeaker,
isLocal: p.userId === client.getUserId(), isLocal: p.userId === client.getUserId(),
presenter: false, presenter: false,
connectionState: connStates.get(p.userId),
}); });
} }
@ -231,11 +296,19 @@ export function InCallView({
focused: true, focused: true,
isLocal: screenshareFeed.isLocal(), isLocal: screenshareFeed.isLocal(),
presenter: false, presenter: false,
connectionState: connStates.get(screenshareFeed.userId),
}); });
} }
return tileDescriptors; return tileDescriptors;
}, [client, participants, userMediaFeeds, activeSpeaker, screenshareFeeds]); }, [
client,
participants,
userMediaFeeds,
activeSpeaker,
screenshareFeeds,
connStates,
]);
// 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

View file

@ -21,7 +21,7 @@ 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 { TileDescriptor } from "../room/InCallView"; import { ConnectionState, TileDescriptor } from "../room/InCallView";
export default { export default {
title: "VideoGrid", title: "VideoGrid",
@ -41,6 +41,7 @@ export const ParticipantsTest = () => {
member: new RoomMember("!fake:room.id", `@user${i}:fake.dummy`), member: new RoomMember("!fake:room.id", `@user${i}:fake.dummy`),
focused: false, focused: false,
presenter: false, presenter: false,
connectionState: ConnectionState.Connected,
})), })),
[participantCount] [participantCount]
); );
@ -79,7 +80,7 @@ export const ParticipantsTest = () => {
key={item.id} key={item.id}
name={`User ${item.id}`} name={`User ${item.id}`}
disableSpeakingIndicator={items.length < 3} disableSpeakingIndicator={items.length < 3}
hasFeed={true} connectionState={ConnectionState.Connected}
{...rest} {...rest}
/> />
)} )}

View file

@ -23,10 +23,11 @@ import styles from "./VideoTile.module.css";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg"; import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg"; import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
import { AudioButton, FullscreenButton } from "../button/Button"; import { AudioButton, FullscreenButton } from "../button/Button";
import { ConnectionState } from "../room/InCallView";
interface Props { interface Props {
name: string; name: string;
hasFeed: Boolean; connectionState: ConnectionState;
speaking?: boolean; speaking?: boolean;
audioMuted?: boolean; audioMuted?: boolean;
videoMuted?: boolean; videoMuted?: boolean;
@ -48,7 +49,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
( (
{ {
name, name,
hasFeed, connectionState,
speaking, speaking,
audioMuted, audioMuted,
videoMuted, videoMuted,
@ -72,7 +73,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
const { t } = useTranslation(); const { t } = useTranslation();
const toolbarButtons: JSX.Element[] = []; const toolbarButtons: JSX.Element[] = [];
if (hasFeed && !isLocal) { if (connectionState == ConnectionState.Connected && !isLocal) {
toolbarButtons.push( toolbarButtons.push(
<AudioButton <AudioButton
key="localVolume" key="localVolume"
@ -94,7 +95,19 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
} }
} }
const caption = hasFeed ? name : t("{{name}} (Connecting...)", { name }); let caption: string;
switch (connectionState) {
case ConnectionState.EstablishingCall:
caption = t("{{name}} (Connecting...)", { name });
break;
case ConnectionState.WaitMedia:
// not strictly true, but probably easier to understand than, "Waiting for media"
caption = t("{{name}} (Waiting for video...)", { name });
break;
case ConnectionState.Connected:
caption = name;
break;
}
return ( return (
<animated.div <animated.div

View file

@ -98,7 +98,7 @@ export function VideoTileContainer({
videoMuted={videoMuted} videoMuted={videoMuted}
screenshare={purpose === SDPStreamMetadataPurpose.Screenshare} screenshare={purpose === SDPStreamMetadataPurpose.Screenshare}
name={rawDisplayName} name={rawDisplayName}
hasFeed={Boolean(item.callFeed)} connectionState={item.connectionState}
ref={tileRef} ref={tileRef}
mediaRef={mediaRef} mediaRef={mediaRef}
avatar={getAvatar && getAvatar(item.member, width, height)} avatar={getAvatar && getAvatar(item.member, width, height)}

View file

@ -10196,9 +10196,9 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#c6ee258789c9e01d328b5d9158b5b372e3a0da82": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#3f1c3392d45b0fc054c3788cc6c043cd5b4fb730":
version "21.1.0" version "21.1.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c6ee258789c9e01d328b5d9158b5b372e3a0da82" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3f1c3392d45b0fc054c3788cc6c043cd5b4fb730"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@types/sdp-transform" "^2.4.5" "@types/sdp-transform" "^2.4.5"