signal-desktop/ts/components/CallScreen.tsx
2020-07-30 13:27:41 -07:00

382 lines
9 KiB
TypeScript

import React from 'react';
import classNames from 'classnames';
import {
CallDetailsType,
HangUpType,
SetLocalAudioType,
SetLocalVideoType,
SetVideoCapturerType,
SetVideoRendererType,
} from '../state/ducks/calling';
import { Avatar } from './Avatar';
import { CallState } from '../types/Calling';
import { LocalizerType } from '../types/Util';
import { CanvasVideoRenderer, GumVideoCapturer } from '../window.d';
type CallingButtonProps = {
classNameSuffix: string;
onClick: () => void;
};
const CallingButton = ({
classNameSuffix,
onClick,
}: CallingButtonProps): JSX.Element => {
const className = classNames(
'module-ongoing-call__icon',
`module-ongoing-call__icon${classNameSuffix}`
);
return (
<button className={className} onClick={onClick}>
<div />
</button>
);
};
export type PropsType = {
callDetails?: CallDetailsType;
callState?: CallState;
getVideoCapturer: (
ref: React.RefObject<HTMLVideoElement>
) => GumVideoCapturer;
getVideoRenderer: (
ref: React.RefObject<HTMLCanvasElement>
) => CanvasVideoRenderer;
hangUp: (_: HangUpType) => void;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
hasRemoteVideo: boolean;
i18n: LocalizerType;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setVideoCapturer: (_: SetVideoCapturerType) => void;
setVideoRenderer: (_: SetVideoRendererType) => void;
};
type StateType = {
acceptedTime: number | null;
acceptedDuration: number | null;
showControls: boolean;
};
export class CallScreen extends React.Component<PropsType, StateType> {
private interval: any;
private controlsFadeTimer: any;
private readonly localVideoRef: React.RefObject<HTMLVideoElement>;
private readonly remoteVideoRef: React.RefObject<HTMLCanvasElement>;
constructor(props: PropsType) {
super(props);
this.state = {
acceptedTime: null,
acceptedDuration: null,
showControls: true,
};
this.interval = null;
this.controlsFadeTimer = null;
this.localVideoRef = React.createRef();
this.remoteVideoRef = React.createRef();
this.setVideoCapturerAndRenderer(
props.getVideoCapturer(this.localVideoRef),
props.getVideoRenderer(this.remoteVideoRef)
);
}
public componentDidMount() {
// It's really jump with a value of 500ms.
this.interval = setInterval(this.updateAcceptedTimer, 100);
this.fadeControls();
document.addEventListener('keydown', this.handleKeyDown);
}
public componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
if (this.interval) {
clearInterval(this.interval);
}
if (this.controlsFadeTimer) {
clearTimeout(this.controlsFadeTimer);
}
this.setVideoCapturerAndRenderer(null, null);
}
updateAcceptedTimer = () => {
const { acceptedTime } = this.state;
const { callState } = this.props;
if (acceptedTime) {
this.setState({
acceptedTime,
acceptedDuration: Date.now() - acceptedTime,
});
} else if (
callState === CallState.Accepted ||
callState === CallState.Reconnecting
) {
this.setState({
acceptedTime: Date.now(),
acceptedDuration: 1,
});
}
};
handleKeyDown = (event: KeyboardEvent) => {
const { callDetails } = this.props;
if (!callDetails) {
return;
}
let eventHandled = false;
if (event.key === 'V') {
this.toggleVideo();
eventHandled = true;
} else if (event.key === 'M') {
this.toggleAudio();
eventHandled = true;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
this.showControls();
}
};
showControls = () => {
if (!this.state.showControls) {
this.setState({
showControls: true,
});
}
this.fadeControls();
};
fadeControls = () => {
if (this.controlsFadeTimer) {
clearTimeout(this.controlsFadeTimer);
}
this.controlsFadeTimer = setTimeout(() => {
this.setState({
showControls: false,
});
}, 5000);
};
toggleAudio = () => {
const { callDetails, hasLocalAudio, setLocalAudio } = this.props;
if (!callDetails) {
return;
}
setLocalAudio({
callId: callDetails.callId,
enabled: !hasLocalAudio,
});
};
toggleVideo = () => {
const { callDetails, hasLocalVideo, setLocalVideo } = this.props;
if (!callDetails) {
return;
}
setLocalVideo({ callId: callDetails.callId, enabled: !hasLocalVideo });
};
public render() {
const {
callDetails,
callState,
hangUp,
hasLocalAudio,
hasLocalVideo,
hasRemoteVideo,
} = this.props;
const { showControls } = this.state;
const isAudioOnly = !hasLocalVideo && !hasRemoteVideo;
if (!callDetails || !callState) {
return null;
}
const controlsFadeClass = classNames({
'module-ongoing-call__controls--fadeIn':
(showControls || isAudioOnly) && callState !== CallState.Accepted,
'module-ongoing-call__controls--fadeOut':
!showControls && !isAudioOnly && callState === CallState.Accepted,
});
const toggleAudioSuffix = hasLocalAudio
? '--audio--enabled'
: '--audio--disabled';
const toggleVideoSuffix = hasLocalVideo
? '--video--enabled'
: '--video--disabled';
return (
<div
className="module-ongoing-call"
onMouseMove={this.showControls}
role="group"
>
<div
className={classNames(
'module-ongoing-call__header',
controlsFadeClass
)}
>
<div className="module-ongoing-call__header-name">
{callDetails.name}
</div>
{this.renderMessage(callState)}
</div>
{hasRemoteVideo
? this.renderRemoteVideo()
: this.renderAvatar(callDetails)}
{hasLocalVideo ? this.renderLocalVideo() : null}
<div
className={classNames(
'module-ongoing-call__actions',
controlsFadeClass
)}
>
<CallingButton
classNameSuffix={toggleVideoSuffix}
onClick={this.toggleVideo}
/>
<CallingButton
classNameSuffix={toggleAudioSuffix}
onClick={this.toggleAudio}
/>
<CallingButton
classNameSuffix="--hangup"
onClick={() => {
hangUp({ callId: callDetails.callId });
}}
/>
</div>
</div>
);
}
private renderAvatar(callDetails: CallDetailsType) {
const { i18n } = this.props;
const {
avatarPath,
color,
name,
phoneNumber,
profileName,
title,
} = callDetails;
return (
<div className="module-ongoing-call__remote-video-disabled">
<Avatar
avatarPath={avatarPath}
color={color || 'ultramarine'}
noteToSelf={false}
conversationType="direct"
i18n={i18n}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
size={112}
/>
</div>
);
}
private renderLocalVideo() {
return (
<video
className="module-ongoing-call__local-video"
ref={this.localVideoRef}
autoPlay
/>
);
}
private renderRemoteVideo() {
return (
<canvas
className="module-ongoing-call__remote-video-enabled"
ref={this.remoteVideoRef}
/>
);
}
private renderMessage(callState: CallState) {
const { i18n } = this.props;
let message = null;
if (callState === CallState.Prering) {
message = i18n('outgoingCallPrering');
} else if (callState === CallState.Ringing) {
message = i18n('outgoingCallRinging');
} else if (callState === CallState.Reconnecting) {
message = i18n('callReconnecting');
} else if (
callState === CallState.Accepted &&
this.state.acceptedDuration
) {
message = i18n('callDuration', [
this.renderDuration(this.state.acceptedDuration),
]);
}
if (!message) {
return null;
}
return <div className="module-ongoing-call__header-message">{message}</div>;
}
private renderDuration(ms: number): string {
const secs = Math.floor((ms / 1000) % 60)
.toString()
.padStart(2, '0');
const mins = Math.floor((ms / 60000) % 60)
.toString()
.padStart(2, '0');
const hours = Math.floor(ms / 3600000);
if (hours > 0) {
return `${hours}:${mins}:${secs}`;
}
return `${mins}:${secs}`;
}
private setVideoCapturerAndRenderer(
capturer: GumVideoCapturer | null,
renderer: CanvasVideoRenderer | null
) {
const { callDetails, setVideoCapturer, setVideoRenderer } = this.props;
if (!callDetails) {
return;
}
const { callId } = callDetails;
setVideoCapturer({
callId,
capturer,
});
setVideoRenderer({
callId,
renderer,
});
}
}