signal-desktop/ts/views/recorder_view.ts
2021-09-17 14:27:53 -04:00

143 lines
3.9 KiB
TypeScript

// Copyright 2016-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import moment from 'moment';
import * as log from '../logging/log';
window.Whisper = window.Whisper || {};
const { Whisper } = window;
Whisper.RecorderView = Whisper.View.extend({
className: 'recorder clearfix',
template: () => $('#recorder').html(),
initialize() {
this.startTime = Date.now();
this.interval = setInterval(this.updateTime.bind(this), 1000);
this.onSwitchAwayBound = this.onSwitchAway.bind(this);
$(window).on('blur', this.onSwitchAwayBound);
this.handleKeyDownBound = this.handleKeyDown.bind(this);
this.$el.on('keydown', this.handleKeyDownBound);
this.start();
},
events: {
'click .close': 'remove',
'click .finish': 'finish',
close: 'remove',
},
onSwitchAway() {
this.lostFocus = true;
this.recorder.finishRecording();
},
handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
this.remove();
event.preventDefault();
event.stopPropagation();
}
},
updateTime() {
const duration = moment.duration(Date.now() - this.startTime, 'ms');
const minutes = `${Math.trunc(duration.asMinutes())}`;
let seconds = `${duration.seconds()}`;
if (seconds.length < 2) {
seconds = `0${seconds}`;
}
this.$('.time').text(`${minutes}:${seconds}`);
},
async remove() {
// Note: the 'close' event can be triggered by InboxView, when the user clicks
// anywhere outside the recording pane.
if (this.recorder.isRecording()) {
this.recorder.cancelRecording();
}
// Reach in and terminate the web worker used by WebAudioRecorder, otherwise
// it gets leaked due to a reference cycle with its onmessage listener
this.recorder.worker.terminate();
this.recorder = null;
if (this.interval) {
clearInterval(this.interval);
}
this.interval = null;
if (this.source) {
this.source.disconnect();
}
this.source = null;
if (this.context) {
await this.context.close();
log.info('audio context closed');
}
this.context = null;
Whisper.View.prototype.remove.call(this);
this.trigger('closed');
$(window).off('blur', this.onSwitchAwayBound);
this.$el.off('keydown', this.handleKeyDownBound);
},
finish() {
this.clickedFinish = true;
this.recorder.finishRecording();
},
handleBlob(_: unknown, blob: Blob) {
if (blob && this.clickedFinish) {
this.trigger('send', blob);
} else if (blob) {
this.trigger('confirm', blob, this.lostFocus);
}
this.remove();
},
async start() {
this.lostFocus = false;
this.clickedFinish = false;
this.context = new AudioContext();
this.input = this.context.createGain();
this.recorder = new window.WebAudioRecorder(this.input, {
encoding: 'mp3',
workerDir: 'js/', // must end with slash
});
this.recorder.onComplete = this.handleBlob.bind(this);
this.recorder.onError = this.onError.bind(this);
this.recorder.onTimeout = this.onTimeout.bind(this);
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.source = this.context.createMediaStreamSource(stream);
this.source.connect(this.input);
this.recorder.startRecording();
} catch (err) {
this.onError(err);
}
},
onTimeout() {
this.recorder.finishRecording();
},
onError(error: Error) {
// Protect against out-of-band errors, which can happen if the user revokes media
// permissions after successfully accessing the microphone.
if (!this.recorder) {
return;
}
this.remove();
if (error && error.name === 'NotAllowedError') {
log.warn('RecorderView.onError: Microphone access is not allowed!');
window.showPermissionsPopup();
} else {
log.error(
'RecorderView.onError:',
error && error.stack ? error.stack : error
);
}
},
});