Calling: Device Selection
This commit is contained in:
parent
8b34294c97
commit
8ab1013f70
17 changed files with 1038 additions and 135 deletions
|
@ -2725,5 +2725,25 @@
|
||||||
"example": "00:01"
|
"example": "00:01"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"callingDeviceSelection__settings": {
|
||||||
|
"message": "Settings",
|
||||||
|
"description": "Title for device selection settings"
|
||||||
|
},
|
||||||
|
"callingDeviceSelection__label--video": {
|
||||||
|
"message": "Video",
|
||||||
|
"description": "Label for video input selector"
|
||||||
|
},
|
||||||
|
"callingDeviceSelection__label--audio-input": {
|
||||||
|
"message": "Microphone",
|
||||||
|
"description": "Label for audio input selector"
|
||||||
|
},
|
||||||
|
"callingDeviceSelection__label--audio-output": {
|
||||||
|
"message": "Speakers",
|
||||||
|
"description": "Label for audio output selector"
|
||||||
|
},
|
||||||
|
"callingDeviceSelection__select--no-device": {
|
||||||
|
"message": "No devices available",
|
||||||
|
"description": "Message for when there are no available devices to select for input/output audio or video"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
"redux-ts-utils": "3.2.2",
|
"redux-ts-utils": "3.2.2",
|
||||||
"reselect": "4.0.0",
|
"reselect": "4.0.0",
|
||||||
"rimraf": "2.6.2",
|
"rimraf": "2.6.2",
|
||||||
"ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#de96fa13f04e846bdcb0ae9be8e2dc80a932f2be",
|
"ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#0cd60f529d8f17734fe19e7195c9fd3c3f4271db",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"sanitize.css": "11.0.0",
|
"sanitize.css": "11.0.0",
|
||||||
"semver": "5.4.1",
|
"semver": "5.4.1",
|
||||||
|
|
|
@ -5984,6 +5984,21 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-ongoing-call__settings {
|
||||||
|
position: absolute;
|
||||||
|
top: 25px;
|
||||||
|
right: 25px;
|
||||||
|
|
||||||
|
&--button {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/settings-solid-16.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Module: Left Pane
|
// Module: Left Pane
|
||||||
|
|
||||||
.module-left-pane {
|
.module-left-pane {
|
||||||
|
@ -8847,6 +8862,87 @@ button.module-image__border-overlay:focus {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Calling: Device Selection */
|
||||||
|
|
||||||
|
.module-calling-device-selection {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-calling-device-selection__close-button {
|
||||||
|
@include button-reset;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg('../images/x-shadow-16.svg', $color-gray-75);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/x-shadow-16.svg', $color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 0;
|
||||||
|
width: 16px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid $ultramarine-ui-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-calling-device-selection__title {
|
||||||
|
@include font-title-2;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-calling-device-selection__label {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-calling-device-selection__select {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
select {
|
||||||
|
@include font-body-1;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid $color-gray-45;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 40px;
|
||||||
|
outline: 0;
|
||||||
|
padding: 10px;
|
||||||
|
padding-right: 32px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: 2px solid $color-gray-75;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
border-right: 0;
|
||||||
|
border-top: 0;
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
height: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 16px;
|
||||||
|
transform-origin: center;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
width: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Third-party module: react-tooltip-lite */
|
/* Third-party module: react-tooltip-lite */
|
||||||
|
|
||||||
.react-tooltip-lite {
|
.react-tooltip-lite {
|
||||||
|
|
|
@ -31,17 +31,18 @@ const defaultProps = {
|
||||||
callDetails,
|
callDetails,
|
||||||
callState: CallState.Accepted,
|
callState: CallState.Accepted,
|
||||||
declineCall: action('decline-call'),
|
declineCall: action('decline-call'),
|
||||||
getVideoCapturer: () => ({}),
|
|
||||||
getVideoRenderer: () => ({}),
|
|
||||||
hangUp: action('hang-up'),
|
hangUp: action('hang-up'),
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: true,
|
hasLocalVideo: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
i18n,
|
i18n,
|
||||||
|
renderDeviceSelection: () => <div />,
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
setVideoCapturer: action('set-video-capturer'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
setVideoRenderer: action('set-video-renderer'),
|
settingsDialogOpen: false,
|
||||||
|
toggleSettings: action('toggle-settings'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const permutations = [
|
const permutations = [
|
||||||
|
|
|
@ -10,7 +10,10 @@ import { CallDetailsType } from '../state/ducks/calling';
|
||||||
type CallManagerPropsType = {
|
type CallManagerPropsType = {
|
||||||
callDetails?: CallDetailsType;
|
callDetails?: CallDetailsType;
|
||||||
callState?: CallState;
|
callState?: CallState;
|
||||||
|
renderDeviceSelection: () => JSX.Element;
|
||||||
|
settingsDialogOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsType = IncomingCallBarPropsType &
|
type PropsType = IncomingCallBarPropsType &
|
||||||
CallScreenPropsType &
|
CallScreenPropsType &
|
||||||
CallManagerPropsType;
|
CallManagerPropsType;
|
||||||
|
@ -20,17 +23,18 @@ export const CallManager = ({
|
||||||
callDetails,
|
callDetails,
|
||||||
callState,
|
callState,
|
||||||
declineCall,
|
declineCall,
|
||||||
getVideoCapturer,
|
|
||||||
getVideoRenderer,
|
|
||||||
hangUp,
|
hangUp,
|
||||||
hasLocalAudio,
|
hasLocalAudio,
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
hasRemoteVideo,
|
hasRemoteVideo,
|
||||||
i18n,
|
i18n,
|
||||||
|
renderDeviceSelection,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
|
setLocalPreview,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setVideoCapturer,
|
setRendererCanvas,
|
||||||
setVideoRenderer,
|
settingsDialogOpen,
|
||||||
|
toggleSettings,
|
||||||
}: PropsType): JSX.Element | null => {
|
}: PropsType): JSX.Element | null => {
|
||||||
if (!callDetails || !callState) {
|
if (!callDetails || !callState) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -43,21 +47,23 @@ export const CallManager = ({
|
||||||
|
|
||||||
if (outgoing || ongoing) {
|
if (outgoing || ongoing) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<CallScreen
|
<CallScreen
|
||||||
callDetails={callDetails}
|
callDetails={callDetails}
|
||||||
callState={callState}
|
callState={callState}
|
||||||
getVideoCapturer={getVideoCapturer}
|
|
||||||
getVideoRenderer={getVideoRenderer}
|
|
||||||
hangUp={hangUp}
|
hangUp={hangUp}
|
||||||
hasLocalAudio={hasLocalAudio}
|
hasLocalAudio={hasLocalAudio}
|
||||||
hasLocalVideo={hasLocalVideo}
|
hasLocalVideo={hasLocalVideo}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
hasRemoteVideo={hasRemoteVideo}
|
hasRemoteVideo={hasRemoteVideo}
|
||||||
setVideoCapturer={setVideoCapturer}
|
setLocalPreview={setLocalPreview}
|
||||||
setVideoRenderer={setVideoRenderer}
|
setRendererCanvas={setRendererCanvas}
|
||||||
setLocalAudio={setLocalAudio}
|
setLocalAudio={setLocalAudio}
|
||||||
setLocalVideo={setLocalVideo}
|
setLocalVideo={setLocalVideo}
|
||||||
|
toggleSettings={toggleSettings}
|
||||||
/>
|
/>
|
||||||
|
{settingsDialogOpen && renderDeviceSelection()}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,17 +30,16 @@ const callDetails = {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
callDetails,
|
callDetails,
|
||||||
callState: CallState.Accepted,
|
callState: CallState.Accepted,
|
||||||
getVideoCapturer: () => ({}),
|
|
||||||
getVideoRenderer: () => ({}),
|
|
||||||
hangUp: action('hang-up'),
|
hangUp: action('hang-up'),
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: true,
|
hasLocalVideo: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
i18n,
|
i18n,
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
setVideoCapturer: action('set-video-capturer'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
setVideoRenderer: action('set-video-renderer'),
|
toggleSettings: action('toggle-settings'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const permutations = [
|
const permutations = [
|
||||||
|
|
|
@ -4,14 +4,13 @@ import {
|
||||||
CallDetailsType,
|
CallDetailsType,
|
||||||
HangUpType,
|
HangUpType,
|
||||||
SetLocalAudioType,
|
SetLocalAudioType,
|
||||||
|
SetLocalPreviewType,
|
||||||
SetLocalVideoType,
|
SetLocalVideoType,
|
||||||
SetVideoCapturerType,
|
SetRendererCanvasType,
|
||||||
SetVideoRendererType,
|
|
||||||
} from '../state/ducks/calling';
|
} from '../state/ducks/calling';
|
||||||
import { Avatar } from './Avatar';
|
import { Avatar } from './Avatar';
|
||||||
import { CallState } from '../types/Calling';
|
import { CallState } from '../types/Calling';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { CanvasVideoRenderer, GumVideoCapturer } from '../window.d';
|
|
||||||
|
|
||||||
type CallingButtonProps = {
|
type CallingButtonProps = {
|
||||||
classNameSuffix: string;
|
classNameSuffix: string;
|
||||||
|
@ -37,12 +36,6 @@ const CallingButton = ({
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
callDetails?: CallDetailsType;
|
callDetails?: CallDetailsType;
|
||||||
callState?: CallState;
|
callState?: CallState;
|
||||||
getVideoCapturer: (
|
|
||||||
ref: React.RefObject<HTMLVideoElement>
|
|
||||||
) => GumVideoCapturer;
|
|
||||||
getVideoRenderer: (
|
|
||||||
ref: React.RefObject<HTMLCanvasElement>
|
|
||||||
) => CanvasVideoRenderer;
|
|
||||||
hangUp: (_: HangUpType) => void;
|
hangUp: (_: HangUpType) => void;
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
|
@ -50,8 +43,9 @@ export type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||||
setVideoCapturer: (_: SetVideoCapturerType) => void;
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
setVideoRenderer: (_: SetVideoRendererType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
|
toggleSettings: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateType = {
|
type StateType = {
|
||||||
|
@ -79,11 +73,6 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
||||||
this.controlsFadeTimer = null;
|
this.controlsFadeTimer = null;
|
||||||
this.localVideoRef = React.createRef();
|
this.localVideoRef = React.createRef();
|
||||||
this.remoteVideoRef = React.createRef();
|
this.remoteVideoRef = React.createRef();
|
||||||
|
|
||||||
this.setVideoCapturerAndRenderer(
|
|
||||||
props.getVideoCapturer(this.localVideoRef),
|
|
||||||
props.getVideoRenderer(this.remoteVideoRef)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
@ -92,6 +81,9 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
||||||
this.fadeControls();
|
this.fadeControls();
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleKeyDown);
|
document.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
|
||||||
|
this.props.setLocalPreview({ element: this.localVideoRef });
|
||||||
|
this.props.setRendererCanvas({ element: this.remoteVideoRef });
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -103,7 +95,8 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
||||||
if (this.controlsFadeTimer) {
|
if (this.controlsFadeTimer) {
|
||||||
clearTimeout(this.controlsFadeTimer);
|
clearTimeout(this.controlsFadeTimer);
|
||||||
}
|
}
|
||||||
this.setVideoCapturerAndRenderer(null, null);
|
this.props.setLocalPreview({ element: undefined });
|
||||||
|
this.props.setRendererCanvas({ element: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAcceptedTimer = () => {
|
updateAcceptedTimer = () => {
|
||||||
|
@ -203,6 +196,8 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
||||||
hasLocalAudio,
|
hasLocalAudio,
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
hasRemoteVideo,
|
hasRemoteVideo,
|
||||||
|
i18n,
|
||||||
|
toggleSettings,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showControls } = this.state;
|
const { showControls } = this.state;
|
||||||
const isAudioOnly = !hasLocalVideo && !hasRemoteVideo;
|
const isAudioOnly = !hasLocalVideo && !hasRemoteVideo;
|
||||||
|
@ -241,6 +236,13 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
||||||
{callDetails.title}
|
{callDetails.title}
|
||||||
</div>
|
</div>
|
||||||
{this.renderMessage(callState)}
|
{this.renderMessage(callState)}
|
||||||
|
<div className="module-ongoing-call__settings">
|
||||||
|
<button
|
||||||
|
aria-label={i18n('callingDeviceSelection__settings')}
|
||||||
|
className="module-ongoing-call__settings--button"
|
||||||
|
onClick={toggleSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasRemoteVideo
|
{hasRemoteVideo
|
||||||
? this.renderRemoteVideo()
|
? this.renderRemoteVideo()
|
||||||
|
@ -356,27 +358,4 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
return `${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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
145
ts/components/CallingDeviceSelection.stories.tsx
Normal file
145
ts/components/CallingDeviceSelection.stories.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { CallingDeviceSelection, Props } from './CallingDeviceSelection';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const audioDevice = {
|
||||||
|
name: '',
|
||||||
|
index: 0,
|
||||||
|
same_name_index: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createProps = ({
|
||||||
|
availableMicrophones = [],
|
||||||
|
availableSpeakers = [],
|
||||||
|
selectedMicrophone = audioDevice,
|
||||||
|
selectedSpeaker = audioDevice,
|
||||||
|
availableCameras = [],
|
||||||
|
selectedCamera = '',
|
||||||
|
}: Partial<Props> = {}): Props => ({
|
||||||
|
availableCameras,
|
||||||
|
availableMicrophones,
|
||||||
|
availableSpeakers,
|
||||||
|
changeIODevice: action('change-io-device'),
|
||||||
|
i18n,
|
||||||
|
selectedCamera,
|
||||||
|
selectedMicrophone,
|
||||||
|
selectedSpeaker,
|
||||||
|
toggleSettings: action('toggle-settings'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const stories = storiesOf('Components/CallingDeviceSelection', module);
|
||||||
|
|
||||||
|
stories.add('Default', () => {
|
||||||
|
return <CallingDeviceSelection {...createProps()} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
stories.add('Some Devices', () => {
|
||||||
|
const availableSpeakers = [
|
||||||
|
{
|
||||||
|
name: 'Default - Internal Microphone',
|
||||||
|
index: 0,
|
||||||
|
same_name_index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Natalie's Airpods (Bluetooth)",
|
||||||
|
index: 1,
|
||||||
|
same_name_index: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UE Boom (Bluetooth)',
|
||||||
|
index: 2,
|
||||||
|
same_name_index: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const selectedSpeaker = availableSpeakers[0];
|
||||||
|
|
||||||
|
const props = createProps({
|
||||||
|
availableSpeakers,
|
||||||
|
selectedSpeaker,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <CallingDeviceSelection {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
stories.add('All Devices', () => {
|
||||||
|
const availableSpeakers = [
|
||||||
|
{
|
||||||
|
name: 'Default - Internal Speakers',
|
||||||
|
index: 0,
|
||||||
|
same_name_index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Natalie's Airpods (Bluetooth)",
|
||||||
|
index: 1,
|
||||||
|
same_name_index: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UE Boom (Bluetooth)',
|
||||||
|
index: 2,
|
||||||
|
same_name_index: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const selectedSpeaker = availableSpeakers[0];
|
||||||
|
|
||||||
|
const availableMicrophones = [
|
||||||
|
{
|
||||||
|
name: 'Default - Internal Microphone',
|
||||||
|
index: 0,
|
||||||
|
same_name_index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Natalie's Airpods (Bluetooth)",
|
||||||
|
index: 1,
|
||||||
|
same_name_index: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const selectedMicrophone = availableMicrophones[0];
|
||||||
|
|
||||||
|
const availableCameras = [
|
||||||
|
{
|
||||||
|
deviceId:
|
||||||
|
'dfbe6effe70b0611ba0fdc2a9ea3f39f6cb110e6687948f7e5f016c111b7329c',
|
||||||
|
groupId:
|
||||||
|
'63ee218d2446869e40adfc958ff98263e51f74382b0143328ee4826f20a76f47',
|
||||||
|
kind: 'videoinput' as MediaDeviceKind,
|
||||||
|
label: 'FaceTime HD Camera (Built-in) (9fba:bced)',
|
||||||
|
toJSON() {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId:
|
||||||
|
'e2db196a31d50ff9b135299dc0beea67f65b1a25a06d8a4ce76976751bb7a08d',
|
||||||
|
groupId:
|
||||||
|
'218ba7f00d7b1239cca15b9116769e5e7d30cc01104ebf84d667643661e0ecf9',
|
||||||
|
kind: 'videoinput' as MediaDeviceKind,
|
||||||
|
label: 'Logitech Webcam (4e72:9058)',
|
||||||
|
toJSON() {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedCamera =
|
||||||
|
'dfbe6effe70b0611ba0fdc2a9ea3f39f6cb110e6687948f7e5f016c111b7329c';
|
||||||
|
|
||||||
|
const props = createProps({
|
||||||
|
availableCameras,
|
||||||
|
availableMicrophones,
|
||||||
|
availableSpeakers,
|
||||||
|
selectedCamera,
|
||||||
|
selectedMicrophone,
|
||||||
|
selectedSpeaker,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <CallingDeviceSelection {...props} />;
|
||||||
|
});
|
192
ts/components/CallingDeviceSelection.tsx
Normal file
192
ts/components/CallingDeviceSelection.tsx
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { ConfirmationModal } from './ConfirmationModal';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import {
|
||||||
|
AudioDevice,
|
||||||
|
CallingDeviceType,
|
||||||
|
ChangeIODevicePayloadType,
|
||||||
|
MediaDeviceSettings,
|
||||||
|
} from '../types/Calling';
|
||||||
|
|
||||||
|
export type Props = MediaDeviceSettings & {
|
||||||
|
changeIODevice: (payload: ChangeIODevicePayloadType) => void;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
toggleSettings: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderAudioOptions(
|
||||||
|
devices: Array<AudioDevice>,
|
||||||
|
i18n: LocalizerType,
|
||||||
|
selectedDevice: AudioDevice | undefined
|
||||||
|
): JSX.Element {
|
||||||
|
if (!devices.length) {
|
||||||
|
return (
|
||||||
|
<option aria-selected={true}>
|
||||||
|
{i18n('callingDeviceSelection__select--no-device')}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{devices.map((device: AudioDevice) => {
|
||||||
|
const isSelected =
|
||||||
|
selectedDevice && selectedDevice.index === device.index;
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
aria-selected={isSelected}
|
||||||
|
key={device.index}
|
||||||
|
value={device.index}
|
||||||
|
>
|
||||||
|
{device.name}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVideoOptions(
|
||||||
|
devices: Array<MediaDeviceInfo>,
|
||||||
|
i18n: LocalizerType,
|
||||||
|
selectedCamera: string | undefined
|
||||||
|
): JSX.Element {
|
||||||
|
if (!devices.length) {
|
||||||
|
return (
|
||||||
|
<option aria-selected={true}>
|
||||||
|
{i18n('callingDeviceSelection__select--no-device')}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{devices.map((device: MediaDeviceInfo) => {
|
||||||
|
const isSelected = selectedCamera === device.deviceId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
aria-selected={isSelected}
|
||||||
|
key={device.deviceId}
|
||||||
|
value={device.deviceId}
|
||||||
|
>
|
||||||
|
{device.label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAudioChangeHandler(
|
||||||
|
devices: Array<AudioDevice>,
|
||||||
|
changeIODevice: (payload: ChangeIODevicePayloadType) => void,
|
||||||
|
type: CallingDeviceType.SPEAKER | CallingDeviceType.MICROPHONE
|
||||||
|
) {
|
||||||
|
return (ev: React.FormEvent<HTMLSelectElement>): void => {
|
||||||
|
changeIODevice({
|
||||||
|
type,
|
||||||
|
selectedDevice: devices[Number(ev.currentTarget.value)],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCameraChangeHandler(
|
||||||
|
changeIODevice: (payload: ChangeIODevicePayloadType) => void
|
||||||
|
) {
|
||||||
|
return (ev: React.FormEvent<HTMLSelectElement>): void => {
|
||||||
|
changeIODevice({
|
||||||
|
type: CallingDeviceType.CAMERA,
|
||||||
|
selectedDevice: String(ev.currentTarget.value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CallingDeviceSelection = ({
|
||||||
|
availableCameras,
|
||||||
|
availableMicrophones,
|
||||||
|
availableSpeakers,
|
||||||
|
changeIODevice,
|
||||||
|
i18n,
|
||||||
|
selectedCamera,
|
||||||
|
selectedMicrophone,
|
||||||
|
selectedSpeaker,
|
||||||
|
toggleSettings,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const selectedMicrophoneIndex = selectedMicrophone
|
||||||
|
? selectedMicrophone.index
|
||||||
|
: undefined;
|
||||||
|
const selectedSpeakerIndex = selectedSpeaker
|
||||||
|
? selectedSpeaker.index
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationModal actions={[]} i18n={i18n} onClose={toggleSettings}>
|
||||||
|
<div className="module-calling-device-selection">
|
||||||
|
<button
|
||||||
|
className="module-calling-device-selection__close-button"
|
||||||
|
onClick={toggleSettings}
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="module-calling-device-selection__title">
|
||||||
|
{i18n('callingDeviceSelection__settings')}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<label className="module-calling-device-selection__label">
|
||||||
|
{i18n('callingDeviceSelection__label--video')}
|
||||||
|
</label>
|
||||||
|
<div className="module-calling-device-selection__select">
|
||||||
|
<select
|
||||||
|
disabled={!availableCameras.length}
|
||||||
|
name="video"
|
||||||
|
// tslint:disable-next-line react-a11y-no-onchange
|
||||||
|
onChange={createCameraChangeHandler(changeIODevice)}
|
||||||
|
value={selectedCamera}
|
||||||
|
>
|
||||||
|
{renderVideoOptions(availableCameras, i18n, selectedCamera)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="module-calling-device-selection__label">
|
||||||
|
{i18n('callingDeviceSelection__label--audio-input')}
|
||||||
|
</label>
|
||||||
|
<div className="module-calling-device-selection__select">
|
||||||
|
<select
|
||||||
|
disabled={!availableMicrophones.length}
|
||||||
|
name="audio-input"
|
||||||
|
// tslint:disable-next-line react-a11y-no-onchange
|
||||||
|
onChange={createAudioChangeHandler(
|
||||||
|
availableMicrophones,
|
||||||
|
changeIODevice,
|
||||||
|
CallingDeviceType.MICROPHONE
|
||||||
|
)}
|
||||||
|
value={selectedMicrophoneIndex}
|
||||||
|
>
|
||||||
|
{renderAudioOptions(availableMicrophones, i18n, selectedMicrophone)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="module-calling-device-selection__label">
|
||||||
|
{i18n('callingDeviceSelection__label--audio-output')}
|
||||||
|
</label>
|
||||||
|
<div className="module-calling-device-selection__select">
|
||||||
|
<select
|
||||||
|
disabled={!availableSpeakers.length}
|
||||||
|
name="audio-output"
|
||||||
|
// tslint:disable-next-line react-a11y-no-onchange
|
||||||
|
onChange={createAudioChangeHandler(
|
||||||
|
availableSpeakers,
|
||||||
|
changeIODevice,
|
||||||
|
CallingDeviceType.SPEAKER
|
||||||
|
)}
|
||||||
|
value={selectedSpeakerIndex}
|
||||||
|
>
|
||||||
|
{renderAudioOptions(availableSpeakers, i18n, selectedSpeaker)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</ConfirmationModal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,12 +5,13 @@ import {
|
||||||
CallLogLevel,
|
CallLogLevel,
|
||||||
CallSettings,
|
CallSettings,
|
||||||
CallState,
|
CallState,
|
||||||
|
CanvasVideoRenderer,
|
||||||
DeviceId,
|
DeviceId,
|
||||||
|
GumVideoCapturer,
|
||||||
RingRTC,
|
RingRTC,
|
||||||
UserId,
|
UserId,
|
||||||
VideoCapturer,
|
|
||||||
VideoRenderer,
|
|
||||||
} from 'ringrtc';
|
} from 'ringrtc';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActionsType as UxActionsType,
|
ActionsType as UxActionsType,
|
||||||
CallDetailsType,
|
CallDetailsType,
|
||||||
|
@ -18,6 +19,7 @@ import {
|
||||||
import { CallingMessageClass, EnvelopeClass } from '../textsecure.d';
|
import { CallingMessageClass, EnvelopeClass } from '../textsecure.d';
|
||||||
import { ConversationModelType } from '../model-types.d';
|
import { ConversationModelType } from '../model-types.d';
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
|
import { AudioDevice, MediaDeviceSettings } from '../types/Calling';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
CallState,
|
CallState,
|
||||||
|
@ -36,7 +38,16 @@ export type CallHistoryDetailsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CallingClass {
|
export class CallingClass {
|
||||||
|
readonly videoCapturer: GumVideoCapturer;
|
||||||
|
readonly videoRenderer: CanvasVideoRenderer;
|
||||||
private uxActions?: UxActionsType;
|
private uxActions?: UxActionsType;
|
||||||
|
private lastMediaDeviceSettings?: MediaDeviceSettings;
|
||||||
|
private deviceReselectionTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.videoCapturer = new GumVideoCapturer(640, 480, 30);
|
||||||
|
this.videoRenderer = new CanvasVideoRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
initialize(uxActions: UxActionsType): void {
|
initialize(uxActions: UxActionsType): void {
|
||||||
this.uxActions = uxActions;
|
this.uxActions = uxActions;
|
||||||
|
@ -80,11 +91,6 @@ export class CallingClass {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RingRTC.call && RingRTC.call.state !== CallState.Ended) {
|
|
||||||
window.log.info('Call already in progress, new call not allowed.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteUserId = this.getRemoteUserIdFromConversation(conversation);
|
const remoteUserId = this.getRemoteUserIdFromConversation(conversation);
|
||||||
if (!remoteUserId || !this.localDeviceId) {
|
if (!remoteUserId || !this.localDeviceId) {
|
||||||
window.log.error('Missing identifier, new call not allowed.');
|
window.log.error('Missing identifier, new call not allowed.');
|
||||||
|
@ -97,15 +103,26 @@ export class CallingClass {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const callSettings = await this.getCallSettings(conversation);
|
||||||
|
|
||||||
|
// Check state after awaiting to debounce call button.
|
||||||
|
if (RingRTC.call && RingRTC.call.state !== CallState.Ended) {
|
||||||
|
window.log.info('Call already in progress, new call not allowed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We could make this faster by getting the call object
|
// We could make this faster by getting the call object
|
||||||
// from the RingRTC before we lookup the ICE servers.
|
// from the RingRTC before we lookup the ICE servers.
|
||||||
const call = RingRTC.startOutgoingCall(
|
const call = RingRTC.startOutgoingCall(
|
||||||
remoteUserId,
|
remoteUserId,
|
||||||
isVideoCall,
|
isVideoCall,
|
||||||
this.localDeviceId,
|
this.localDeviceId,
|
||||||
await this.getCallSettings(conversation)
|
callSettings
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.startDeviceReselectionTimer();
|
||||||
|
RingRTC.setVideoCapturer(call.callId, this.videoCapturer);
|
||||||
|
RingRTC.setVideoRenderer(call.callId, this.videoRenderer);
|
||||||
this.attachToCall(conversation, call);
|
this.attachToCall(conversation, call);
|
||||||
|
|
||||||
this.uxActions.outgoingCall({
|
this.uxActions.outgoingCall({
|
||||||
|
@ -116,6 +133,9 @@ export class CallingClass {
|
||||||
async accept(callId: CallId, asVideoCall: boolean) {
|
async accept(callId: CallId, asVideoCall: boolean) {
|
||||||
const haveMediaPermissions = await this.requestPermissions(asVideoCall);
|
const haveMediaPermissions = await this.requestPermissions(asVideoCall);
|
||||||
if (haveMediaPermissions) {
|
if (haveMediaPermissions) {
|
||||||
|
await this.startDeviceReselectionTimer();
|
||||||
|
RingRTC.setVideoCapturer(callId, this.videoCapturer);
|
||||||
|
RingRTC.setVideoRenderer(callId, this.videoRenderer);
|
||||||
RingRTC.accept(callId, asVideoCall);
|
RingRTC.accept(callId, asVideoCall);
|
||||||
} else {
|
} else {
|
||||||
window.log.info('Permissions were denied, call not allowed, hanging up.');
|
window.log.info('Permissions were denied, call not allowed, hanging up.');
|
||||||
|
@ -139,12 +159,230 @@ export class CallingClass {
|
||||||
RingRTC.setOutgoingVideo(callId, enabled);
|
RingRTC.setOutgoingVideo(callId, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
setVideoCapturer(callId: CallId, capturer: VideoCapturer | null) {
|
private async startDeviceReselectionTimer(): Promise<void> {
|
||||||
RingRTC.setVideoCapturer(callId, capturer);
|
// Poll once
|
||||||
|
await this.pollForMediaDevices();
|
||||||
|
// Start the timer
|
||||||
|
if (!this.deviceReselectionTimer) {
|
||||||
|
this.deviceReselectionTimer = setInterval(async () => {
|
||||||
|
await this.pollForMediaDevices();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setVideoRenderer(callId: CallId, renderer: VideoRenderer | null) {
|
private stopDeviceReselectionTimer() {
|
||||||
RingRTC.setVideoRenderer(callId, renderer);
|
if (this.deviceReselectionTimer) {
|
||||||
|
clearInterval(this.deviceReselectionTimer);
|
||||||
|
this.deviceReselectionTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line cyclomatic-complexity
|
||||||
|
private mediaDeviceSettingsEqual(
|
||||||
|
a?: MediaDeviceSettings,
|
||||||
|
b?: MediaDeviceSettings
|
||||||
|
): boolean {
|
||||||
|
if (!a && !b) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!a || !b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
a.availableCameras.length !== b.availableCameras.length ||
|
||||||
|
a.availableMicrophones.length !== b.availableMicrophones.length ||
|
||||||
|
a.availableSpeakers.length !== b.availableSpeakers.length
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < a.availableCameras.length; i++) {
|
||||||
|
if (
|
||||||
|
a.availableCameras[i].deviceId !== b.availableCameras[i].deviceId ||
|
||||||
|
a.availableCameras[i].groupId !== b.availableCameras[i].groupId ||
|
||||||
|
a.availableCameras[i].label !== b.availableCameras[i].label
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < a.availableMicrophones.length; i++) {
|
||||||
|
if (
|
||||||
|
a.availableMicrophones[i].name !== b.availableMicrophones[i].name ||
|
||||||
|
a.availableMicrophones[i].unique_id !==
|
||||||
|
b.availableMicrophones[i].unique_id ||
|
||||||
|
a.availableMicrophones[i].same_name_index !==
|
||||||
|
b.availableMicrophones[i].same_name_index
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < a.availableSpeakers.length; i++) {
|
||||||
|
if (
|
||||||
|
a.availableSpeakers[i].name !== b.availableSpeakers[i].name ||
|
||||||
|
a.availableSpeakers[i].unique_id !== b.availableSpeakers[i].unique_id ||
|
||||||
|
a.availableSpeakers[i].same_name_index !==
|
||||||
|
b.availableSpeakers[i].same_name_index
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(a.selectedCamera && !b.selectedCamera) ||
|
||||||
|
(!a.selectedCamera && b.selectedCamera) ||
|
||||||
|
(a.selectedMicrophone && !b.selectedMicrophone) ||
|
||||||
|
(!a.selectedMicrophone && b.selectedMicrophone) ||
|
||||||
|
(a.selectedSpeaker && !b.selectedSpeaker) ||
|
||||||
|
(!a.selectedSpeaker && b.selectedSpeaker)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
a.selectedCamera &&
|
||||||
|
b.selectedCamera &&
|
||||||
|
a.selectedCamera !== b.selectedCamera
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
a.selectedMicrophone &&
|
||||||
|
b.selectedMicrophone &&
|
||||||
|
a.selectedMicrophone.index !== b.selectedMicrophone.index
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
a.selectedSpeaker &&
|
||||||
|
b.selectedSpeaker &&
|
||||||
|
a.selectedSpeaker.index !== b.selectedSpeaker.index
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pollForMediaDevices(): Promise<void> {
|
||||||
|
const newSettings = await this.getMediaDeviceSettings();
|
||||||
|
if (
|
||||||
|
!this.mediaDeviceSettingsEqual(this.lastMediaDeviceSettings, newSettings)
|
||||||
|
) {
|
||||||
|
window.log.info(
|
||||||
|
'MediaDevice: available devices changed (from->to)',
|
||||||
|
this.lastMediaDeviceSettings,
|
||||||
|
newSettings
|
||||||
|
);
|
||||||
|
await this.selectPreferredMediaDevices(newSettings);
|
||||||
|
this.lastMediaDeviceSettings = newSettings;
|
||||||
|
this.uxActions?.refreshIODevices(newSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMediaDeviceSettings(): Promise<MediaDeviceSettings> {
|
||||||
|
const availableMicrophones = RingRTC.getAudioInputs();
|
||||||
|
const preferredMicrophone = window.storage.get(
|
||||||
|
'preferred-audio-input-device'
|
||||||
|
);
|
||||||
|
const selectedMicIndex = this.findBestMatchingDeviceIndex(
|
||||||
|
availableMicrophones,
|
||||||
|
preferredMicrophone
|
||||||
|
);
|
||||||
|
const selectedMicrophone =
|
||||||
|
selectedMicIndex !== undefined
|
||||||
|
? availableMicrophones[selectedMicIndex]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const availableSpeakers = RingRTC.getAudioOutputs();
|
||||||
|
const preferredSpeaker = window.storage.get(
|
||||||
|
'preferred-audio-output-device'
|
||||||
|
);
|
||||||
|
const selectedSpeakerIndex = this.findBestMatchingDeviceIndex(
|
||||||
|
availableSpeakers,
|
||||||
|
preferredSpeaker
|
||||||
|
);
|
||||||
|
const selectedSpeaker =
|
||||||
|
selectedSpeakerIndex !== undefined
|
||||||
|
? availableSpeakers[selectedSpeakerIndex]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const availableCameras = await window.Signal.Services.calling.videoCapturer.enumerateDevices();
|
||||||
|
const preferredCamera = window.storage.get('preferred-video-input-device');
|
||||||
|
const selectedCamera = this.findBestMatchingCamera(
|
||||||
|
availableCameras,
|
||||||
|
preferredCamera
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableMicrophones,
|
||||||
|
availableSpeakers,
|
||||||
|
selectedMicrophone,
|
||||||
|
selectedSpeaker,
|
||||||
|
availableCameras,
|
||||||
|
selectedCamera,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
findBestMatchingDeviceIndex(
|
||||||
|
available: Array<AudioDevice>,
|
||||||
|
preferred: AudioDevice | undefined
|
||||||
|
): number | undefined {
|
||||||
|
if (!preferred) {
|
||||||
|
// No preference stored
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// Match by UUID first, if available
|
||||||
|
if (preferred.unique_id) {
|
||||||
|
const matchIndex = available.findIndex(
|
||||||
|
d => d.unique_id === preferred.unique_id
|
||||||
|
);
|
||||||
|
if (matchIndex !== -1) {
|
||||||
|
return matchIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match by name second, and if there are multiple such names - by instance index.
|
||||||
|
const matchingNames = available.filter(d => d.name === preferred.name);
|
||||||
|
if (matchingNames.length > preferred.same_name_index) {
|
||||||
|
return matchingNames[preferred.same_name_index].index;
|
||||||
|
}
|
||||||
|
if (matchingNames.length > 0) {
|
||||||
|
return matchingNames[0].index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing matches; take the first device if there are any
|
||||||
|
return available.length > 0 ? 0 : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
findBestMatchingCamera(
|
||||||
|
available: Array<MediaDeviceInfo>,
|
||||||
|
preferred?: string
|
||||||
|
): string | undefined {
|
||||||
|
const matchingId = available.filter(d => d.deviceId === preferred);
|
||||||
|
const nonInfrared = available.filter(d => !d.label.includes('IR Camera'));
|
||||||
|
|
||||||
|
/// By default, pick the first non-IR camera (but allow the user to pick the infrared if they so desire)
|
||||||
|
if (matchingId.length > 0) {
|
||||||
|
return matchingId[0].deviceId;
|
||||||
|
} else if (nonInfrared.length > 0) {
|
||||||
|
return nonInfrared[0].deviceId;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreferredMicrophone(device: AudioDevice) {
|
||||||
|
window.log.info('MediaDevice: setPreferredMicrophone', device);
|
||||||
|
window.storage.put('preferred-audio-input-device', device);
|
||||||
|
RingRTC.setAudioInput(device.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreferredSpeaker(device: AudioDevice) {
|
||||||
|
window.log.info('MediaDevice: setPreferredSpeaker', device);
|
||||||
|
window.storage.put('preferred-audio-output-device', device);
|
||||||
|
RingRTC.setAudioOutput(device.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPreferredCamera(device: string) {
|
||||||
|
window.log.info('MediaDevice: setPreferredCamera', device);
|
||||||
|
window.storage.put('preferred-video-input-device', device);
|
||||||
|
await this.videoCapturer.setPreferredDevice(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleCallingMessage(
|
async handleCallingMessage(
|
||||||
|
@ -176,6 +414,30 @@ export class CallingClass {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async selectPreferredMediaDevices(
|
||||||
|
settings: MediaDeviceSettings
|
||||||
|
): Promise<void> {
|
||||||
|
// Assume that the MediaDeviceSettings have been obtained very recently and the index is still valid (no devices have been plugged in in between).
|
||||||
|
if (settings.selectedMicrophone) {
|
||||||
|
window.log.info(
|
||||||
|
'MediaDevice: selecting microphone',
|
||||||
|
settings.selectedMicrophone
|
||||||
|
);
|
||||||
|
RingRTC.setAudioInput(settings.selectedMicrophone.index);
|
||||||
|
}
|
||||||
|
if (settings.selectedSpeaker) {
|
||||||
|
window.log.info(
|
||||||
|
'MediaDevice: selecting speaker',
|
||||||
|
settings.selectedMicrophone
|
||||||
|
);
|
||||||
|
RingRTC.setAudioOutput(settings.selectedSpeaker.index);
|
||||||
|
}
|
||||||
|
if (settings.selectedCamera) {
|
||||||
|
window.log.info('MediaDevice: selecting camera', settings.selectedCamera);
|
||||||
|
await this.videoCapturer.setPreferredDevice(settings.selectedCamera);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async requestCameraPermissions(): Promise<boolean> {
|
private async requestCameraPermissions(): Promise<boolean> {
|
||||||
const cameraPermission = await window.getMediaCameraPermissions();
|
const cameraPermission = await window.getMediaCameraPermissions();
|
||||||
if (!cameraPermission) {
|
if (!cameraPermission) {
|
||||||
|
@ -325,6 +587,8 @@ export class CallingClass {
|
||||||
acceptedTime = Date.now();
|
acceptedTime = Date.now();
|
||||||
} else if (call.state === CallState.Ended) {
|
} else if (call.state === CallState.Ended) {
|
||||||
this.addCallHistoryForEndedCall(conversation, call, acceptedTime);
|
this.addCallHistoryForEndedCall(conversation, call, acceptedTime);
|
||||||
|
this.stopDeviceReselectionTimer();
|
||||||
|
this.lastMediaDeviceSettings = undefined;
|
||||||
}
|
}
|
||||||
uxActions.callStateChange({
|
uxActions.callStateChange({
|
||||||
callState: call.state,
|
callState: call.state,
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { notify } from '../../services/notify';
|
import { notify } from '../../services/notify';
|
||||||
import { calling, VideoCapturer, VideoRenderer } from '../../services/calling';
|
import { calling } from '../../services/calling';
|
||||||
import { CallState } from '../../types/Calling';
|
import {
|
||||||
import { CanvasVideoRenderer, GumVideoCapturer } from '../../window.d';
|
CallingDeviceType,
|
||||||
|
CallState,
|
||||||
|
ChangeIODevicePayloadType,
|
||||||
|
MediaDeviceSettings,
|
||||||
|
} from '../../types/Calling';
|
||||||
import { ColorType } from '../../types/Colors';
|
import { ColorType } from '../../types/Colors';
|
||||||
import { NoopActionType } from './noop';
|
import { NoopActionType } from './noop';
|
||||||
import { callingTones } from '../../util/callingTones';
|
import { callingTones } from '../../util/callingTones';
|
||||||
|
@ -28,12 +32,13 @@ export type CallDetailsType = {
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CallingStateType = {
|
export type CallingStateType = MediaDeviceSettings & {
|
||||||
callDetails?: CallDetailsType;
|
callDetails?: CallDetailsType;
|
||||||
callState?: CallState;
|
callState?: CallState;
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
|
settingsDialogOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AcceptCallType = {
|
export type AcceptCallType = {
|
||||||
|
@ -76,14 +81,12 @@ export type SetLocalVideoType = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SetVideoCapturerType = {
|
export type SetLocalPreviewType = {
|
||||||
callId: CallId;
|
element: React.RefObject<HTMLVideoElement> | undefined;
|
||||||
capturer: CanvasVideoRenderer | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SetVideoRendererType = {
|
export type SetRendererCanvasType = {
|
||||||
callId: CallId;
|
element: React.RefObject<HTMLCanvasElement> | undefined;
|
||||||
renderer: GumVideoCapturer | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
@ -91,14 +94,18 @@ export type SetVideoRendererType = {
|
||||||
const ACCEPT_CALL = 'calling/ACCEPT_CALL';
|
const ACCEPT_CALL = 'calling/ACCEPT_CALL';
|
||||||
const CALL_STATE_CHANGE = 'calling/CALL_STATE_CHANGE';
|
const CALL_STATE_CHANGE = 'calling/CALL_STATE_CHANGE';
|
||||||
const CALL_STATE_CHANGE_FULFILLED = 'calling/CALL_STATE_CHANGE_FULFILLED';
|
const CALL_STATE_CHANGE_FULFILLED = 'calling/CALL_STATE_CHANGE_FULFILLED';
|
||||||
|
const CHANGE_IO_DEVICE = 'calling/CHANGE_IO_DEVICE';
|
||||||
|
const CHANGE_IO_DEVICE_FULFILLED = 'calling/CHANGE_IO_DEVICE_FULFILLED';
|
||||||
const DECLINE_CALL = 'calling/DECLINE_CALL';
|
const DECLINE_CALL = 'calling/DECLINE_CALL';
|
||||||
const HANG_UP = 'calling/HANG_UP';
|
const HANG_UP = 'calling/HANG_UP';
|
||||||
const INCOMING_CALL = 'calling/INCOMING_CALL';
|
const INCOMING_CALL = 'calling/INCOMING_CALL';
|
||||||
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
||||||
|
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
|
||||||
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||||
const SET_LOCAL_AUDIO = 'calling/SET_LOCAL_AUDIO';
|
const SET_LOCAL_AUDIO = 'calling/SET_LOCAL_AUDIO';
|
||||||
const SET_LOCAL_VIDEO = 'calling/SET_LOCAL_VIDEO';
|
const SET_LOCAL_VIDEO = 'calling/SET_LOCAL_VIDEO';
|
||||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||||
|
const TOGGLE_SETTINGS = 'calling/TOGGLE_SETTINGS';
|
||||||
|
|
||||||
type AcceptCallActionType = {
|
type AcceptCallActionType = {
|
||||||
type: 'calling/ACCEPT_CALL';
|
type: 'calling/ACCEPT_CALL';
|
||||||
|
@ -115,6 +122,16 @@ type CallStateChangeFulfilledActionType = {
|
||||||
payload: CallStateChangeType;
|
payload: CallStateChangeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ChangeIODeviceActionType = {
|
||||||
|
type: 'calling/CHANGE_IO_DEVICE';
|
||||||
|
payload: Promise<ChangeIODevicePayloadType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChangeIODeviceFulfilledActionType = {
|
||||||
|
type: 'calling/CHANGE_IO_DEVICE_FULFILLED';
|
||||||
|
payload: ChangeIODevicePayloadType;
|
||||||
|
};
|
||||||
|
|
||||||
type DeclineCallActionType = {
|
type DeclineCallActionType = {
|
||||||
type: 'calling/DECLINE_CALL';
|
type: 'calling/DECLINE_CALL';
|
||||||
payload: DeclineCallType;
|
payload: DeclineCallType;
|
||||||
|
@ -135,6 +152,11 @@ type OutgoingCallActionType = {
|
||||||
payload: OutgoingCallType;
|
payload: OutgoingCallType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RefreshIODevicesActionType = {
|
||||||
|
type: 'calling/REFRESH_IO_DEVICES';
|
||||||
|
payload: MediaDeviceSettings;
|
||||||
|
};
|
||||||
|
|
||||||
type RemoteVideoChangeActionType = {
|
type RemoteVideoChangeActionType = {
|
||||||
type: 'calling/REMOTE_VIDEO_CHANGE';
|
type: 'calling/REMOTE_VIDEO_CHANGE';
|
||||||
payload: RemoteVideoChangeType;
|
payload: RemoteVideoChangeType;
|
||||||
|
@ -155,18 +177,26 @@ type SetLocalVideoFulfilledActionType = {
|
||||||
payload: SetLocalVideoType;
|
payload: SetLocalVideoType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ToggleSettingsActionType = {
|
||||||
|
type: 'calling/TOGGLE_SETTINGS';
|
||||||
|
};
|
||||||
|
|
||||||
export type CallingActionType =
|
export type CallingActionType =
|
||||||
| AcceptCallActionType
|
| AcceptCallActionType
|
||||||
| CallStateChangeActionType
|
| CallStateChangeActionType
|
||||||
| CallStateChangeFulfilledActionType
|
| CallStateChangeFulfilledActionType
|
||||||
|
| ChangeIODeviceActionType
|
||||||
|
| ChangeIODeviceFulfilledActionType
|
||||||
| DeclineCallActionType
|
| DeclineCallActionType
|
||||||
| HangUpActionType
|
| HangUpActionType
|
||||||
| IncomingCallActionType
|
| IncomingCallActionType
|
||||||
| OutgoingCallActionType
|
| OutgoingCallActionType
|
||||||
|
| RefreshIODevicesActionType
|
||||||
| RemoteVideoChangeActionType
|
| RemoteVideoChangeActionType
|
||||||
| SetLocalAudioActionType
|
| SetLocalAudioActionType
|
||||||
| SetLocalVideoActionType
|
| SetLocalVideoActionType
|
||||||
| SetLocalVideoFulfilledActionType;
|
| SetLocalVideoFulfilledActionType
|
||||||
|
| ToggleSettingsActionType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
|
@ -197,6 +227,29 @@ function callStateChange(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeIODevice(
|
||||||
|
payload: ChangeIODevicePayloadType
|
||||||
|
): ChangeIODeviceActionType {
|
||||||
|
return {
|
||||||
|
type: CHANGE_IO_DEVICE,
|
||||||
|
payload: doChangeIODevice(payload),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doChangeIODevice(
|
||||||
|
payload: ChangeIODevicePayloadType
|
||||||
|
): Promise<ChangeIODevicePayloadType> {
|
||||||
|
if (payload.type === CallingDeviceType.CAMERA) {
|
||||||
|
await calling.setPreferredCamera(payload.selectedDevice);
|
||||||
|
} else if (payload.type === CallingDeviceType.MICROPHONE) {
|
||||||
|
calling.setPreferredMicrophone(payload.selectedDevice);
|
||||||
|
} else if (payload.type === CallingDeviceType.SPEAKER) {
|
||||||
|
calling.setPreferredSpeaker(payload.selectedDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
async function doCallStateChange(
|
async function doCallStateChange(
|
||||||
payload: CallStateChangeType
|
payload: CallStateChangeType
|
||||||
): Promise<CallStateChangeType> {
|
): Promise<CallStateChangeType> {
|
||||||
|
@ -208,12 +261,11 @@ async function doCallStateChange(
|
||||||
bounceAppIconStart();
|
bounceAppIconStart();
|
||||||
}
|
}
|
||||||
if (callState !== CallState.Ringing) {
|
if (callState !== CallState.Ringing) {
|
||||||
callingTones.stopRingtone();
|
await callingTones.stopRingtone();
|
||||||
bounceAppIconStop();
|
bounceAppIconStop();
|
||||||
}
|
}
|
||||||
if (callState === CallState.Ended) {
|
if (callState === CallState.Ended) {
|
||||||
// tslint:disable-next-line no-floating-promises
|
await callingTones.playEndCall();
|
||||||
callingTones.playEndCall();
|
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
@ -275,6 +327,15 @@ function outgoingCall(payload: OutgoingCallType): OutgoingCallActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshIODevices(
|
||||||
|
payload: MediaDeviceSettings
|
||||||
|
): RefreshIODevicesActionType {
|
||||||
|
return {
|
||||||
|
type: REFRESH_IO_DEVICES,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function remoteVideoChange(
|
function remoteVideoChange(
|
||||||
payload: RemoteVideoChangeType
|
payload: RemoteVideoChangeType
|
||||||
): RemoteVideoChangeActionType {
|
): RemoteVideoChangeActionType {
|
||||||
|
@ -284,8 +345,8 @@ function remoteVideoChange(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVideoCapturer(payload: SetVideoCapturerType): NoopActionType {
|
function setLocalPreview(payload: SetLocalPreviewType): NoopActionType {
|
||||||
calling.setVideoCapturer(payload.callId, payload.capturer as VideoCapturer);
|
calling.videoCapturer.setLocalPreview(payload.element);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'NOOP',
|
type: 'NOOP',
|
||||||
|
@ -293,8 +354,8 @@ function setVideoCapturer(payload: SetVideoCapturerType): NoopActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVideoRenderer(payload: SetVideoRendererType): NoopActionType {
|
function setRendererCanvas(payload: SetRendererCanvasType): NoopActionType {
|
||||||
calling.setVideoRenderer(payload.callId, payload.renderer as VideoRenderer);
|
calling.videoRenderer.setCanvas(payload.element);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'NOOP',
|
type: 'NOOP',
|
||||||
|
@ -318,6 +379,12 @@ function setLocalVideo(payload: SetLocalVideoType): SetLocalVideoActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSettings(): ToggleSettingsActionType {
|
||||||
|
return {
|
||||||
|
type: TOGGLE_SETTINGS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function doSetLocalVideo(
|
async function doSetLocalVideo(
|
||||||
payload: SetLocalVideoType
|
payload: SetLocalVideoType
|
||||||
): Promise<SetLocalVideoType> {
|
): Promise<SetLocalVideoType> {
|
||||||
|
@ -335,15 +402,18 @@ async function doSetLocalVideo(
|
||||||
export const actions = {
|
export const actions = {
|
||||||
acceptCall,
|
acceptCall,
|
||||||
callStateChange,
|
callStateChange,
|
||||||
|
changeIODevice,
|
||||||
declineCall,
|
declineCall,
|
||||||
hangUp,
|
hangUp,
|
||||||
incomingCall,
|
incomingCall,
|
||||||
outgoingCall,
|
outgoingCall,
|
||||||
|
refreshIODevices,
|
||||||
remoteVideoChange,
|
remoteVideoChange,
|
||||||
setVideoCapturer,
|
setLocalPreview,
|
||||||
setVideoRenderer,
|
setRendererCanvas,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
|
toggleSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActionsType = typeof actions;
|
export type ActionsType = typeof actions;
|
||||||
|
@ -352,14 +422,22 @@ export type ActionsType = typeof actions;
|
||||||
|
|
||||||
function getEmptyState(): CallingStateType {
|
function getEmptyState(): CallingStateType {
|
||||||
return {
|
return {
|
||||||
|
availableCameras: [],
|
||||||
|
availableMicrophones: [],
|
||||||
|
availableSpeakers: [],
|
||||||
callDetails: undefined,
|
callDetails: undefined,
|
||||||
callState: undefined,
|
callState: undefined,
|
||||||
hasLocalAudio: false,
|
hasLocalAudio: false,
|
||||||
hasLocalVideo: false,
|
hasLocalVideo: false,
|
||||||
hasRemoteVideo: false,
|
hasRemoteVideo: false,
|
||||||
|
selectedCamera: undefined,
|
||||||
|
selectedMicrophone: undefined,
|
||||||
|
selectedSpeaker: undefined,
|
||||||
|
settingsDialogOpen: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: CallingStateType = getEmptyState(),
|
state: CallingStateType = getEmptyState(),
|
||||||
action: CallingActionType
|
action: CallingActionType
|
||||||
|
@ -425,5 +503,51 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === CHANGE_IO_DEVICE_FULFILLED) {
|
||||||
|
const { selectedDevice } = action.payload;
|
||||||
|
const nextState = Object.create(null);
|
||||||
|
|
||||||
|
if (action.payload.type === CallingDeviceType.CAMERA) {
|
||||||
|
nextState.selectedCamera = selectedDevice;
|
||||||
|
} else if (action.payload.type === CallingDeviceType.MICROPHONE) {
|
||||||
|
nextState.selectedMicrophone = selectedDevice;
|
||||||
|
} else if (action.payload.type === CallingDeviceType.SPEAKER) {
|
||||||
|
nextState.selectedSpeaker = selectedDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...nextState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === REFRESH_IO_DEVICES) {
|
||||||
|
const {
|
||||||
|
availableMicrophones,
|
||||||
|
selectedMicrophone,
|
||||||
|
availableSpeakers,
|
||||||
|
selectedSpeaker,
|
||||||
|
availableCameras,
|
||||||
|
selectedCamera,
|
||||||
|
} = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
availableMicrophones,
|
||||||
|
selectedMicrophone,
|
||||||
|
availableSpeakers,
|
||||||
|
selectedSpeaker,
|
||||||
|
availableCameras,
|
||||||
|
selectedCamera,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === TOGGLE_SETTINGS) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
settingsDialogOpen: !state.settingsDialogOpen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,26 @@
|
||||||
import { RefObject } from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { CanvasVideoRenderer, GumVideoCapturer } from 'ringrtc';
|
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import { CallManager } from '../../components/CallManager';
|
import { CallManager } from '../../components/CallManager';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
|
||||||
|
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
|
||||||
|
|
||||||
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
const FilteredCallingDeviceSelection = SmartCallingDeviceSelection as any;
|
||||||
|
|
||||||
|
function renderDeviceSelection(): JSX.Element {
|
||||||
|
return <FilteredCallingDeviceSelection />;
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType) => {
|
const mapStateToProps = (state: StateType) => {
|
||||||
return {
|
return {
|
||||||
...state.calling,
|
...state.calling,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
getVideoCapturer: (localVideoRef: RefObject<HTMLVideoElement>) =>
|
renderDeviceSelection,
|
||||||
new GumVideoCapturer(640, 480, 30, localVideoRef),
|
|
||||||
getVideoRenderer: (remoteVideoRef: RefObject<HTMLCanvasElement>) =>
|
|
||||||
new CanvasVideoRenderer(remoteVideoRef),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
31
ts/state/smart/CallingDeviceSelection.tsx
Normal file
31
ts/state/smart/CallingDeviceSelection.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { CallingDeviceSelection } from '../../components/CallingDeviceSelection';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType) => {
|
||||||
|
const {
|
||||||
|
availableMicrophones,
|
||||||
|
availableSpeakers,
|
||||||
|
selectedMicrophone,
|
||||||
|
selectedSpeaker,
|
||||||
|
availableCameras,
|
||||||
|
selectedCamera,
|
||||||
|
} = state.calling;
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableCameras,
|
||||||
|
availableMicrophones,
|
||||||
|
availableSpeakers,
|
||||||
|
i18n: getIntl(state),
|
||||||
|
selectedCamera,
|
||||||
|
selectedMicrophone,
|
||||||
|
selectedSpeaker,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartCallingDeviceSelection = smart(CallingDeviceSelection);
|
|
@ -1,3 +1,15 @@
|
||||||
|
// Must be kept in sync with RingRTC.AudioDevice
|
||||||
|
export interface AudioDevice {
|
||||||
|
// Name, present on every platform.
|
||||||
|
name: string;
|
||||||
|
// Index of this device, starting from 0.
|
||||||
|
index: number;
|
||||||
|
// Index of this device out of all devices sharing the same name.
|
||||||
|
same_name_index: number;
|
||||||
|
// If present, a unique and stable identifier of this device. Only available on WIndows.
|
||||||
|
unique_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// This must be kept in sync with RingRTC.CallState.
|
// This must be kept in sync with RingRTC.CallState.
|
||||||
export enum CallState {
|
export enum CallState {
|
||||||
Prering = 'init',
|
Prering = 'init',
|
||||||
|
@ -6,3 +18,23 @@ export enum CallState {
|
||||||
Reconnecting = 'connecting',
|
Reconnecting = 'connecting',
|
||||||
Ended = 'ended',
|
Ended = 'ended',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CallingDeviceType {
|
||||||
|
CAMERA,
|
||||||
|
MICROPHONE,
|
||||||
|
SPEAKER,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MediaDeviceSettings = {
|
||||||
|
availableMicrophones: Array<AudioDevice>;
|
||||||
|
selectedMicrophone: AudioDevice | undefined;
|
||||||
|
availableSpeakers: Array<AudioDevice>;
|
||||||
|
selectedSpeaker: AudioDevice | undefined;
|
||||||
|
availableCameras: Array<MediaDeviceInfo>;
|
||||||
|
selectedCamera: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChangeIODevicePayloadType =
|
||||||
|
| { type: CallingDeviceType.CAMERA; selectedDevice: string }
|
||||||
|
| { type: CallingDeviceType.MICROPHONE; selectedDevice: AudioDevice }
|
||||||
|
| { type: CallingDeviceType.SPEAKER; selectedDevice: AudioDevice };
|
||||||
|
|
|
@ -1,43 +1,51 @@
|
||||||
import { Sound, SoundOpts } from './Sound';
|
import { Sound } from './Sound';
|
||||||
|
import PQueue from 'p-queue';
|
||||||
|
|
||||||
async function playSound(howlProps: SoundOpts): Promise<Sound | undefined> {
|
const ringtoneEventQueue = new PQueue({ concurrency: 1 });
|
||||||
const canPlayTone = await window.getCallRingtoneNotification();
|
|
||||||
|
|
||||||
if (!canPlayTone) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tone = new Sound(howlProps);
|
|
||||||
await tone.play();
|
|
||||||
|
|
||||||
return tone;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CallingTones {
|
class CallingTones {
|
||||||
private ringtone?: Sound;
|
private ringtone?: Sound;
|
||||||
|
|
||||||
async playEndCall() {
|
async playEndCall() {
|
||||||
await playSound({
|
const canPlayTone = await window.getCallRingtoneNotification();
|
||||||
|
if (!canPlayTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tone = new Sound({
|
||||||
src: 'sounds/navigation-cancel.ogg',
|
src: 'sounds/navigation-cancel.ogg',
|
||||||
});
|
});
|
||||||
|
await tone.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
async playRingtone() {
|
async playRingtone() {
|
||||||
if (this.ringtone) {
|
await ringtoneEventQueue.add(async () => {
|
||||||
this.stopRingtone();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ringtone = await playSound({
|
|
||||||
loop: true,
|
|
||||||
src: 'sounds/ringtone_minimal.ogg',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
stopRingtone() {
|
|
||||||
if (this.ringtone) {
|
if (this.ringtone) {
|
||||||
this.ringtone.stop();
|
this.ringtone.stop();
|
||||||
this.ringtone = undefined;
|
this.ringtone = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canPlayTone = await window.getCallRingtoneNotification();
|
||||||
|
if (!canPlayTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ringtone = new Sound({
|
||||||
|
loop: true,
|
||||||
|
src: 'sounds/ringtone_minimal.ogg',
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.ringtone.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopRingtone() {
|
||||||
|
await ringtoneEventQueue.add(async () => {
|
||||||
|
if (this.ringtone) {
|
||||||
|
this.ringtone.stop();
|
||||||
|
this.ringtone = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11304,7 +11304,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/CallScreen.tsx",
|
"path": "ts/components/CallScreen.tsx",
|
||||||
"line": " this.localVideoRef = React.createRef();",
|
"line": " this.localVideoRef = React.createRef();",
|
||||||
"lineNumber": 80,
|
"lineNumber": 74,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-06-02T21:51:34.813Z",
|
"updated": "2020-06-02T21:51:34.813Z",
|
||||||
"reasonDetail": "Used to render local preview video"
|
"reasonDetail": "Used to render local preview video"
|
||||||
|
|
|
@ -14923,9 +14923,9 @@ rimraf@~2.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^6.0.1"
|
glob "^6.0.1"
|
||||||
|
|
||||||
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#de96fa13f04e846bdcb0ae9be8e2dc80a932f2be":
|
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#0cd60f529d8f17734fe19e7195c9fd3c3f4271db":
|
||||||
version "2.4.2"
|
version "2.5.0"
|
||||||
resolved "https://github.com/signalapp/signal-ringrtc-node.git#de96fa13f04e846bdcb0ae9be8e2dc80a932f2be"
|
resolved "https://github.com/signalapp/signal-ringrtc-node.git#0cd60f529d8f17734fe19e7195c9fd3c3f4271db"
|
||||||
|
|
||||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue