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:
commit
b92acd4822
8 changed files with 102 additions and 12 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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}}",
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue