From 0a86a6b93d78cbf9727a072f03513707dabaae33 Mon Sep 17 00:00:00 2001 From: hayyaksi <193020925+hayaksi1@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:51:08 +0300 Subject: [PATCH] Stop large audio playback when the media element ends (#34495) --- apps/web/src/audio/Playback.ts | 7 +- .../test/unit-tests/audio/Playback-test.ts | 85 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/apps/web/src/audio/Playback.ts b/apps/web/src/audio/Playback.ts index d94e4593a2..0181943cb9 100644 --- a/apps/web/src/audio/Playback.ts +++ b/apps/web/src/audio/Playback.ts @@ -135,6 +135,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte this.clock.destroy(); this.waveformObservable.close(); if (this.element) { + this.element.removeEventListener("ended", this.onPlaybackEnd); URL.revokeObjectURL(this.element.src); this.element.remove(); } @@ -236,12 +237,16 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte if (this.element) { this.source = this.context.createMediaElementSource(this.element); + // A MediaElementAudioSourceNode is not a scheduled source node and never emits "ended", + // so the media element has to be listened to instead. Without this, playback of a large + // file never returns to Stopped and the clock keeps running past the end of the clip. + this.element.addEventListener("ended", this.onPlaybackEnd); } else { this.source = this.context.createBufferSource(); this.source.buffer = this.audioBuf ?? null; + this.source.addEventListener("ended", this.onPlaybackEnd); } - this.source.addEventListener("ended", this.onPlaybackEnd); this.source.connect(this.context.destination); } diff --git a/apps/web/test/unit-tests/audio/Playback-test.ts b/apps/web/test/unit-tests/audio/Playback-test.ts index 9732132d6d..3f06ef9342 100644 --- a/apps/web/test/unit-tests/audio/Playback-test.ts +++ b/apps/web/test/unit-tests/audio/Playback-test.ts @@ -29,11 +29,18 @@ describe("Playback", () => { connect: jest.fn(), start: jest.fn(), }; + const mockMediaElementSourceNode = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + connect: jest.fn(), + disconnect: jest.fn(), + }; const mockAudioContext = { decodeAudioData: jest.fn(), suspend: jest.fn(), resume: jest.fn(), createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode), + createMediaElementSource: jest.fn().mockReturnValue(mockMediaElementSourceNode), currentTime: 1337, }; @@ -181,4 +188,82 @@ describe("Playback", () => { expect(mockAudioContext.decodeAudioData).toHaveBeenCalledTimes(1); }); }); + + describe("audio larger than 5mb", () => { + // Anything over 5mb is played through an