Skip to content

Commit

Permalink
Fix issue where firefox audio gets louder on each recording
Browse files Browse the repository at this point in the history
- Maintain single captured stream per video
  • Loading branch information
killergerbah committed Sep 7, 2024
1 parent 5e6aa39 commit 5ab318b
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 24 deletions.
32 changes: 22 additions & 10 deletions extension/src/services/audio-recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ export default class AudioRecorder {
this.blobPromise = null;
}

startWithTimeout(stream: MediaStream, time: number, onStartedCallback: () => void): Promise<string> {
startWithTimeout(
stream: MediaStream,
time: number,
onStartedCallback: () => void,
doNotManageStream: boolean = false
): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
if (this.recording) {
Expand All @@ -26,20 +31,20 @@ export default class AudioRecorder {
return;
}

await this.start(stream);
await this.start(stream, doNotManageStream);
onStartedCallback();
this.timeoutResolve = resolve;
this.timeoutId = setTimeout(async () => {
this.timeoutId = undefined;
resolve(await this.stop());
resolve(await this.stop(doNotManageStream));
}, time);
} catch (e) {
reject(e);
}
});
}

start(stream: MediaStream): Promise<void> {
start(stream: MediaStream, doNotManageStream: boolean = false): Promise<void> {
return new Promise((resolve, reject) => {
if (this.recording) {
reject('Already recording, cannot start');
Expand All @@ -58,9 +63,12 @@ export default class AudioRecorder {
};
});
recorder.start();
const output = new AudioContext();
const source = output.createMediaStreamSource(stream);
source.connect(output.destination);

if (!doNotManageStream) {
const output = new AudioContext();
const source = output.createMediaStreamSource(stream);
source.connect(output.destination);
}

this.recorder = recorder;
this.recording = true;
Expand All @@ -72,16 +80,20 @@ export default class AudioRecorder {
});
}

async stop(): Promise<string> {
async stop(doNotManageStream: boolean = false): Promise<string> {
if (!this.recording) {
throw new Error('Not recording, unable to stop');
}

this.recording = false;
this.recorder?.stop();
this.recorder = null;
this.stream?.getTracks()?.forEach((t) => t.stop());
this.stream = null;

if (!doNotManageStream) {
this.stream?.getTracks()?.forEach((t) => t.stop());
this.stream = null;
}

const blob = await this.blobPromise;
this.blobPromise = null;
const base64 = await bufferToBase64(await blob!.arrayBuffer());
Expand Down
33 changes: 19 additions & 14 deletions extension/src/services/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ export default class Binding {
) => void;
private heartbeatInterval?: NodeJS.Timeout;

// In the case of firefox, we need to avoid capturing the audio stream more than once,
// so we keep a reference to the first one we capture here.
private audioStream?: MediaStream;

private readonly frameId?: string;

constructor(video: HTMLMediaElement, syncAvailable: boolean, frameId?: string) {
Expand Down Expand Up @@ -740,23 +744,22 @@ export default class Binding {
this._audioRecorder.startWithTimeout(
stream,
startRecordingAudioWithTimeoutMessage.timeout,
() => sendResponse(true)
() => sendResponse(true),
true
)
)
.then((audioBase64) =>
this._sendAudioBase64(audioBase64, startRecordingAudioWithTimeoutMessage.preferMp3)
)
.then(() => this._resumeAudioAfterRecording())
.catch((e) => {
console.error(e instanceof Error ? e.message : String(e));
sendResponse(false);
});
return true;
case 'start-recording-audio':
this._captureStream()
.then((stream) => this._audioRecorder.start(stream))
.then((stream) => this._audioRecorder.start(stream, true))
.then(() => sendResponse(true))
.then(() => this._resumeAudioAfterRecording())
.catch((e) => {
console.error(e instanceof Error ? e.message : String(e));
sendResponse(false);
Expand All @@ -765,7 +768,7 @@ export default class Binding {
case 'stop-recording-audio':
const stopRecordingAudioMessage = request.message as StopRecordingAudioMessage;
this._audioRecorder
.stop()
.stop(true)
.then((audioBase64) => {
sendResponse(true);
this._sendAudioBase64(audioBase64, stopRecordingAudioMessage.preferMp3);
Expand Down Expand Up @@ -1327,6 +1330,11 @@ export default class Binding {

private _captureStream(): Promise<MediaStream> {
return new Promise((resolve, reject) => {
if (this.audioStream !== undefined) {
resolve(this.audioStream);
return;
}

try {
let stream: MediaStream | undefined;

Expand Down Expand Up @@ -1355,22 +1363,19 @@ export default class Binding {
}
}

// Ensure audio keeps playing through computer speakers
const output = new AudioContext();
const source = output.createMediaStreamSource(audioStream);
source.connect(output.destination);

this.audioStream = audioStream;
resolve(audioStream);
} catch (e) {
reject(e);
}
});
}

private async _resumeAudioAfterRecording() {
// On Firefox the audio is muted once audio recording is stopped.
// Below is a hack to resume the audio.
const stream = await this._captureStream();
const output = new AudioContext();
const source = output.createMediaStreamSource(stream);
source.connect(output.destination);
}

private async _sendAudioBase64(base64: string, preferMp3: boolean) {
if (preferMp3) {
const blob = await (await fetch('data:audio/webm;base64,' + base64)).blob();
Expand Down

0 comments on commit 5ab318b

Please sign in to comment.