Make CallView use CallFeed

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
pull/21833/head
Šimon Brandner 2021-03-07 08:13:35 +01:00
parent 62e9d7f46b
commit bb13dc49a6
No known key found for this signature in database
GPG Key ID: 9760693FDD98A790
5 changed files with 185 additions and 68 deletions

View File

@ -112,6 +112,7 @@ limitations under the License.
z-index: 30; z-index: 30;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
display: flex;
} }
.mx_CallView_video_hold { .mx_CallView_video_hold {

View File

@ -14,11 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_VideoFeed_remote { .mx_VideoFeed_voice {
width: 100%; // We don't want to collide with the call controls that have 52px of height
height: 100%; padding-bottom: 52px;
background-color: $inverted-bg-color;
}
.mx_VideoFeed_video {
background-color: #000; background-color: #000;
z-index: 50; }
.mx_VideoFeed_remote {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
} }
.mx_VideoFeed_local { .mx_VideoFeed_local {

View File

@ -621,7 +621,6 @@ export default class CallHandler {
private async placeCall( private async placeCall(
roomId: string, type: PlaceCallType, roomId: string, type: PlaceCallType,
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
) { ) {
Analytics.trackEvent('voip', 'placeCall', 'type', type); Analytics.trackEvent('voip', 'placeCall', 'type', type);
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false); CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
@ -643,10 +642,7 @@ export default class CallHandler {
if (type === PlaceCallType.Voice) { if (type === PlaceCallType.Voice) {
call.placeVoiceCall(); call.placeVoiceCall();
} else if (type === 'video') { } else if (type === 'video') {
call.placeVideoCall( call.placeVideoCall();
remoteElement,
localElement,
);
} else if (type === PlaceCallType.ScreenSharing) { } else if (type === PlaceCallType.ScreenSharing) {
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
if (screenCapErrorString) { if (screenCapErrorString) {
@ -660,8 +656,6 @@ export default class CallHandler {
} }
call.placeScreenSharingCall( call.placeScreenSharingCall(
remoteElement,
localElement,
async () : Promise<DesktopCapturerSource> => { async () : Promise<DesktopCapturerSource> => {
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker); const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished; const [source] = await finished;
@ -715,14 +709,12 @@ export default class CallHandler {
} else if (members.length === 2) { } else if (members.length === 2) {
console.info(`Place ${payload.type} call in ${payload.room_id}`); console.info(`Place ${payload.type} call in ${payload.room_id}`);
this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element); this.placeCall(payload.room_id, payload.type);
} else { // > 2 } else { // > 2
dis.dispatch({ dis.dispatch({
action: "place_conference_call", action: "place_conference_call",
room_id: payload.room_id, room_id: payload.room_id,
type: payload.type, type: payload.type,
remote_element: payload.remote_element,
local_element: payload.local_element,
}); });
} }
} }

View File

@ -20,7 +20,7 @@ import dis from '../../../dispatcher/dispatcher';
import CallHandler from '../../../CallHandler'; import CallHandler from '../../../CallHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import VideoFeed, { VideoFeedType } from "./VideoFeed"; import VideoFeed from './VideoFeed';
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call'; import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call';
import classNames from 'classnames'; import classNames from 'classnames';
@ -30,6 +30,7 @@ import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} f
import CallContextMenu from '../context_menus/CallContextMenu'; import CallContextMenu from '../context_menus/CallContextMenu';
import { avatarUrlForMember } from '../../../Avatar'; import { avatarUrlForMember } from '../../../Avatar';
import DialpadContextMenu from '../context_menus/DialpadContextMenu'; import DialpadContextMenu from '../context_menus/DialpadContextMenu';
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
interface IProps { interface IProps {
// The call for us to display // The call for us to display
@ -58,6 +59,7 @@ interface IState {
controlsVisible: boolean, controlsVisible: boolean,
showMoreMenu: boolean, showMoreMenu: boolean,
showDialpad: boolean, showDialpad: boolean,
feeds: CallFeed[],
} }
function getFullScreenElement() { function getFullScreenElement() {
@ -112,6 +114,7 @@ export default class CallView extends React.Component<IProps, IState> {
controlsVisible: true, controlsVisible: true,
showMoreMenu: false, showMoreMenu: false,
showDialpad: false, showDialpad: false,
feeds: this.props.call.getFeeds(),
} }
this.updateCallListeners(null, this.props.call); this.updateCallListeners(null, this.props.call);
@ -169,11 +172,13 @@ export default class CallView extends React.Component<IProps, IState> {
oldCall.removeListener(CallEvent.State, this.onCallState); oldCall.removeListener(CallEvent.State, this.onCallState);
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold); oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold); oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
oldCall.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged);
} }
if (newCall) { if (newCall) {
newCall.on(CallEvent.State, this.onCallState); newCall.on(CallEvent.State, this.onCallState);
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold); newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold); newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
newCall.on(CallEvent.FeedsChanged, this.onFeedsChanged);
} }
} }
@ -183,6 +188,10 @@ export default class CallView extends React.Component<IProps, IState> {
}); });
}; };
private onFeedsChanged = (newFeeds: Array<CallFeed>) => {
this.setState({feeds: newFeeds});
}
private onCallLocalHoldUnhold = () => { private onCallLocalHoldUnhold = () => {
this.setState({ this.setState({
isLocalOnHold: this.props.call.isLocalOnHold(), isLocalOnHold: this.props.call.isLocalOnHold(),
@ -486,44 +495,71 @@ export default class CallView extends React.Component<IProps, IState> {
}); });
} }
if (this.props.call.type === CallType.Video) { const avatarSize = this.props.pipMode ? 76 : 160;
let localVideoFeed = null; if (isOnHold) {
let onHoldContent = null; if (this.props.call.type === CallType.Video) {
let onHoldBackground = null; const containerClasses = classNames({
const backgroundStyle: CSSProperties = {}; mx_CallView_video: true,
const containerClasses = classNames({ mx_CallView_video_hold: isOnHold,
mx_CallView_video: true, });
mx_CallView_video_hold: isOnHold, let onHoldContent = null;
}); let onHoldBackground = null;
if (isOnHold) { const backgroundStyle: CSSProperties = {};
onHoldContent = <div className="mx_CallView_video_holdContent"> onHoldContent = (
{onHoldText} <div className="mx_CallView_video_holdContent">
</div>; {onHoldText}
</div>
);
const backgroundAvatarUrl = avatarUrlForMember( const backgroundAvatarUrl = avatarUrlForMember(
// is it worth getting the size of the div to pass here? // is it worth getting the size of the div to pass here?
this.props.call.getOpponentMember(), 1024, 1024, 'crop', this.props.call.getOpponentMember(), 1024, 1024, 'crop',
); );
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')'; backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />; onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
}
if (!this.state.vidMuted) {
localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />;
}
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}> contentView = (
{onHoldBackground} <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize} /> {onHoldBackground}
{localVideoFeed} {onHoldContent}
{onHoldContent} {callControls}
{callControls} </div>
</div>; );
} else { } else {
const avatarSize = this.props.pipMode ? 76 : 160; const classes = classNames({
mx_CallView_voice: true,
mx_CallView_voice_hold: isOnHold,
});
contentView =(
<div className={classes} onMouseMove={this.onMouseMove}>
<div className="mx_CallView_voice_avatarsContainer">
<div
className="mx_CallView_voice_avatarContainer"
style={{width: avatarSize, height: avatarSize}}
>
<RoomAvatar
room={callRoom}
height={avatarSize}
width={avatarSize}
/>
</div>
</div>
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
{callControls}
</div>
);
}
} else if (this.props.call.noIncomingFeeds()) {
// Here we're reusing the css classes from voice on hold, because
// I am lazy. If this gets merged, the CallView might be subject
// to change anyway - I might take an axe to this file in order to
// try to get other things working
const classes = classNames({ const classes = classNames({
mx_CallView_voice: true, mx_CallView_voice: true,
mx_CallView_voice_hold: isOnHold,
}); });
// Saying "Connecting" here isn't really true, but the best thing
// I can come up with, but this might be subject to change as well
contentView = <div className={classes} onMouseMove={this.onMouseMove}> contentView = <div className={classes} onMouseMove={this.onMouseMove}>
<div className="mx_CallView_voice_avatarsContainer"> <div className="mx_CallView_voice_avatarsContainer">
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}> <div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
@ -534,7 +570,33 @@ export default class CallView extends React.Component<IProps, IState> {
/> />
</div> </div>
</div> </div>
<div className="mx_CallView_voice_holdText">{onHoldText}</div> <div className="mx_CallView_voice_holdText">{_t("Connecting")}</div>
{callControls}
</div>;
} else {
const containerClasses = classNames({
mx_CallView_video: true,
});
// TODO: Later the CallView should probably be reworked to support any
// number of feeds but now we can always expect there to be two feeds
const feeds = this.state.feeds.map((feed, i) => {
// Here we check to hide local audio feeds to achieve the same UI/UX
// as before. But once again this might be subject to change
if (feed.isAudioOnly() && feed.isLocal()) return;
return (
<VideoFeed
key={i}
feed={feed}
call={this.props.call}
pipMode={this.props.pipMode}
onResize={this.props.onResize}
/>
);
});
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
{feeds}
{callControls} {callControls}
</div>; </div>;
} }

View File

@ -18,50 +18,81 @@ import classnames from 'classnames';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
export enum VideoFeedType { import { MatrixClientPeg } from '../../../MatrixClientPeg';
Local, import { logger } from 'matrix-js-sdk/src/logger';
Remote, import MemberAvatar from "../avatars/MemberAvatar"
} import CallHandler from '../../../CallHandler';
interface IProps { interface IProps {
call: MatrixCall, call: MatrixCall,
type: VideoFeedType, 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;
// a callback which is called when the video element is resized // a callback which is called when the video element is resized
// due to a change in video metadata // due to a change in video metadata
onResize?: (e: Event) => void, onResize?: (e: Event) => void,
} }
export default class VideoFeed extends React.Component<IProps> { interface IState {
audioOnly: boolean;
}
export default class VideoFeed extends React.Component<IProps, IState> {
private vid = createRef<HTMLVideoElement>(); private vid = createRef<HTMLVideoElement>();
componentDidMount() { constructor(props: IProps) {
this.vid.current.addEventListener('resize', this.onResize); super(props);
this.setVideoElement();
this.state = {
audioOnly: this.props.feed.isAudioOnly(),
};
} }
componentDidUpdate(prevProps) { componentDidMount() {
if (this.props.call !== prevProps.call) { this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
this.setVideoElement(); if (!this.vid.current) return;
// 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
this.vid.current.srcObject = this.props.feed.stream;
this.vid.current.autoplay = true;
this.vid.current.muted = true;
try {
this.vid.current.play();
} catch (e) {
logger.info("Failed to play video element with feed", this.props.feed, e);
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
if (!this.vid.current) return;
this.vid.current.removeEventListener('resize', this.onResize); this.vid.current.removeEventListener('resize', this.onResize);
this.vid.current.pause();
this.vid.current.srcObject = null;
} }
private setVideoElement() { onNewStream = (newStream: MediaStream) => {
if (this.props.type === VideoFeedType.Local) { this.setState({ audioOnly: this.props.feed.isAudioOnly()});
this.props.call.setLocalVideoElement(this.vid.current); if (!this.vid.current) return;
} else { this.vid.current.srcObject = newStream;
this.props.call.setRemoteVideoElement(this.vid.current);
}
} }
onResize = (e) => { onResize = (e) => {
if (this.props.onResize) { if (this.props.onResize && !this.props.feed.isLocal()) {
this.props.onResize(e); this.props.onResize(e);
} }
}; };
@ -69,14 +100,35 @@ export default class VideoFeed extends React.Component<IProps> {
render() { render() {
const videoClasses = { const videoClasses = {
mx_VideoFeed: true, mx_VideoFeed: true,
mx_VideoFeed_local: this.props.type === VideoFeedType.Local, mx_VideoFeed_local: this.props.feed.isLocal(),
mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote, mx_VideoFeed_remote: !this.props.feed.isLocal(),
mx_VideoFeed_voice: this.state.audioOnly,
mx_VideoFeed_video: !this.state.audioOnly,
mx_VideoFeed_mirror: ( mx_VideoFeed_mirror: (
this.props.type === VideoFeedType.Local && this.props.feed.isLocal() &&
SettingsStore.getValue('VideoView.flipVideoHorizontally') SettingsStore.getValue('VideoView.flipVideoHorizontally')
), ),
}; };
return <video className={classnames(videoClasses)} ref={this.vid} />; if (this.state.audioOnly) {
const callRoomId = CallHandler.roomIdForCall(this.props.call);
const callRoom = MatrixClientPeg.get().getRoom(callRoomId);
const member = callRoom.getMember(this.props.feed.userId);
const avatarSize = this.props.pipMode ? 76 : 160;
return (
<div className={classnames(videoClasses)} >
<MemberAvatar
member={member}
height={avatarSize}
width={avatarSize}
/>
</div>
);
} else {
return (
<video className={classnames(videoClasses)} ref={this.vid} />
);
}
} }
} }