Fix microphone permission checking for audio recording
See [#5580][0]. [0]: https://github.com/signalapp/Signal-Desktop/pull/5580
This commit is contained in:
parent
1dc353f089
commit
79b3b6408e
8 changed files with 69 additions and 43 deletions
11
app/main.ts
11
app/main.ts
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
ts/util/requestMicrophonePermissions.ts
Normal file
14
ts/util/requestMicrophonePermissions.ts
Normal 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
2
ts/window.d.ts
vendored
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue