2020-10-29 20:56:24 +03:00
|
|
|
/*
|
2020-11-05 12:51:02 +03:00
|
|
|
Copyright 2015, 2016, 2019 The Matrix.org Foundation C.I.C.
|
2020-10-29 20:56:24 +03:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import classnames from 'classnames';
|
|
|
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|
|
|
import React, {createRef} from 'react';
|
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2021-03-07 10:13:35 +03:00
|
|
|
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
|
|
|
import { logger } from 'matrix-js-sdk/src/logger';
|
|
|
|
import MemberAvatar from "../avatars/MemberAvatar"
|
2021-03-10 10:31:01 +03:00
|
|
|
import CallMediaHandler from "../../../CallMediaHandler";
|
2021-03-09 06:20:07 +03:00
|
|
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
2020-10-29 20:56:24 +03:00
|
|
|
|
|
|
|
interface IProps {
|
|
|
|
call: MatrixCall,
|
|
|
|
|
2021-03-07 10:13:35 +03:00
|
|
|
feed: CallFeed,
|
|
|
|
|
|
|
|
// Whether this call view is for picture-in-pictue mode
|
|
|
|
// 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 20:56:24 +03:00
|
|
|
|
|
|
|
// a callback which is called when the video element is resized
|
|
|
|
// due to a change in video metadata
|
|
|
|
onResize?: (e: Event) => void,
|
|
|
|
}
|
|
|
|
|
2021-03-07 10:13:35 +03:00
|
|
|
interface IState {
|
2021-04-04 09:50:25 +03:00
|
|
|
audioMuted: boolean;
|
|
|
|
videoMuted: boolean;
|
2021-03-07 10:13:35 +03:00
|
|
|
}
|
|
|
|
|
2021-03-17 18:13:40 +03:00
|
|
|
|
2021-03-09 06:20:07 +03:00
|
|
|
@replaceableComponent("views.voip.VideoFeed")
|
2021-03-07 10:13:35 +03:00
|
|
|
export default class VideoFeed extends React.Component<IProps, IState> {
|
2021-03-10 10:31:01 +03:00
|
|
|
private video = createRef<HTMLVideoElement>();
|
|
|
|
private audio = createRef<HTMLAudioElement>();
|
2020-10-29 20:56:24 +03:00
|
|
|
|
2021-03-07 10:13:35 +03:00
|
|
|
constructor(props: IProps) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
2021-04-04 09:50:25 +03:00
|
|
|
audioMuted: this.props.feed.isAudioMuted(),
|
|
|
|
videoMuted: this.props.feed.isVideoMuted(),
|
2021-03-07 10:13:35 +03:00
|
|
|
};
|
2020-12-04 22:41:48 +03:00
|
|
|
}
|
|
|
|
|
2021-03-07 10:13:35 +03:00
|
|
|
componentDidMount() {
|
|
|
|
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
2021-03-10 10:31:01 +03:00
|
|
|
|
|
|
|
const audioOutput = CallMediaHandler.getAudioOutput();
|
|
|
|
const currentMedia = this.getCurrentMedia();
|
|
|
|
|
|
|
|
currentMedia.srcObject = this.props.feed.stream;
|
|
|
|
currentMedia.autoplay = true;
|
|
|
|
currentMedia.muted = false;
|
|
|
|
|
2021-03-07 10:13:35 +03:00
|
|
|
try {
|
2021-03-10 10:31:01 +03:00
|
|
|
if (audioOutput) {
|
|
|
|
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
|
|
|
// it fails.
|
|
|
|
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
|
|
|
|
// back to the default after the call is over - Dave
|
|
|
|
currentMedia.setSinkId(audioOutput);
|
|
|
|
}
|
2021-03-07 10:13:35 +03:00
|
|
|
} catch (e) {
|
2021-03-10 10:31:01 +03:00
|
|
|
console.error("Couldn't set requested audio output device: using default", e);
|
|
|
|
logger.warn("Couldn't set requested audio output device: using default", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
currentMedia.play()
|
|
|
|
} catch (e) {
|
|
|
|
logger.info("Failed to play media element with feed", this.props.feed, e);
|
2020-10-29 20:56:24 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2021-03-07 10:13:35 +03:00
|
|
|
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
2021-03-10 10:31:01 +03:00
|
|
|
this.video.current?.removeEventListener('resize', this.onResize);
|
|
|
|
|
|
|
|
const currentMedia = this.getCurrentMedia();
|
|
|
|
currentMedia.pause();
|
|
|
|
currentMedia.srcObject = null;
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
getCurrentMedia() {
|
|
|
|
return this.audio.current || this.video.current;
|
2020-10-29 20:56:24 +03:00
|
|
|
}
|
|
|
|
|
2021-03-07 10:13:35 +03:00
|
|
|
onNewStream = (newStream: MediaStream) => {
|
2021-04-04 09:50:25 +03:00
|
|
|
this.setState({
|
|
|
|
audioMuted: this.props.feed.isAudioMuted(),
|
|
|
|
videoMuted: this.props.feed.isVideoMuted(),
|
|
|
|
});
|
2021-03-10 10:31:01 +03:00
|
|
|
const currentMedia = this.getCurrentMedia();
|
|
|
|
currentMedia.srcObject = newStream;
|
2021-03-17 18:10:50 +03:00
|
|
|
currentMedia.play();
|
2020-12-04 22:41:48 +03:00
|
|
|
}
|
|
|
|
|
2020-10-29 20:56:24 +03:00
|
|
|
onResize = (e) => {
|
2021-03-07 10:13:35 +03:00
|
|
|
if (this.props.onResize && !this.props.feed.isLocal()) {
|
2020-10-29 20:56:24 +03:00
|
|
|
this.props.onResize(e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const videoClasses = {
|
|
|
|
mx_VideoFeed: true,
|
2021-03-07 10:13:35 +03:00
|
|
|
mx_VideoFeed_local: this.props.feed.isLocal(),
|
|
|
|
mx_VideoFeed_remote: !this.props.feed.isLocal(),
|
2021-04-04 09:50:25 +03:00
|
|
|
mx_VideoFeed_voice: this.state.videoMuted,
|
|
|
|
mx_VideoFeed_video: !this.state.videoMuted,
|
2020-10-29 20:56:24 +03:00
|
|
|
mx_VideoFeed_mirror: (
|
2021-03-07 10:13:35 +03:00
|
|
|
this.props.feed.isLocal() &&
|
2020-10-29 20:56:24 +03:00
|
|
|
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
|
|
|
),
|
|
|
|
};
|
|
|
|
|
2021-04-04 09:50:25 +03:00
|
|
|
if (this.state.videoMuted) {
|
2021-04-04 09:33:53 +03:00
|
|
|
const member = this.props.feed.getMember();
|
2021-03-07 10:13:35 +03:00
|
|
|
const avatarSize = this.props.pipMode ? 76 : 160;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={classnames(videoClasses)} >
|
|
|
|
<MemberAvatar
|
|
|
|
member={member}
|
|
|
|
height={avatarSize}
|
|
|
|
width={avatarSize}
|
|
|
|
/>
|
2021-03-10 10:31:01 +03:00
|
|
|
<audio ref={this.audio}></audio>
|
2021-03-07 10:13:35 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (
|
2021-03-10 10:31:01 +03:00
|
|
|
<video className={classnames(videoClasses)} ref={this.video} />
|
2021-03-07 10:13:35 +03:00
|
|
|
);
|
|
|
|
}
|
2020-10-29 20:56:24 +03:00
|
|
|
}
|
|
|
|
}
|