Calling: Device Selection
This commit is contained in:
parent
8b34294c97
commit
8ab1013f70
17 changed files with 1038 additions and 135 deletions
|
@ -31,17 +31,18 @@ const defaultProps = {
|
|||
callDetails,
|
||||
callState: CallState.Accepted,
|
||||
declineCall: action('decline-call'),
|
||||
getVideoCapturer: () => ({}),
|
||||
getVideoRenderer: () => ({}),
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
hasRemoteVideo: true,
|
||||
i18n,
|
||||
renderDeviceSelection: () => <div />,
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
setVideoCapturer: action('set-video-capturer'),
|
||||
setVideoRenderer: action('set-video-renderer'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
settingsDialogOpen: false,
|
||||
toggleSettings: action('toggle-settings'),
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
|
|
|
@ -10,7 +10,10 @@ import { CallDetailsType } from '../state/ducks/calling';
|
|||
type CallManagerPropsType = {
|
||||
callDetails?: CallDetailsType;
|
||||
callState?: CallState;
|
||||
renderDeviceSelection: () => JSX.Element;
|
||||
settingsDialogOpen: boolean;
|
||||
};
|
||||
|
||||
type PropsType = IncomingCallBarPropsType &
|
||||
CallScreenPropsType &
|
||||
CallManagerPropsType;
|
||||
|
@ -20,17 +23,18 @@ export const CallManager = ({
|
|||
callDetails,
|
||||
callState,
|
||||
declineCall,
|
||||
getVideoCapturer,
|
||||
getVideoRenderer,
|
||||
hangUp,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
hasRemoteVideo,
|
||||
i18n,
|
||||
renderDeviceSelection,
|
||||
setLocalAudio,
|
||||
setLocalPreview,
|
||||
setLocalVideo,
|
||||
setVideoCapturer,
|
||||
setVideoRenderer,
|
||||
setRendererCanvas,
|
||||
settingsDialogOpen,
|
||||
toggleSettings,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (!callDetails || !callState) {
|
||||
return null;
|
||||
|
@ -43,21 +47,23 @@ export const CallManager = ({
|
|||
|
||||
if (outgoing || ongoing) {
|
||||
return (
|
||||
<CallScreen
|
||||
callDetails={callDetails}
|
||||
callState={callState}
|
||||
getVideoCapturer={getVideoCapturer}
|
||||
getVideoRenderer={getVideoRenderer}
|
||||
hangUp={hangUp}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
setVideoCapturer={setVideoCapturer}
|
||||
setVideoRenderer={setVideoRenderer}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
/>
|
||||
<>
|
||||
<CallScreen
|
||||
callDetails={callDetails}
|
||||
callState={callState}
|
||||
hangUp={hangUp}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
setLocalPreview={setLocalPreview}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
{settingsDialogOpen && renderDeviceSelection()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,17 +30,16 @@ const callDetails = {
|
|||
const defaultProps = {
|
||||
callDetails,
|
||||
callState: CallState.Accepted,
|
||||
getVideoCapturer: () => ({}),
|
||||
getVideoRenderer: () => ({}),
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
hasRemoteVideo: true,
|
||||
i18n,
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
setVideoCapturer: action('set-video-capturer'),
|
||||
setVideoRenderer: action('set-video-renderer'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
|
|
|
@ -4,14 +4,13 @@ import {
|
|||
CallDetailsType,
|
||||
HangUpType,
|
||||
SetLocalAudioType,
|
||||
SetLocalPreviewType,
|
||||
SetLocalVideoType,
|
||||
SetVideoCapturerType,
|
||||
SetVideoRendererType,
|
||||
SetRendererCanvasType,
|
||||
} 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;
|
||||
|
@ -37,12 +36,6 @@ const CallingButton = ({
|
|||
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;
|
||||
|
@ -50,8 +43,9 @@ export type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setVideoCapturer: (_: SetVideoCapturerType) => void;
|
||||
setVideoRenderer: (_: SetVideoRendererType) => void;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
toggleSettings: () => void;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
|
@ -79,11 +73,6 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
this.controlsFadeTimer = null;
|
||||
this.localVideoRef = React.createRef();
|
||||
this.remoteVideoRef = React.createRef();
|
||||
|
||||
this.setVideoCapturerAndRenderer(
|
||||
props.getVideoCapturer(this.localVideoRef),
|
||||
props.getVideoRenderer(this.remoteVideoRef)
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -92,6 +81,9 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
this.fadeControls();
|
||||
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
this.props.setLocalPreview({ element: this.localVideoRef });
|
||||
this.props.setRendererCanvas({ element: this.remoteVideoRef });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -103,7 +95,8 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
if (this.controlsFadeTimer) {
|
||||
clearTimeout(this.controlsFadeTimer);
|
||||
}
|
||||
this.setVideoCapturerAndRenderer(null, null);
|
||||
this.props.setLocalPreview({ element: undefined });
|
||||
this.props.setRendererCanvas({ element: undefined });
|
||||
}
|
||||
|
||||
updateAcceptedTimer = () => {
|
||||
|
@ -203,6 +196,8 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
hasRemoteVideo,
|
||||
i18n,
|
||||
toggleSettings,
|
||||
} = this.props;
|
||||
const { showControls } = this.state;
|
||||
const isAudioOnly = !hasLocalVideo && !hasRemoteVideo;
|
||||
|
@ -241,6 +236,13 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
{callDetails.title}
|
||||
</div>
|
||||
{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>
|
||||
{hasRemoteVideo
|
||||
? this.renderRemoteVideo()
|
||||
|
@ -356,27 +358,4 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
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>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue