Add input device selection during voice broadcast (#9620)

This commit is contained in:
Michael Weimann 2022-11-28 15:16:44 +01:00 committed by GitHub
parent 5b5c3ab33c
commit b302275289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 486 additions and 87 deletions

View file

@ -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>;
};

View file

@ -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>;
};

View file

@ -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();