Implement call hold
Currently just by adding /holdcall and /unholdcall slash commands The only place the hold status of the call is currently represented is when the call is a voice call and you're viewing a different room: it's not wired up when you're viewing the room because that currently uses the room status bar which it won't do with the new UI. Also convert VideoFeed to typescript, and remove videoview because it essentially just managed the fullscreen functionality, but we'll want and 'on hold' representation (and probably chrome for hagnup etc) in the fullscreen UI too, so let's just make CallView the thing that gets fullscreened.
This commit is contained in:
parent
ca4e7202ae
commit
f828c6d494
15 changed files with 287 additions and 330 deletions
|
@ -9,6 +9,7 @@
|
||||||
@import "./structures/_CustomRoomTagPanel.scss";
|
@import "./structures/_CustomRoomTagPanel.scss";
|
||||||
@import "./structures/_FilePanel.scss";
|
@import "./structures/_FilePanel.scss";
|
||||||
@import "./structures/_GenericErrorPage.scss";
|
@import "./structures/_GenericErrorPage.scss";
|
||||||
|
@import "./structures/_GroupFilterPanel.scss";
|
||||||
@import "./structures/_GroupView.scss";
|
@import "./structures/_GroupView.scss";
|
||||||
@import "./structures/_HeaderButtons.scss";
|
@import "./structures/_HeaderButtons.scss";
|
||||||
@import "./structures/_HomePage.scss";
|
@import "./structures/_HomePage.scss";
|
||||||
|
@ -27,7 +28,6 @@
|
||||||
@import "./structures/_ScrollPanel.scss";
|
@import "./structures/_ScrollPanel.scss";
|
||||||
@import "./structures/_SearchBox.scss";
|
@import "./structures/_SearchBox.scss";
|
||||||
@import "./structures/_TabbedView.scss";
|
@import "./structures/_TabbedView.scss";
|
||||||
@import "./structures/_GroupFilterPanel.scss";
|
|
||||||
@import "./structures/_ToastContainer.scss";
|
@import "./structures/_ToastContainer.scss";
|
||||||
@import "./structures/_UploadBar.scss";
|
@import "./structures/_UploadBar.scss";
|
||||||
@import "./structures/_UserMenu.scss";
|
@import "./structures/_UserMenu.scss";
|
||||||
|
@ -227,4 +227,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,49 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_VideoView {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VideoView video {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VideoView_remoteVideoFeed {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #000;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed {
|
|
||||||
width: 25%;
|
|
||||||
height: 25%;
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
bottom: 10px;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed video {
|
|
||||||
width: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video {
|
|
||||||
transform: scale(-1, 1);
|
|
||||||
}
|
|
14
src/@types/global.d.ts
vendored
14
src/@types/global.d.ts
vendored
|
@ -65,6 +65,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 {
|
||||||
|
@ -94,4 +101,11 @@ declare global {
|
||||||
interface HTMLAudioElement {
|
interface HTMLAudioElement {
|
||||||
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>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 Matrix from 'matrix-js-sdk/src/browser-index';
|
||||||
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 } from "matrix-js-sdk/lib/webrtc/call";
|
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
|
|
||||||
enum AudioID {
|
enum AudioID {
|
||||||
|
@ -97,6 +96,18 @@ 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 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>>();
|
||||||
|
@ -290,6 +301,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}`,
|
||||||
|
@ -344,6 +360,8 @@ export default class CallHandler {
|
||||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), roomId);
|
const call = Matrix.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') {
|
||||||
|
@ -448,6 +466,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 {
|
||||||
|
@ -1039,6 +1040,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";
|
||||||
|
|
||||||
|
|
|
@ -163,7 +163,7 @@ export default class AppTile extends React.Component {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
this._sgWidget.start(ref);
|
this._sgWidget.start(ref);
|
||||||
} else {
|
} else {
|
||||||
this._resetWidget(this.props);
|
//this._resetWidget(this.props);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
82
src/components/views/voip/VideoFeed.tsx
Normal file
82
src/components/views/voip/VideoFeed.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
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 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -213,6 +213,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.",
|
||||||
|
@ -529,6 +531,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…
Reference in a new issue