Merge pull request #5366 from matrix-org/dbkr/call_hold
Implement call hold
This commit is contained in:
commit
50bce642d5
14 changed files with 293 additions and 292 deletions
|
@ -229,4 +229,4 @@
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_VideoView.scss";
|
@import "./views/voip/_VideoFeed.scss";
|
||||||
|
|
|
@ -33,11 +33,11 @@ limitations under the License.
|
||||||
pointer-events: initial; // restore pointer events so the user can leave/interact
|
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.mx_VideoView {
|
.mx_CallView_video {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed {
|
.mx_VideoFeed_local {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,3 +92,10 @@ limitations under the License.
|
||||||
background-color: $primary-fg-color;
|
background-color: $primary-fg-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_video {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,23 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VideoView {
|
.mx_VideoFeed video {
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VideoView video {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_remoteVideoFeed {
|
.mx_VideoFeed_remote {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed {
|
.mx_VideoFeed_local {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
height: 25%;
|
height: 25%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -39,11 +33,11 @@ limitations under the License.
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed video {
|
.mx_VideoFeed_local video {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video {
|
.mx_VideoFeed_mirror video {
|
||||||
transform: scale(-1, 1);
|
transform: scale(-1, 1);
|
||||||
}
|
}
|
14
src/@types/global.d.ts
vendored
14
src/@types/global.d.ts
vendored
|
@ -69,6 +69,13 @@ declare global {
|
||||||
interface Document {
|
interface Document {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
|
||||||
hasStorageAccess?: () => Promise<boolean>;
|
hasStorageAccess?: () => Promise<boolean>;
|
||||||
|
|
||||||
|
// Safari & IE11 only have this prefixed: we used prefixed versions
|
||||||
|
// previously so let's continue to support them for now
|
||||||
|
webkitExitFullscreen(): Promise<void>;
|
||||||
|
msExitFullscreen(): Promise<void>;
|
||||||
|
readonly webkitFullscreenElement: Element | null;
|
||||||
|
readonly msFullscreenElement: Element | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Navigator {
|
interface Navigator {
|
||||||
|
@ -99,6 +106,13 @@ declare global {
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Element {
|
||||||
|
// Safari & IE11 only have this prefixed: we used prefixed versions
|
||||||
|
// previously so let's continue to support them for now
|
||||||
|
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
|
||||||
|
msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
interface Error {
|
interface Error {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
|
|
|
@ -59,8 +59,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import Matrix from 'matrix-js-sdk';
|
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||||
|
@ -77,7 +76,7 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import WidgetStore from "./stores/WidgetStore";
|
import WidgetStore from "./stores/WidgetStore";
|
||||||
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||||
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/lib/webrtc/call";
|
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
|
|
||||||
|
@ -98,6 +97,21 @@ export enum PlaceCallType {
|
||||||
ScreenSharing = 'screensharing',
|
ScreenSharing = 'screensharing',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRemoteAudioElement(): HTMLAudioElement {
|
||||||
|
// this needs to be somewhere at the top of the DOM which
|
||||||
|
// always exists to avoid audio interruptions.
|
||||||
|
// Might as well just use DOM.
|
||||||
|
const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement;
|
||||||
|
if (!remoteAudioElement) {
|
||||||
|
console.error(
|
||||||
|
"Failed to find remoteAudio element - cannot play audio!" +
|
||||||
|
"You need to add an <audio/> to the DOM.",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return remoteAudioElement;
|
||||||
|
}
|
||||||
|
|
||||||
export default class CallHandler {
|
export default class CallHandler {
|
||||||
private calls = new Map<string, MatrixCall>();
|
private calls = new Map<string, MatrixCall>();
|
||||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||||
|
@ -291,6 +305,11 @@ export default class CallHandler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setCallAudioElement(call: MatrixCall) {
|
||||||
|
const audioElement = getRemoteAudioElement();
|
||||||
|
if (audioElement) call.setRemoteAudioElement(audioElement);
|
||||||
|
}
|
||||||
|
|
||||||
private setCallState(call: MatrixCall, status: CallState) {
|
private setCallState(call: MatrixCall, status: CallState) {
|
||||||
console.log(
|
console.log(
|
||||||
`Call state in ${call.roomId} changed to ${status}`,
|
`Call state in ${call.roomId} changed to ${status}`,
|
||||||
|
@ -343,9 +362,11 @@ export default class CallHandler {
|
||||||
) {
|
) {
|
||||||
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);
|
||||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), roomId);
|
const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
|
||||||
this.calls.set(roomId, call);
|
this.calls.set(roomId, call);
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
this.setCallAudioElement(call);
|
||||||
|
|
||||||
if (type === PlaceCallType.Voice) {
|
if (type === PlaceCallType.Voice) {
|
||||||
call.placeVoiceCall();
|
call.placeVoiceCall();
|
||||||
} else if (type === 'video') {
|
} else if (type === 'video') {
|
||||||
|
@ -451,6 +472,7 @@ export default class CallHandler {
|
||||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||||
this.calls.set(call.roomId, call)
|
this.calls.set(call.roomId, call)
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
this.setCallAudioElement(call);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'hangup':
|
case 'hangup':
|
||||||
|
|
|
@ -46,6 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import {UIFeature} from "./settings/UIFeature";
|
import {UIFeature} from "./settings/UIFeature";
|
||||||
|
import CallHandler from "./CallHandler";
|
||||||
|
|
||||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||||
interface HTMLInputEvent extends Event {
|
interface HTMLInputEvent extends Event {
|
||||||
|
@ -1057,6 +1058,32 @@ export const Commands = [
|
||||||
},
|
},
|
||||||
category: CommandCategories.actions,
|
category: CommandCategories.actions,
|
||||||
}),
|
}),
|
||||||
|
new Command({
|
||||||
|
command: "holdcall",
|
||||||
|
description: _td("Places the call in the current room on hold"),
|
||||||
|
category: CommandCategories.other,
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||||
|
if (!call) {
|
||||||
|
return reject("No active call in this room");
|
||||||
|
}
|
||||||
|
call.setRemoteOnHold(true);
|
||||||
|
return success();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new Command({
|
||||||
|
command: "unholdcall",
|
||||||
|
description: _td("Takes the call in the current room off hold"),
|
||||||
|
category: CommandCategories.other,
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||||
|
if (!call) {
|
||||||
|
return reject("No active call in this room");
|
||||||
|
}
|
||||||
|
call.setRemoteOnHold(false);
|
||||||
|
return success();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
// Command definitions for autocompletion ONLY:
|
// Command definitions for autocompletion ONLY:
|
||||||
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
||||||
|
|
|
@ -71,7 +71,7 @@ import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import TintableSvg from "../views/elements/TintableSvg";
|
import TintableSvg from "../views/elements/TintableSvg";
|
||||||
import {XOR} from "../../@types/common";
|
import {XOR} from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
|
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import WidgetStore from "../../stores/WidgetStore";
|
import WidgetStore from "../../stores/WidgetStore";
|
||||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||||
import Notifier from "../../Notifier";
|
import Notifier from "../../Notifier";
|
||||||
|
|
|
@ -24,7 +24,7 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import PersistentApp from "../elements/PersistentApp";
|
import PersistentApp from "../elements/PersistentApp";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CallState, MatrixCall } from 'matrix-js-sdk/lib/webrtc/call';
|
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,17 +15,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {createRef} from 'react';
|
import React, { createRef } from 'react';
|
||||||
import Room from 'matrix-js-sdk/src/models/room';
|
import Room from 'matrix-js-sdk/src/models/room';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import CallHandler from '../../../CallHandler';
|
import CallHandler from '../../../CallHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import VideoView from "./VideoView";
|
import VideoFeed, { VideoFeedType } from "./VideoFeed";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import PulsedAvatar from '../avatars/PulsedAvatar';
|
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||||
import { CallState, MatrixCall } from 'matrix-js-sdk/lib/webrtc/call';
|
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// js-sdk room object. If set, we will only show calls for the given
|
// js-sdk room object. If set, we will only show calls for the given
|
||||||
|
@ -50,53 +51,104 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
call: any;
|
call: MatrixCall;
|
||||||
|
isLocalOnHold: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFullScreenElement() {
|
||||||
|
return (
|
||||||
|
document.fullscreenElement ||
|
||||||
|
// moz omitted because firefox supports this unprefixed now (webkit here for safari)
|
||||||
|
document.webkitFullscreenElement ||
|
||||||
|
document.msFullscreenElement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestFullscreen(element: Element) {
|
||||||
|
const method = (
|
||||||
|
element.requestFullscreen ||
|
||||||
|
// moz omitted since firefox supports unprefixed now
|
||||||
|
element.webkitRequestFullScreen ||
|
||||||
|
element.msRequestFullscreen
|
||||||
|
);
|
||||||
|
if (method) method.call(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitFullscreen() {
|
||||||
|
const exitMethod = (
|
||||||
|
document.exitFullscreen ||
|
||||||
|
document.webkitExitFullscreen ||
|
||||||
|
document.msExitFullscreen
|
||||||
|
);
|
||||||
|
if (exitMethod) exitMethod.call(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallView extends React.Component<IProps, IState> {
|
export default class CallView extends React.Component<IProps, IState> {
|
||||||
private videoref: React.RefObject<any>;
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
public call: any;
|
private container = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const call = this.getCall();
|
||||||
this.state = {
|
this.state = {
|
||||||
// the call this view is displaying (if any)
|
call,
|
||||||
call: null,
|
isLocalOnHold: call ? call.isLocalOnHold() : null,
|
||||||
};
|
}
|
||||||
|
|
||||||
this.videoref = createRef();
|
this.updateCallListeners(null, call);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.showCall();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
this.updateCallListeners(this.state.call, null);
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload) => {
|
private onAction = (payload) => {
|
||||||
// don't filter out payloads for room IDs other than props.room because
|
switch (payload.action) {
|
||||||
// we may be interested in the conf 1:1 room
|
case 'video_fullscreen': {
|
||||||
if (payload.action !== 'call_state') {
|
if (!this.container.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.showCall();
|
if (payload.fullscreen) {
|
||||||
|
requestFullscreen(this.container.current);
|
||||||
|
} else if (getFullScreenElement()) {
|
||||||
|
exitFullscreen();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'call_state': {
|
||||||
|
const newCall = this.getCall();
|
||||||
|
if (newCall !== this.state.call) {
|
||||||
|
this.updateCallListeners(this.state.call, newCall);
|
||||||
|
this.setState({
|
||||||
|
call: newCall,
|
||||||
|
isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!newCall && getFullScreenElement()) {
|
||||||
|
exitFullscreen();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private showCall() {
|
private getCall(): MatrixCall {
|
||||||
let call: MatrixCall;
|
let call: MatrixCall;
|
||||||
|
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||||
|
|
||||||
if (this.call) {
|
// We don't currently show voice calls in this view when in the room:
|
||||||
this.setState({ call: call });
|
// they're represented in the room status bar at the bottom instead
|
||||||
}
|
// (but this will all change with the new designs)
|
||||||
|
if (call && call.type == CallType.Voice) call = null;
|
||||||
} else {
|
} else {
|
||||||
call = CallHandler.sharedInstance().getAnyActiveCall();
|
call = CallHandler.sharedInstance().getAnyActiveCall();
|
||||||
// Ignore calls if we can't get the room associated with them.
|
// Ignore calls if we can't get the room associated with them.
|
||||||
|
@ -106,45 +158,42 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
|
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
|
||||||
call = null;
|
call = null;
|
||||||
}
|
}
|
||||||
this.setState({ call: call });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (call) {
|
if (call && call.state == CallState.Ended) return null;
|
||||||
if (this.getVideoView()) {
|
return call;
|
||||||
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
|
||||||
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
|
||||||
|
|
||||||
// always use a separate element for audio stream playback.
|
|
||||||
// this is to let us move CallView around the DOM without interrupting remote audio
|
|
||||||
// during playback, by having the audio rendered by a top-level <audio/> element.
|
|
||||||
// rather than being rendered by the main remoteVideo <video/> element.
|
|
||||||
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (call && call.type === "video" && call.state !== CallState.Ended && call.state !== CallState.Ringing) {
|
|
||||||
this.getVideoView().getLocalVideoElement().style.display = "block";
|
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
|
||||||
} else {
|
|
||||||
this.getVideoView().getLocalVideoElement().style.display = "none";
|
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "none";
|
|
||||||
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onResize) {
|
private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
|
||||||
this.props.onResize();
|
if (oldCall === newCall) return;
|
||||||
}
|
|
||||||
|
if (oldCall) oldCall.removeListener(CallEvent.HoldUnhold, this.onCallHoldUnhold);
|
||||||
|
if (newCall) newCall.on(CallEvent.HoldUnhold, this.onCallHoldUnhold);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVideoView() {
|
private onCallHoldUnhold = () => {
|
||||||
return this.videoref.current;
|
this.setState({
|
||||||
}
|
isLocalOnHold: this.state.call ? this.state.call.isLocalOnHold() : null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
let view: React.ReactNode;
|
let view: React.ReactNode;
|
||||||
if (this.state.call && this.state.call.type === "voice") {
|
|
||||||
|
if (this.state.call) {
|
||||||
|
if (this.state.call.type === "voice") {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const callRoom = client.getRoom(this.state.call.roomId);
|
const callRoom = client.getRoom(this.state.call.roomId);
|
||||||
|
|
||||||
|
let caption = _t("Active call");
|
||||||
|
if (this.state.isLocalOnHold) {
|
||||||
|
// we currently have no UI for holding / unholding a call (apart from slash
|
||||||
|
// commands) so we don't disintguish between when we've put the call on hold
|
||||||
|
// (ie. we'd show an unhold button) and when the other side has put us on hold
|
||||||
|
// (where obviously we would not show such a button).
|
||||||
|
caption = _t("Call Paused");
|
||||||
|
}
|
||||||
|
|
||||||
view = <AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
|
view = <AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
|
||||||
<PulsedAvatar>
|
<PulsedAvatar>
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
|
@ -155,16 +204,22 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
</PulsedAvatar>
|
</PulsedAvatar>
|
||||||
<div>
|
<div>
|
||||||
<h1>{callRoom.name}</h1>
|
<h1>{callRoom.name}</h1>
|
||||||
<p>{ _t("Active call") }</p>
|
<p>{ caption }</p>
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else {
|
} else {
|
||||||
view = <VideoView
|
// For video calls, we currently ignore the call hold state altogether
|
||||||
ref={this.videoref}
|
// (the video will just go black)
|
||||||
onClick={this.props.onClick}
|
|
||||||
onResize={this.props.onResize}
|
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||||
maxHeight={this.props.maxVideoHeight}
|
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight;
|
||||||
/>;
|
view = <div className="mx_CallView_video" onClick={this.props.onClick}>
|
||||||
|
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
|
||||||
|
maxHeight={maxVideoHeight}
|
||||||
|
/>
|
||||||
|
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hangup: React.ReactNode;
|
let hangup: React.ReactNode;
|
||||||
|
@ -180,10 +235,9 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={this.props.className}>
|
return <div className={this.props.className} ref={this.container}>
|
||||||
{view}
|
{view}
|
||||||
{hangup}
|
{hangup}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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 React, {createRef} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
export default class VideoFeed extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
// maxHeight style attribute for the video element
|
|
||||||
maxHeight: PropTypes.number,
|
|
||||||
|
|
||||||
// a callback which is called when the video element is resized
|
|
||||||
// due to a change in video metadata
|
|
||||||
onResize: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this._vid = createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._vid.current.addEventListener('resize', this.onResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this._vid.current.removeEventListener('resize', this.onResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
onResize = (e) => {
|
|
||||||
if (this.props.onResize) {
|
|
||||||
this.props.onResize(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<video ref={this._vid} style={{maxHeight: this.props.maxHeight}}>
|
|
||||||
</video>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
80
src/components/views/voip/VideoFeed.tsx
Normal file
80
src/components/views/voip/VideoFeed.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016, 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
export enum VideoFeedType {
|
||||||
|
Local,
|
||||||
|
Remote,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
call: MatrixCall,
|
||||||
|
|
||||||
|
type: VideoFeedType,
|
||||||
|
|
||||||
|
// maxHeight style attribute for the video element
|
||||||
|
maxHeight?: number,
|
||||||
|
|
||||||
|
// a callback which is called when the video element is resized
|
||||||
|
// due to a change in video metadata
|
||||||
|
onResize?: (e: Event) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class VideoFeed extends React.Component<IProps> {
|
||||||
|
private vid = createRef<HTMLVideoElement>();
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.vid.current.addEventListener('resize', this.onResize);
|
||||||
|
if (this.props.type === VideoFeedType.Local) {
|
||||||
|
this.props.call.setLocalVideoElement(this.vid.current);
|
||||||
|
} else {
|
||||||
|
this.props.call.setRemoteVideoElement(this.vid.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.vid.current.removeEventListener('resize', this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize = (e) => {
|
||||||
|
if (this.props.onResize) {
|
||||||
|
this.props.onResize(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const videoClasses = {
|
||||||
|
mx_VideoFeed: true,
|
||||||
|
mx_VideoFeed_local: this.props.type === VideoFeedType.Local,
|
||||||
|
mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote,
|
||||||
|
mx_VideoFeed_mirror: (
|
||||||
|
this.props.type === VideoFeedType.Local &&
|
||||||
|
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let videoStyle = {};
|
||||||
|
if (this.props.maxHeight) videoStyle = { maxHeight: this.props.maxHeight };
|
||||||
|
|
||||||
|
return <div className={classnames(videoClasses)}>
|
||||||
|
<video ref={this.vid} style={videoStyle}></video>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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 React, {createRef} from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
|
||||||
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
|
|
||||||
function getFullScreenElement() {
|
|
||||||
return (
|
|
||||||
document.fullscreenElement ||
|
|
||||||
document.mozFullScreenElement ||
|
|
||||||
document.webkitFullscreenElement ||
|
|
||||||
document.msFullscreenElement
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class VideoView extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
// maxHeight style attribute for the video element
|
|
||||||
maxHeight: PropTypes.number,
|
|
||||||
|
|
||||||
// a callback which is called when the user clicks on the video div
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
|
|
||||||
// a callback which is called when the video element is resized due to
|
|
||||||
// a change in video metadata
|
|
||||||
onResize: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this._local = createRef();
|
|
||||||
this._remote = createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
dis.unregister(this.dispatcherRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRemoteVideoElement = () => {
|
|
||||||
return ReactDOM.findDOMNode(this._remote.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
getRemoteAudioElement = () => {
|
|
||||||
// this needs to be somewhere at the top of the DOM which
|
|
||||||
// always exists to avoid audio interruptions.
|
|
||||||
// Might as well just use DOM.
|
|
||||||
const remoteAudioElement = document.getElementById("remoteAudio");
|
|
||||||
if (!remoteAudioElement) {
|
|
||||||
console.error("Failed to find remoteAudio element - cannot play audio!"
|
|
||||||
+ "You need to add an <audio/> to the DOM.");
|
|
||||||
}
|
|
||||||
return remoteAudioElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
getLocalVideoElement = () => {
|
|
||||||
return ReactDOM.findDOMNode(this._local.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
setContainer = (c) => {
|
|
||||||
this.container = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
onAction = (payload) => {
|
|
||||||
switch (payload.action) {
|
|
||||||
case 'video_fullscreen': {
|
|
||||||
if (!this.container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const element = this.container;
|
|
||||||
if (payload.fullscreen) {
|
|
||||||
const requestMethod = (
|
|
||||||
element.requestFullScreen ||
|
|
||||||
element.webkitRequestFullScreen ||
|
|
||||||
element.mozRequestFullScreen ||
|
|
||||||
element.msRequestFullscreen
|
|
||||||
);
|
|
||||||
requestMethod.call(element);
|
|
||||||
} else if (getFullScreenElement()) {
|
|
||||||
const exitMethod = (
|
|
||||||
document.exitFullscreen ||
|
|
||||||
document.mozCancelFullScreen ||
|
|
||||||
document.webkitExitFullscreen ||
|
|
||||||
document.msExitFullscreen
|
|
||||||
);
|
|
||||||
if (exitMethod) {
|
|
||||||
exitMethod.call(document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const VideoFeed = sdk.getComponent('voip.VideoFeed');
|
|
||||||
|
|
||||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
|
||||||
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxHeight;
|
|
||||||
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
|
|
||||||
{ "mx_VideoView_localVideoFeed_flipped":
|
|
||||||
SettingsStore.getValue('VideoView.flipVideoHorizontally'),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="mx_VideoView" ref={this.setContainer} onClick={this.props.onClick}>
|
|
||||||
<div className="mx_VideoView_remoteVideoFeed">
|
|
||||||
<VideoFeed ref={this._remote} onResize={this.props.onResize}
|
|
||||||
maxHeight={maxVideoHeight} />
|
|
||||||
</div>
|
|
||||||
<div className={localVideoFeedClasses}>
|
|
||||||
<VideoFeed ref={this._local} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -462,6 +462,8 @@
|
||||||
"Send a bug report with logs": "Send a bug report with logs",
|
"Send a bug report with logs": "Send a bug report with logs",
|
||||||
"Opens chat with the given user": "Opens chat with the given user",
|
"Opens chat with the given user": "Opens chat with the given user",
|
||||||
"Sends a message to the given user": "Sends a message to the given user",
|
"Sends a message to the given user": "Sends a message to the given user",
|
||||||
|
"Places the call in the current room on hold": "Places the call in the current room on hold",
|
||||||
|
"Takes the call in the current room off hold": "Takes the call in the current room off hold",
|
||||||
"Displays action": "Displays action",
|
"Displays action": "Displays action",
|
||||||
"Reason": "Reason",
|
"Reason": "Reason",
|
||||||
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
|
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
|
||||||
|
@ -777,6 +779,7 @@
|
||||||
"My Ban List": "My Ban List",
|
"My Ban List": "My Ban List",
|
||||||
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
||||||
"Active call": "Active call",
|
"Active call": "Active call",
|
||||||
|
"Call Paused": "Call Paused",
|
||||||
"Unknown caller": "Unknown caller",
|
"Unknown caller": "Unknown caller",
|
||||||
"Incoming voice call": "Incoming voice call",
|
"Incoming voice call": "Incoming voice call",
|
||||||
"Incoming video call": "Incoming video call",
|
"Incoming video call": "Incoming video call",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue