2020-10-29 17:56:24 +00:00
|
|
|
/*
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2024 New Vector Ltd.
|
|
|
|
|
Copyright 2021, 2022 Šimon Brandner <simon.bra.ag@gmail.com>
|
|
|
|
|
Copyright 2015, 2016 , 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
|
2020-10-29 17:56:24 +00:00
|
|
|
|
2025-01-06 11:18:54 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-09-09 14:57:16 +01:00
|
|
|
Please see LICENSE files in the repository root for full details.
|
2020-10-29 17:56:24 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import classnames from "classnames";
|
2025-02-05 13:25:06 +00:00
|
|
|
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
2021-07-27 16:25:38 +02:00
|
|
|
import React from "react";
|
2025-02-05 13:25:06 +00:00
|
|
|
import { type CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
2021-03-07 08:13:35 +01:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2021-10-22 17:23:32 -05:00
|
|
|
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
|
2025-12-04 13:04:13 +00:00
|
|
|
import { MicOffSolidIcon, MicOnSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
2021-10-22 17:23:32 -05:00
|
|
|
|
|
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2023-02-27 09:34:02 +00:00
|
|
|
import RoomAvatar from "../avatars/RoomAvatar";
|
2026-07-07 13:29:54 +01:00
|
|
|
import { SDKContext } from "../../../contexts/SDKContext.ts";
|
2020-10-29 17:56:24 +00:00
|
|
|
|
|
|
|
|
interface IProps {
|
2021-07-01 23:23:03 +01:00
|
|
|
call: MatrixCall;
|
2020-10-29 17:56:24 +00:00
|
|
|
|
2021-07-01 23:23:03 +01:00
|
|
|
feed: CallFeed;
|
2021-03-07 08:13:35 +01:00
|
|
|
|
2021-04-27 11:59:26 +02:00
|
|
|
// Whether this call view is for picture-in-picture mode
|
2021-03-07 08:13:35 +01:00
|
|
|
// otherwise, it's the larger call view when viewing the room the call is in.
|
|
|
|
|
// This is sort of a proxy for a number of things but we currently have no
|
|
|
|
|
// need to control those things separately, so this is simpler.
|
|
|
|
|
pipMode?: boolean;
|
2020-10-29 17:56:24 +00:00
|
|
|
|
|
|
|
|
// a callback which is called when the video element is resized
|
|
|
|
|
// due to a change in video metadata
|
2021-07-07 11:00:42 +02:00
|
|
|
onResize?: (e: Event) => void;
|
2021-05-07 21:34:56 +02:00
|
|
|
|
2022-05-04 17:16:38 +02:00
|
|
|
primary?: boolean;
|
|
|
|
|
secondary?: boolean;
|
2020-10-29 17:56:24 +00:00
|
|
|
}
|
|
|
|
|
|
2021-03-07 08:13:35 +01:00
|
|
|
interface IState {
|
2021-04-04 08:50:25 +02:00
|
|
|
audioMuted: boolean;
|
|
|
|
|
videoMuted: boolean;
|
2021-03-07 08:13:35 +01:00
|
|
|
}
|
|
|
|
|
|
2021-07-28 15:11:31 +02:00
|
|
|
export default class VideoFeed extends React.PureComponent<IProps, IState> {
|
2026-07-07 13:29:54 +01:00
|
|
|
public static contextType = SDKContext;
|
|
|
|
|
declare public context: React.ContextType<typeof SDKContext>;
|
|
|
|
|
|
2023-05-05 09:11:14 +01:00
|
|
|
private element?: HTMLVideoElement;
|
2020-10-29 17:56:24 +00:00
|
|
|
|
2022-12-16 12:29:59 +00:00
|
|
|
public constructor(props: IProps) {
|
2021-03-07 08:13:35 +01:00
|
|
|
super(props);
|
|
|
|
|
|
|
|
|
|
this.state = {
|
2021-04-04 08:50:25 +02:00
|
|
|
audioMuted: this.props.feed.isAudioMuted(),
|
|
|
|
|
videoMuted: this.props.feed.isVideoMuted(),
|
2021-03-07 08:13:35 +01:00
|
|
|
};
|
2020-12-04 19:41:48 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public componentDidMount(): void {
|
2021-06-12 09:19:45 +02:00
|
|
|
this.updateFeed(null, this.props.feed);
|
2021-04-27 12:02:41 +02:00
|
|
|
this.playMedia();
|
2021-04-12 16:19:05 +02:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public componentWillUnmount(): void {
|
2021-06-12 09:19:45 +02:00
|
|
|
this.updateFeed(this.props.feed, null);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
2021-06-12 09:19:45 +02:00
|
|
|
this.updateFeed(prevProps.feed, this.props.feed);
|
2021-07-29 08:45:32 +02:00
|
|
|
// If the mutes state has changed, we try to playMedia()
|
2021-07-29 15:31:25 +02:00
|
|
|
if (prevState.videoMuted !== this.state.videoMuted || prevProps.feed.stream !== this.props.feed.stream) {
|
|
|
|
|
this.playMedia();
|
|
|
|
|
}
|
2021-06-12 09:19:45 +02:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public static getDerivedStateFromProps(props: IProps): IState {
|
2021-06-12 12:16:52 +02:00
|
|
|
return {
|
|
|
|
|
audioMuted: props.feed.isAudioMuted(),
|
|
|
|
|
videoMuted: props.feed.isVideoMuted(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-27 16:25:38 +02:00
|
|
|
private setElementRef = (element: HTMLVideoElement): void => {
|
2021-07-27 18:05:50 +02:00
|
|
|
if (!element) {
|
|
|
|
|
this.element?.removeEventListener("resize", this.onResize);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-07-27 16:25:38 +02:00
|
|
|
|
|
|
|
|
this.element = element;
|
|
|
|
|
element.addEventListener("resize", this.onResize);
|
|
|
|
|
};
|
|
|
|
|
|
2023-02-16 17:21:44 +00:00
|
|
|
private updateFeed(oldFeed: CallFeed | null, newFeed: CallFeed | null): void {
|
2021-06-12 09:19:45 +02:00
|
|
|
if (oldFeed === newFeed) return;
|
|
|
|
|
|
|
|
|
|
if (oldFeed) {
|
|
|
|
|
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
2021-07-27 14:31:42 +02:00
|
|
|
this.props.feed.removeListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged);
|
2021-08-19 11:05:08 +02:00
|
|
|
if (this.props.feed.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
|
|
|
|
this.props.feed.measureVolumeActivity(false);
|
|
|
|
|
}
|
2021-06-12 09:19:45 +02:00
|
|
|
this.stopMedia();
|
|
|
|
|
}
|
|
|
|
|
if (newFeed) {
|
|
|
|
|
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
2021-07-27 14:31:42 +02:00
|
|
|
this.props.feed.addListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged);
|
2021-08-19 11:05:08 +02:00
|
|
|
if (this.props.feed.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
|
|
|
|
this.props.feed.measureVolumeActivity(true);
|
|
|
|
|
}
|
2021-06-12 09:19:45 +02:00
|
|
|
this.playMedia();
|
|
|
|
|
}
|
2021-04-23 19:41:55 +02:00
|
|
|
}
|
2021-03-10 08:31:01 +01:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private async playMedia(): Promise<void> {
|
2021-07-27 16:25:38 +02:00
|
|
|
const element = this.element;
|
2021-04-27 12:02:41 +02:00
|
|
|
if (!element) return;
|
|
|
|
|
// We play audio in AudioFeed, not here
|
|
|
|
|
element.muted = true;
|
2021-04-23 19:41:55 +02:00
|
|
|
element.srcObject = this.props.feed.stream;
|
|
|
|
|
element.autoplay = true;
|
2021-03-10 08:31:01 +01:00
|
|
|
try {
|
|
|
|
|
// A note on calling methods on media elements:
|
|
|
|
|
// We used to have queues per media element to serialise all calls on those elements.
|
|
|
|
|
// The reason given for this was that load() and play() were racing. However, we now
|
|
|
|
|
// never call load() explicitly so this seems unnecessary. However, serialising every
|
|
|
|
|
// operation was causing bugs where video would not resume because some play command
|
|
|
|
|
// had got stuck and all media operations were queued up behind it. If necessary, we
|
|
|
|
|
// should serialise the ones that need to be serialised but then be able to interrupt
|
|
|
|
|
// them with another load() which will cancel the pending one, but since we don't call
|
|
|
|
|
// load() explicitly, it shouldn't be a problem. - Dave
|
2021-08-15 13:33:49 +02:00
|
|
|
await element.play();
|
2021-03-10 08:31:01 +01:00
|
|
|
} catch (e) {
|
2022-03-15 13:52:22 +00:00
|
|
|
logger.info(
|
|
|
|
|
`Failed to play media element with feed for userId ` +
|
|
|
|
|
`${this.props.feed.userId} with purpose ${this.props.feed.purpose}`,
|
|
|
|
|
e,
|
|
|
|
|
);
|
2020-10-29 17:56:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private stopMedia(): void {
|
2021-07-27 16:25:38 +02:00
|
|
|
const element = this.element;
|
2021-04-27 12:02:41 +02:00
|
|
|
if (!element) return;
|
|
|
|
|
|
2021-04-23 19:41:55 +02:00
|
|
|
element.pause();
|
2023-04-21 11:50:42 +01:00
|
|
|
element.removeAttribute("src");
|
2021-03-10 08:31:01 +01:00
|
|
|
|
|
|
|
|
// As per comment in componentDidMount, setting the sink ID back to the
|
|
|
|
|
// default once the call is over makes setSinkId work reliably. - Dave
|
|
|
|
|
// Since we are not using the same element anymore, the above doesn't
|
|
|
|
|
// seem to be necessary - Šimon
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onNewStream = (): void => {
|
2021-04-04 08:50:25 +02:00
|
|
|
this.setState({
|
|
|
|
|
audioMuted: this.props.feed.isAudioMuted(),
|
|
|
|
|
videoMuted: this.props.feed.isVideoMuted(),
|
|
|
|
|
});
|
2022-05-04 16:41:56 +02:00
|
|
|
this.playMedia();
|
2021-04-19 07:42:32 +02:00
|
|
|
};
|
2020-12-04 19:41:48 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onMuteStateChanged = (): void => {
|
2021-07-29 08:45:32 +02:00
|
|
|
this.setState({
|
2021-07-27 14:31:42 +02:00
|
|
|
audioMuted: this.props.feed.isAudioMuted(),
|
|
|
|
|
videoMuted: this.props.feed.isVideoMuted(),
|
|
|
|
|
});
|
2021-04-19 07:42:32 +02:00
|
|
|
};
|
2020-12-04 19:41:48 +00:00
|
|
|
|
2023-02-13 11:39:16 +00:00
|
|
|
private onResize = (e: Event): void => {
|
2021-03-07 08:13:35 +01:00
|
|
|
if (this.props.onResize && !this.props.feed.isLocal()) {
|
2020-10-29 17:56:24 +00:00
|
|
|
this.props.onResize(e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-02-13 17:01:43 +00:00
|
|
|
public render(): React.ReactNode {
|
2022-05-04 17:16:38 +02:00
|
|
|
const { pipMode, primary, secondary, feed } = this.props;
|
2021-07-29 15:05:26 +02:00
|
|
|
|
|
|
|
|
const wrapperClasses = classnames("mx_VideoFeed", {
|
2022-05-04 17:16:38 +02:00
|
|
|
mx_VideoFeed_primary: primary,
|
|
|
|
|
mx_VideoFeed_secondary: secondary,
|
2021-04-04 08:50:25 +02:00
|
|
|
mx_VideoFeed_voice: this.state.videoMuted,
|
2021-07-29 15:05:26 +02:00
|
|
|
});
|
2020-10-29 17:56:24 +00:00
|
|
|
|
2021-07-29 15:05:26 +02:00
|
|
|
let micIcon;
|
2024-07-29 17:46:10 +02:00
|
|
|
if (feed.purpose !== SDPStreamMetadataPurpose.Screenshare && !pipMode) {
|
2025-12-04 13:04:13 +00:00
|
|
|
micIcon = (
|
|
|
|
|
<div className="mx_VideoFeed_mic">
|
|
|
|
|
{this.state.audioMuted ? <MicOffSolidIcon /> : <MicOnSolidIcon />}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2021-07-29 15:05:26 +02:00
|
|
|
}
|
2021-07-21 21:27:07 +02:00
|
|
|
|
2021-07-29 15:05:26 +02:00
|
|
|
let content;
|
2021-04-04 08:50:25 +02:00
|
|
|
if (this.state.videoMuted) {
|
2026-07-07 13:29:54 +01:00
|
|
|
const callRoomId = this.context.legacyCallHandler.roomIdForCall(this.props.call);
|
|
|
|
|
const callRoom = (callRoomId ? this.context.client?.getRoom(callRoomId) : undefined) ?? undefined;
|
2021-07-29 15:05:26 +02:00
|
|
|
|
2021-06-12 10:48:32 +02:00
|
|
|
let avatarSize;
|
2023-08-24 04:48:35 +01:00
|
|
|
if (pipMode && primary) avatarSize = "76px";
|
|
|
|
|
else if (pipMode && !primary) avatarSize = "16px";
|
|
|
|
|
else if (!pipMode && primary) avatarSize = "160px";
|
2021-07-21 21:27:07 +02:00
|
|
|
else; // TBD
|
2021-03-07 08:13:35 +01:00
|
|
|
|
2023-08-24 04:48:35 +01:00
|
|
|
content = <RoomAvatar room={callRoom} size={avatarSize} />;
|
2021-03-07 08:13:35 +01:00
|
|
|
} else {
|
2021-07-29 15:05:26 +02:00
|
|
|
const videoClasses = classnames("mx_VideoFeed_video", {
|
|
|
|
|
mx_VideoFeed_video_mirror:
|
|
|
|
|
this.props.feed.isLocal() &&
|
2021-08-03 16:06:12 +02:00
|
|
|
this.props.feed.purpose === SDPStreamMetadataPurpose.Usermedia &&
|
2021-07-29 15:05:26 +02:00
|
|
|
SettingsStore.getValue("VideoView.flipVideoHorizontally"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
content = <video className={videoClasses} ref={this.setElementRef} />;
|
2021-03-07 08:13:35 +01:00
|
|
|
}
|
2021-07-29 15:05:26 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={wrapperClasses}>
|
|
|
|
|
{micIcon}
|
|
|
|
|
{content}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2020-10-29 17:56:24 +00:00
|
|
|
}
|
|
|
|
|
}
|