Fix microphone permission checking for audio recording

See [#5580][0].

[0]: https://github.com/signalapp/Signal-Desktop/pull/5580
This commit is contained in:
David Sanders 2021-10-27 11:16:09 -05:00 committed by Evan Hahn
parent 1dc353f089
commit 79b3b6408e
8 changed files with 69 additions and 43 deletions

View file

@ -1862,8 +1862,15 @@ ipc.on(
// Permissions Popup-related IPC calls // Permissions Popup-related IPC calls
ipc.on('show-permissions-popup', () => { ipc.handle('show-permissions-popup', async () => {
showPermissionsPopupWindow(false, false); try {
await showPermissionsPopupWindow(false, false);
} catch (error) {
getLogger().error(
'show-permissions-popup error:',
error && error.stack ? error.stack : error
);
}
}); });
ipc.handle( ipc.handle(
'show-calling-permissions-popup', 'show-calling-permissions-popup',

View file

@ -239,7 +239,7 @@ try {
// Settings-related events // Settings-related events
window.showSettings = () => ipc.send('show-settings'); window.showSettings = () => ipc.send('show-settings');
window.showPermissionsPopup = () => ipc.send('show-permissions-popup'); window.showPermissionsPopup = () => ipc.invoke('show-permissions-popup');
window.showCallingPermissionsPopup = forCamera => window.showCallingPermissionsPopup = forCamera =>
ipc.invoke('show-calling-permissions-popup', forCamera); ipc.invoke('show-calling-permissions-popup', forCamera);

View file

@ -97,12 +97,19 @@ export const AudioCapture = ({
const startRecordingShortcut = useStartRecordingShortcut(startRecording); const startRecordingShortcut = useStartRecordingShortcut(startRecording);
useKeyboardShortcuts(startRecordingShortcut); useKeyboardShortcuts(startRecordingShortcut);
const closeToast = useCallback(() => {
setToastType(undefined);
}, []);
// Update timestamp regularly, then timeout if recording goes over five minutes // Update timestamp regularly, then timeout if recording goes over five minutes
useEffect(() => { useEffect(() => {
if (!isRecording) { if (!isRecording) {
return; return;
} }
setDurationText(START_DURATION_TEXT);
setToastType(ToastType.VoiceNoteLimit);
const startTime = Date.now(); const startTime = Date.now();
const interval = setInterval(() => { const interval = setInterval(() => {
const duration = moment.duration(Date.now() - startTime, 'ms'); const duration = moment.duration(Date.now() - startTime, 'ms');
@ -120,8 +127,15 @@ export const AudioCapture = ({
return () => { return () => {
clearInterval(interval); clearInterval(interval);
closeToast();
}; };
}, [completeRecording, errorRecording, isRecording, setDurationText]); }, [
closeToast,
completeRecording,
errorRecording,
isRecording,
setDurationText,
]);
const clickCancel = useCallback(() => { const clickCancel = useCallback(() => {
cancelRecording(); cancelRecording();
@ -131,10 +145,6 @@ export const AudioCapture = ({
completeRecording(conversationId, onSendAudioRecording); completeRecording(conversationId, onSendAudioRecording);
}, [conversationId, completeRecording, onSendAudioRecording]); }, [conversationId, completeRecording, onSendAudioRecording]);
const closeToast = useCallback(() => {
setToastType(undefined);
}, []);
let toastElement: JSX.Element | undefined; let toastElement: JSX.Element | undefined;
if (toastType === ToastType.VoiceNoteLimit) { if (toastType === ToastType.VoiceNoteLimit) {
toastElement = <ToastVoiceNoteLimit i18n={i18n} onClose={closeToast} />; toastElement = <ToastVoiceNoteLimit i18n={i18n} onClose={closeToast} />;
@ -226,8 +236,6 @@ export const AudioCapture = ({
if (draftAttachments.length) { if (draftAttachments.length) {
setToastType(ToastType.VoiceNoteMustBeOnlyAttachment); setToastType(ToastType.VoiceNoteMustBeOnlyAttachment);
} else { } else {
setDurationText(START_DURATION_TEXT);
setToastType(ToastType.VoiceNoteLimit);
startRecording(); startRecording();
} }
}} }}

View file

@ -1,6 +1,7 @@
// Copyright 2016-2020 Signal Messenger, LLC // Copyright 2016-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions';
import * as log from '../logging/log'; import * as log from '../logging/log';
import type { WebAudioRecorderClass } from '../window.d'; import type { WebAudioRecorderClass } from '../window.d';
@ -42,7 +43,15 @@ export class RecorderClass {
} }
} }
async start(): Promise<void> { async start(): Promise<boolean> {
const hasMicrophonePermission = await requestMicrophonePermissions();
if (!hasMicrophonePermission) {
log.info(
'Recorder/start: Microphone permission was denied, new audio recording not allowed.'
);
return false;
}
this.clear(); this.clear();
this.context = new AudioContext(); this.context = new AudioContext();
@ -61,11 +70,11 @@ export class RecorderClass {
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
if (!this.context || !this.input) { if (!this.context || !this.input) {
this.onError( const err = new Error(
this.recorder, 'Recorder/getUserMedia/stream: Missing context or input!'
new Error('Recorder/getUserMedia/stream: Missing context or input!')
); );
return; this.onError(this.recorder, err);
throw err;
} }
this.source = this.context.createMediaStreamSource(stream); this.source = this.context.createMediaStreamSource(stream);
this.source.connect(this.input); this.source.connect(this.input);
@ -81,7 +90,10 @@ export class RecorderClass {
if (this.recorder) { if (this.recorder) {
this.recorder.startRecording(); this.recorder.startRecording();
return true;
} }
return false;
} }
async stop(): Promise<Blob | undefined> { async stop(): Promise<Blob | undefined> {
@ -120,15 +132,7 @@ export class RecorderClass {
this.clear(); this.clear();
if (error && error.name === 'NotAllowedError') { log.error('Recorder/onError:', error && error.stack ? error.stack : error);
log.warn('Recorder/onError: Microphone permission missing');
window.showPermissionsPopup();
} else {
log.error(
'Recorder/onError:',
error && error.stack ? error.stack : error
);
}
} }
getBlob(): Blob { getBlob(): Blob {

View file

@ -92,6 +92,7 @@ import {
} from '../calling/constants'; } from '../calling/constants';
import { callingMessageToProto } from '../util/callingMessageToProto'; import { callingMessageToProto } from '../util/callingMessageToProto';
import { getSendOptions } from '../util/getSendOptions'; import { getSendOptions } from '../util/getSendOptions';
import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
import { import {
@ -1510,20 +1511,8 @@ export class CallingClass {
return true; return true;
} }
private async requestMicrophonePermissions(): Promise<boolean> {
const microphonePermission = await window.getMediaPermissions();
if (!microphonePermission) {
await window.showCallingPermissionsPopup(false);
// Check the setting again (from the source of truth).
return window.getMediaPermissions();
}
return true;
}
private async requestPermissions(isVideoCall: boolean): Promise<boolean> { private async requestPermissions(isVideoCall: boolean): Promise<boolean> {
const microphonePermission = await this.requestMicrophonePermissions(); const microphonePermission = await requestMicrophonePermissions();
if (microphonePermission) { if (microphonePermission) {
if (isVideoCall) { if (isVideoCall) {
return this.requestCameraPermissions(); return this.requestCameraPermissions();

View file

@ -77,8 +77,10 @@ function startRecording(): ThunkAction<
return; return;
} }
let recordingStarted = false;
try { try {
await recorder.start(); recordingStarted = await recorder.start();
} catch (err) { } catch (err) {
dispatch({ dispatch({
type: ERROR_RECORDING, type: ERROR_RECORDING,
@ -87,10 +89,12 @@ function startRecording(): ThunkAction<
return; return;
} }
dispatch({ if (recordingStarted) {
type: START_RECORDING, dispatch({
payload: undefined, type: START_RECORDING,
}); payload: undefined,
});
}
}; };
} }

View file

@ -0,0 +1,14 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export async function requestMicrophonePermissions(): Promise<boolean> {
const microphonePermission = await window.getMediaPermissions();
if (!microphonePermission) {
await window.showCallingPermissionsPopup(false);
// Check the setting again (from the source of truth).
return window.getMediaPermissions();
}
return true;
}

2
ts/window.d.ts vendored
View file

@ -160,7 +160,7 @@ declare global {
QRCode: any; QRCode: any;
removeSetupMenuItems: () => unknown; removeSetupMenuItems: () => unknown;
showPermissionsPopup: () => unknown; showPermissionsPopup: () => Promise<void>;
FontFace: typeof FontFace; FontFace: typeof FontFace;
_: typeof Underscore; _: typeof Underscore;