// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useState, useRef, useEffect, useCallback } from 'react'; import { noop } from 'lodash'; import classNames from 'classnames'; import { ActiveCallType, HangUpType, SetLocalAudioType, SetLocalPreviewType, SetLocalVideoType, SetRendererCanvasType, } from '../state/ducks/calling'; import { Avatar } from './Avatar'; import { CallingHeader } from './CallingHeader'; import { CallingButton, CallingButtonType } from './CallingButton'; import { CallBackgroundBlur } from './CallBackgroundBlur'; import { CallMode, CallState, GroupCallConnectionState, VideoFrameSource, } from '../types/Calling'; import { ColorType } from '../types/Colors'; import { LocalizerType } from '../types/Util'; import { missingCaseError } from '../util/missingCaseError'; import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant'; import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants'; export type PropsType = { activeCall: ActiveCallType; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; hangUp: (_: HangUpType) => void; hasLocalAudio: boolean; hasLocalVideo: boolean; i18n: LocalizerType; joinedAt?: number; me: { avatarPath?: string; color?: ColorType; name?: string; phoneNumber?: string; profileName?: string; title: string; }; setLocalAudio: (_: SetLocalAudioType) => void; setLocalVideo: (_: SetLocalVideoType) => void; setLocalPreview: (_: SetLocalPreviewType) => void; setRendererCanvas: (_: SetRendererCanvasType) => void; stickyControls: boolean; toggleParticipants: () => void; togglePip: () => void; toggleSettings: () => void; }; export const CallScreen: React.FC = ({ activeCall, getGroupCallVideoFrameSource, hangUp, hasLocalAudio, hasLocalVideo, i18n, joinedAt, me, setLocalAudio, setLocalVideo, setLocalPreview, setRendererCanvas, stickyControls, toggleParticipants, togglePip, toggleSettings, }) => { const { call, conversation, groupCallParticipants } = activeCall; const toggleAudio = useCallback(() => { setLocalAudio({ enabled: !hasLocalAudio, }); }, [setLocalAudio, hasLocalAudio]); const toggleVideo = useCallback(() => { setLocalVideo({ enabled: !hasLocalVideo, }); }, [setLocalVideo, hasLocalVideo]); const [acceptedDuration, setAcceptedDuration] = useState(null); const [showControls, setShowControls] = useState(true); const localVideoRef = useRef(null); useEffect(() => { setLocalPreview({ element: localVideoRef }); return () => { setLocalPreview({ element: undefined }); }; }, [setLocalPreview, setRendererCanvas]); useEffect(() => { if (!joinedAt) { return noop; } // It's really jumpy with a value of 500ms. const interval = setInterval(() => { setAcceptedDuration(Date.now() - joinedAt); }, 100); return clearInterval.bind(null, interval); }, [joinedAt]); useEffect(() => { if (!showControls || stickyControls) { return noop; } const timer = setTimeout(() => { setShowControls(false); }, 5000); return clearInterval.bind(null, timer); }, [showControls, stickyControls]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent): void => { let eventHandled = false; if (event.shiftKey && (event.key === 'V' || event.key === 'v')) { toggleVideo(); eventHandled = true; } else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) { toggleAudio(); eventHandled = true; } if (eventHandled) { event.preventDefault(); event.stopPropagation(); setShowControls(true); } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [toggleAudio, toggleVideo]); let hasRemoteVideo: boolean; let isConnected: boolean; let remoteParticipantsElement: JSX.Element; switch (call.callMode) { case CallMode.Direct: hasRemoteVideo = Boolean(call.hasRemoteVideo); isConnected = call.callState === CallState.Accepted; remoteParticipantsElement = ( ); break; case CallMode.Group: hasRemoteVideo = call.remoteParticipants.some( remoteParticipant => remoteParticipant.hasRemoteVideo ); isConnected = call.connectionState === GroupCallConnectionState.Connected; remoteParticipantsElement = ( ); break; default: throw missingCaseError(call); } const videoButtonType = hasLocalVideo ? CallingButtonType.VIDEO_ON : CallingButtonType.VIDEO_OFF; const audioButtonType = hasLocalAudio ? CallingButtonType.AUDIO_ON : CallingButtonType.AUDIO_OFF; const isAudioOnly = !hasLocalVideo && !hasRemoteVideo; const controlsFadeClass = classNames({ 'module-ongoing-call__controls--fadeIn': (showControls || isAudioOnly) && !isConnected, 'module-ongoing-call__controls--fadeOut': !showControls && !isAudioOnly && isConnected, }); const remoteParticipants = call.callMode === CallMode.Group ? activeCall.groupCallParticipants.length : 0; const { showParticipantsList } = activeCall.activeCallState; return (
{ setShowControls(true); }} role="group" >
{call.callMode === CallMode.Group && !call.remoteParticipants.length ? i18n('calling__in-this-call--zero') : ''} {call.callMode === CallMode.Direct && renderHeaderMessage( i18n, call.callState || CallState.Prering, acceptedDuration )} } i18n={i18n} isGroupCall={call.callMode === CallMode.Group} remoteParticipants={remoteParticipants} showParticipantsList={showParticipantsList} toggleParticipants={toggleParticipants} togglePip={togglePip} toggleSettings={toggleSettings} />
{remoteParticipantsElement}
{/* This layout-only element is not ideal. See the comment in _modules.css for more. */}
{ hangUp({ conversationId: conversation.id }); }} />
{hasLocalVideo ? (
); }; function getCallModeClassSuffix( callMode: CallMode.Direct | CallMode.Group ): string { switch (callMode) { case CallMode.Direct: return 'direct'; case CallMode.Group: return 'group'; default: throw missingCaseError(callMode); } } function renderHeaderMessage( i18n: LocalizerType, callState: CallState, acceptedDuration: null | number ): JSX.Element | null { 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 && acceptedDuration) { message = i18n('callDuration', [renderDuration(acceptedDuration)]); } if (!message) { return null; } return
{message}
; } function 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}`; }