mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 03:16:38 +00:00
fix(runtime): preserve authored muted attr; clean up WebAudio on end (#1319)
onSetMuted/onSetMediaOutputMuted set el.muted = effective on every <video> and <audio> element. When the bridge sent onSetMuted(false), it unmuted avatar <video muted> elements whose baked-in lip-sync audio should never play — causing double audio alongside the separate TTS. Fix: el.muted = effective || el.defaultMuted. AudioBufferSourceNode fires 'ended' when playback completes naturally, but _activeSources was never cleaned up. This kept isActive() true permanently, which force-muted all HTML audio elements via the outputMuted flag in syncRuntimeMedia — causing audio to disappear after the WebAudio buffer finished (~5s for short TTS clips). Add onended listener that removes the source from _activeSources and restores el.muted to its pre-WebAudio value. All side-effects are guarded by idx !== -1 so a stale ended event after stopAll() is a no-op and cannot clobber bridge state set between stop and the async event delivery.
This commit is contained in:
@@ -3,6 +3,7 @@ import { WebAudioTransport } from "./webAudioTransport";
|
||||
|
||||
function createMockAudioContext(currentTime = 100) {
|
||||
const startFn = vi.fn();
|
||||
const endedListeners: (() => void)[] = [];
|
||||
const sourceNode = {
|
||||
buffer: null as AudioBuffer | null,
|
||||
playbackRate: { value: 1 },
|
||||
@@ -10,6 +11,10 @@ function createMockAudioContext(currentTime = 100) {
|
||||
stop: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
connect: vi.fn(),
|
||||
addEventListener: vi.fn((event: string, cb: () => void) => {
|
||||
if (event === "ended") endedListeners.push(cb);
|
||||
}),
|
||||
_fireEnded: () => endedListeners.forEach((cb) => cb()),
|
||||
};
|
||||
const gainNode = {
|
||||
gain: { value: 1 },
|
||||
@@ -281,4 +286,60 @@ describe("WebAudioTransport", () => {
|
||||
expect(transport.getTime()).toBeCloseTo(9.5, 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onended cleanup (audio dropout fix)", () => {
|
||||
it("cleans up _activeSources when AudioBufferSourceNode ends naturally", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
const el = { muted: false } as HTMLMediaElement;
|
||||
|
||||
await transport.schedulePlayback(el, mockBuffer, 0, 0, 0, 1, gen);
|
||||
expect(transport.isActive()).toBe(true);
|
||||
expect(el.muted).toBe(true);
|
||||
|
||||
mock.sourceNode._fireEnded();
|
||||
|
||||
expect(transport.isActive()).toBe(false);
|
||||
expect(el.muted).toBe(false);
|
||||
});
|
||||
|
||||
it("restores priorMuted=true when element was already muted", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
const el = { muted: true } as HTMLMediaElement;
|
||||
|
||||
await transport.schedulePlayback(el, mockBuffer, 0, 0, 0, 1, gen);
|
||||
expect(el.muted).toBe(true);
|
||||
|
||||
mock.sourceNode._fireEnded();
|
||||
|
||||
expect(el.muted).toBe(true);
|
||||
expect(transport.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("registers onended listener on the sourceNode", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
|
||||
await transport.schedulePlayback(mockEl, mockBuffer, 0, 0, 0, 1, gen);
|
||||
|
||||
expect(mock.sourceNode.addEventListener).toHaveBeenCalledWith("ended", expect.any(Function));
|
||||
});
|
||||
|
||||
it("onended after stopAll is a no-op — does not clobber restored state", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
const el = { muted: false } as HTMLMediaElement;
|
||||
|
||||
await transport.schedulePlayback(el, mockBuffer, 0, 0, 0, 1, gen);
|
||||
expect(el.muted).toBe(true);
|
||||
|
||||
transport.stopAll();
|
||||
expect(el.muted).toBe(false);
|
||||
expect(transport.isActive()).toBe(false);
|
||||
|
||||
el.muted = true;
|
||||
|
||||
mock.sourceNode._fireEnded();
|
||||
|
||||
expect(el.muted).toBe(true);
|
||||
expect(transport.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user