Keep a copy of the audio buffer so the WAV fallback can run (#34481)

* Keep a copy of the audio buffer so the WAV fallback can run

decodeAudioData detaches the buffer it is given, so the catch block handed an
already detached buffer to decodeOgg and it threw "Cannot perform Construct on
a detached ArrayBuffer" before it could re-encode. The fallback could never
run and the user saw an error instead of their voice message.

Fixes https://github.com/element-hq/element-web/issues/24904

* Assert the fallback buffer is usable instead of constructing a view for its side effect

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
hayyaksi
2026-08-11 16:08:59 +00:00
committed by GitHub
co-authored by Michael Telatynski
parent 9e8a57c81c
commit 91e8f3bdae
2 changed files with 32 additions and 1 deletions
@@ -176,6 +176,34 @@ describe("Playback", () => {
expect(playback.currentState).toEqual(PlaybackState.Stopped);
});
it("hands the ogg fallback a buffer which decodeAudioData has not detached", async () => {
// stub logger to keep console clean from expected error
jest.spyOn(logger, "error").mockReturnValue(undefined);
jest.spyOn(logger, "warn").mockReturnValue(undefined);
const buffer = new ArrayBuffer(8);
mockAudioContext.decodeAudioData
.mockImplementationOnce((buf: ArrayBuffer) => {
// The real decodeAudioData detaches the buffer it is handed, even when it fails.
structuredClone(buf, { transfer: [buf] });
return Promise.reject(new Error("test"));
})
.mockResolvedValueOnce(mockAudioBuffer);
// Constructing a view over a detached buffer throws, which is what decodeOgg does first.
mocked(decodeOgg).mockImplementationOnce(async (audioBuffer: ArrayBuffer) => {
expect(() => new Uint8Array(audioBuffer)).not.toThrow();
return new ArrayBuffer(1);
});
const playback = new Playback(buffer);
await playback.prepare();
expect(decodeOgg).toHaveBeenCalled();
expect(mockAudioContext.decodeAudioData).toHaveBeenCalledTimes(2);
expect(playback.currentState).toEqual(PlaybackState.Stopped);
});
it("does not try to re-decode audio", async () => {
const buffer = new ArrayBuffer(8);
const playback = new Playback(buffer);