Add input device selection during voice broadcast (#9620)
This commit is contained in:
parent
5b5c3ab33c
commit
b302275289
12 changed files with 486 additions and 87 deletions
56
src/components/views/audio_messages/DevicesContextMenu.tsx
Normal file
56
src/components/views/audio_messages/DevicesContextMenu.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { MutableRefObject } from "react";
|
||||
|
||||
import { toLeftOrRightOf } from "../../structures/ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
|
||||
interface Props {
|
||||
containerRef: MutableRefObject<HTMLElement | null>;
|
||||
currentDevice: MediaDeviceInfo | null;
|
||||
devices: MediaDeviceInfo[];
|
||||
onDeviceSelect: (device: MediaDeviceInfo) => void;
|
||||
}
|
||||
|
||||
export const DevicesContextMenu: React.FC<Props> = ({
|
||||
containerRef,
|
||||
currentDevice,
|
||||
devices,
|
||||
onDeviceSelect,
|
||||
}) => {
|
||||
const deviceOptions = devices.map((d: MediaDeviceInfo) => {
|
||||
return <IconizedContextMenuRadio
|
||||
key={d.deviceId}
|
||||
active={d.deviceId === currentDevice?.deviceId}
|
||||
onClick={() => onDeviceSelect(d)}
|
||||
label={d.label}
|
||||
/>;
|
||||
});
|
||||
|
||||
return <IconizedContextMenu
|
||||
mountAsChild={false}
|
||||
onFinished={() => {}}
|
||||
{...toLeftOrRightOf(containerRef.current.getBoundingClientRect(), 0)}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ deviceOptions }
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
};
|
76
src/hooks/useAudioDeviceSelection.ts
Normal file
76
src/hooks/useAudioDeviceSelection.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { _t } from "../languageHandler";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
|
||||
import { requestMediaPermissions } from "../utils/media/requestMediaPermissions";
|
||||
|
||||
interface State {
|
||||
devices: MediaDeviceInfo[];
|
||||
device: MediaDeviceInfo | null;
|
||||
}
|
||||
|
||||
export const useAudioDeviceSelection = (
|
||||
onDeviceChanged?: (device: MediaDeviceInfo) => void,
|
||||
) => {
|
||||
const shouldRequestPermissionsRef = useRef<boolean>(true);
|
||||
const [state, setState] = useState<State>({
|
||||
devices: [],
|
||||
device: null,
|
||||
});
|
||||
|
||||
if (shouldRequestPermissionsRef.current) {
|
||||
shouldRequestPermissionsRef.current = false;
|
||||
requestMediaPermissions(false).then((stream: MediaStream | undefined) => {
|
||||
MediaDeviceHandler.getDevices().then(({ audioinput }) => {
|
||||
MediaDeviceHandler.getDefaultDevice(audioinput);
|
||||
const deviceFromSettings = MediaDeviceHandler.getAudioInput();
|
||||
const device = audioinput.find((d) => {
|
||||
return d.deviceId === deviceFromSettings;
|
||||
}) || audioinput[0];
|
||||
setState({
|
||||
...state,
|
||||
devices: audioinput,
|
||||
device,
|
||||
});
|
||||
stream?.getTracks().forEach(t => t.stop());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const setDevice = (device: MediaDeviceInfo) => {
|
||||
const shouldNotify = device.deviceId !== state.device?.deviceId;
|
||||
MediaDeviceHandler.instance.setDevice(device.deviceId, MediaDeviceKindEnum.AudioInput);
|
||||
|
||||
setState({
|
||||
...state,
|
||||
device,
|
||||
});
|
||||
|
||||
if (shouldNotify) {
|
||||
onDeviceChanged?.(device);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
currentDevice: state.device,
|
||||
currentDeviceLabel: state.device?.label || _t("Default Device"),
|
||||
devices: state.devices,
|
||||
setDevice,
|
||||
};
|
||||
};
|
|
@ -654,6 +654,7 @@
|
|||
"30s backward": "30s backward",
|
||||
"30s forward": "30s forward",
|
||||
"Go live": "Go live",
|
||||
"Change input device": "Change input device",
|
||||
"Live": "Live",
|
||||
"Voice broadcast": "Voice broadcast",
|
||||
"Cannot reach homeserver": "Cannot reach homeserver",
|
||||
|
|
|
@ -21,99 +21,34 @@ import AccessibleButton from "../../../components/views/elements/AccessibleButto
|
|||
import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording";
|
||||
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../../../components/views/context_menus/IconizedContextMenu";
|
||||
import { requestMediaPermissions } from "../../../utils/media/requestMediaPermissions";
|
||||
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||
import { toLeftOrRightOf } from "../../../components/structures/ContextMenu";
|
||||
import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection";
|
||||
import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu";
|
||||
|
||||
interface Props {
|
||||
voiceBroadcastPreRecording: VoiceBroadcastPreRecording;
|
||||
}
|
||||
|
||||
interface State {
|
||||
devices: MediaDeviceInfo[];
|
||||
device: MediaDeviceInfo | null;
|
||||
showDeviceSelect: boolean;
|
||||
}
|
||||
|
||||
export const VoiceBroadcastPreRecordingPip: React.FC<Props> = ({
|
||||
voiceBroadcastPreRecording,
|
||||
}) => {
|
||||
const shouldRequestPermissionsRef = useRef<boolean>(true);
|
||||
const pipRef = useRef<HTMLDivElement>(null);
|
||||
const [state, setState] = useState<State>({
|
||||
devices: [],
|
||||
device: null,
|
||||
showDeviceSelect: false,
|
||||
});
|
||||
const pipRef = useRef<HTMLDivElement | null>(null);
|
||||
const { currentDevice, currentDeviceLabel, devices, setDevice } = useAudioDeviceSelection();
|
||||
const [showDeviceSelect, setShowDeviceSelect] = useState<boolean>(false);
|
||||
|
||||
if (shouldRequestPermissionsRef.current) {
|
||||
shouldRequestPermissionsRef.current = false;
|
||||
requestMediaPermissions(false).then((stream: MediaStream | undefined) => {
|
||||
MediaDeviceHandler.getDevices().then(({ audioinput }) => {
|
||||
MediaDeviceHandler.getDefaultDevice(audioinput);
|
||||
const deviceFromSettings = MediaDeviceHandler.getAudioInput();
|
||||
const device = audioinput.find((d) => {
|
||||
return d.deviceId === deviceFromSettings;
|
||||
}) || audioinput[0];
|
||||
setState({
|
||||
...state,
|
||||
devices: audioinput,
|
||||
device,
|
||||
});
|
||||
stream?.getTracks().forEach(t => t.stop());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const onDeviceOptionClick = (device: MediaDeviceInfo) => {
|
||||
setState({
|
||||
...state,
|
||||
device,
|
||||
showDeviceSelect: false,
|
||||
});
|
||||
const onDeviceSelect = (device: MediaDeviceInfo | null) => {
|
||||
setShowDeviceSelect(false);
|
||||
setDevice(device);
|
||||
};
|
||||
|
||||
const onMicrophoneLineClick = () => {
|
||||
setState({
|
||||
...state,
|
||||
showDeviceSelect: true,
|
||||
});
|
||||
};
|
||||
|
||||
const deviceOptions = state.devices.map((d: MediaDeviceInfo) => {
|
||||
return <IconizedContextMenuRadio
|
||||
key={d.deviceId}
|
||||
active={d.deviceId === state.device?.deviceId}
|
||||
onClick={() => onDeviceOptionClick(d)}
|
||||
label={d.label}
|
||||
/>;
|
||||
});
|
||||
|
||||
const devicesMenu = state.showDeviceSelect && pipRef.current
|
||||
? <IconizedContextMenu
|
||||
mountAsChild={false}
|
||||
onFinished={() => {}}
|
||||
{...toLeftOrRightOf(pipRef.current.getBoundingClientRect(), 0)}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ deviceOptions }
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
: null;
|
||||
|
||||
return <div
|
||||
className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip"
|
||||
ref={pipRef}
|
||||
>
|
||||
<VoiceBroadcastHeader
|
||||
onCloseClick={voiceBroadcastPreRecording.cancel}
|
||||
onMicrophoneLineClick={onMicrophoneLineClick}
|
||||
onMicrophoneLineClick={() => setShowDeviceSelect(true)}
|
||||
room={voiceBroadcastPreRecording.room}
|
||||
microphoneLabel={state.device?.label || _t('Default Device')}
|
||||
microphoneLabel={currentDeviceLabel}
|
||||
showClose={true}
|
||||
/>
|
||||
<AccessibleButton
|
||||
|
@ -124,6 +59,13 @@ export const VoiceBroadcastPreRecordingPip: React.FC<Props> = ({
|
|||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
||||
{ _t("Go live") }
|
||||
</AccessibleButton>
|
||||
{ devicesMenu }
|
||||
{
|
||||
showDeviceSelect && <DevicesContextMenu
|
||||
containerRef={pipRef}
|
||||
currentDevice={currentDevice}
|
||||
devices={devices}
|
||||
onDeviceSelect={onDeviceSelect}
|
||||
/>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
VoiceBroadcastControl,
|
||||
|
@ -26,13 +26,18 @@ import { VoiceBroadcastHeader } from "../atoms/VoiceBroadcastHeader";
|
|||
import { Icon as StopIcon } from "../../../../res/img/element-icons/Stop.svg";
|
||||
import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg";
|
||||
import { Icon as RecordIcon } from "../../../../res/img/element-icons/Record.svg";
|
||||
import { Icon as MicrophoneIcon } from "../../../../res/img/element-icons/Mic.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
||||
import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection";
|
||||
import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu";
|
||||
|
||||
interface VoiceBroadcastRecordingPipProps {
|
||||
recording: VoiceBroadcastRecording;
|
||||
}
|
||||
|
||||
export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProps> = ({ recording }) => {
|
||||
const pipRef = useRef<HTMLDivElement | null>(null);
|
||||
const {
|
||||
live,
|
||||
timeLeft,
|
||||
|
@ -41,6 +46,29 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
|
|||
stopRecording,
|
||||
toggleRecording,
|
||||
} = useVoiceBroadcastRecording(recording);
|
||||
const { currentDevice, devices, setDevice } = useAudioDeviceSelection();
|
||||
|
||||
const onDeviceSelect = async (device: MediaDeviceInfo) => {
|
||||
setShowDeviceSelect(false);
|
||||
|
||||
if (currentDevice.deviceId === device.deviceId) {
|
||||
// device unchanged
|
||||
return;
|
||||
}
|
||||
|
||||
setDevice(device);
|
||||
|
||||
if ([VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Stopped].includes(recordingState)) {
|
||||
// Nothing to do in these cases. Resume will use the selected device.
|
||||
return;
|
||||
}
|
||||
|
||||
// pause and resume to switch the input device
|
||||
await recording.pause();
|
||||
await recording.resume();
|
||||
};
|
||||
|
||||
const [showDeviceSelect, setShowDeviceSelect] = useState<boolean>(false);
|
||||
|
||||
const toggleControl = recordingState === VoiceBroadcastInfoState.Paused
|
||||
? <VoiceBroadcastControl
|
||||
|
@ -53,6 +81,7 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
|
|||
|
||||
return <div
|
||||
className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip"
|
||||
ref={pipRef}
|
||||
>
|
||||
<VoiceBroadcastHeader
|
||||
live={live ? "live" : "grey"}
|
||||
|
@ -62,11 +91,25 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
|
|||
<hr className="mx_VoiceBroadcastBody_divider" />
|
||||
<div className="mx_VoiceBroadcastBody_controls">
|
||||
{ toggleControl }
|
||||
<AccessibleButton
|
||||
aria-label={_t("Change input device")}
|
||||
onClick={() => setShowDeviceSelect(true)}
|
||||
>
|
||||
<MicrophoneIcon className="mx_Icon mx_Icon_16 mx_Icon_alert" />
|
||||
</AccessibleButton>
|
||||
<VoiceBroadcastControl
|
||||
icon={StopIcon}
|
||||
label="Stop Recording"
|
||||
onClick={stopRecording}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
showDeviceSelect && <DevicesContextMenu
|
||||
containerRef={pipRef}
|
||||
currentDevice={currentDevice}
|
||||
devices={devices}
|
||||
onDeviceSelect={onDeviceSelect}
|
||||
/>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -47,7 +47,13 @@ const showStopBroadcastingDialog = async (): Promise<boolean> => {
|
|||
|
||||
export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(recording.infoEvent.getRoomId());
|
||||
const roomId = recording.infoEvent.getRoomId();
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
if (!room) {
|
||||
throw new Error("Unable to find voice broadcast room with Id: " + roomId);
|
||||
}
|
||||
|
||||
const stopRecording = async () => {
|
||||
const confirmed = await showStopBroadcastingDialog();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue