From c53ed5be2e1abaffb0e553bcddc035eb1bc8f3cc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Jul 2021 17:53:33 -0600 Subject: [PATCH] Use a MediaElementSourceAudioNode to process large audio files Fixes https://github.com/vector-im/element-web/issues/18149 See comment block contained within diff. --- src/voice/Playback.ts | 90 +++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index df0bf593fa6..b72963e90e0 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -59,9 +59,10 @@ export class Playback extends EventEmitter implements IDestroyable { public readonly thumbnailWaveform: number[]; private readonly context: AudioContext; - private source: AudioBufferSourceNode; + private source: AudioBufferSourceNode | MediaElementAudioSourceNode; private state = PlaybackState.Decoding; private audioBuf: AudioBuffer; + private element: HTMLAudioElement; private resampledWaveform: number[]; private waveformObservable = new SimpleObservable(); private readonly clock: PlaybackClock; @@ -129,36 +130,59 @@ export class Playback extends EventEmitter implements IDestroyable { this.removeAllListeners(); this.clock.destroy(); this.waveformObservable.close(); + if (this.element) { + URL.revokeObjectURL(this.element.src); + this.element.remove(); + } } public async prepare() { - // Safari compat: promise API not supported on this function - this.audioBuf = await new Promise((resolve, reject) => { - this.context.decodeAudioData(this.buf, b => resolve(b), async e => { - // This error handler is largely for Safari as well, which doesn't support Opus/Ogg - // very well. - console.error("Error decoding recording: ", e); - console.warn("Trying to re-encode to WAV instead..."); - - const wav = await decodeOgg(this.buf); - - // noinspection ES6MissingAwait - not needed when using callbacks - this.context.decodeAudioData(wav, b => resolve(b), e => { - console.error("Still failed to decode recording: ", e); - reject(e); + // The point where we use an audio element is fairly arbitrary, though we don't want + // it to be too low. As of writing, voice messages want to show a waveform but audio + // messages do not. Using an audio element means we can't show a waveform preview, so + // we try to target the difference between a voice message file and large audio file. + // Overall, the point of this is to avoid memory-related issues due to storing a massive + // audio buffer in memory, as that can balloon to far greater than the input buffer's + // byte length. + if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb + console.log("Audio file too large: processing through