Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/core/src/runtime/webAudioTransport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,33 @@ describe("WebAudioTransport", () => {
expect([transport.routesElement(gone), transport.routesElement(kept)]).toEqual([false, true]);
});

it("keeps a routed element silent once a decoded buffer plays in its place", async () => {
const { transport, mock, gen } = setupTransport(100);
const node = { connect: vi.fn(), disconnect: vi.fn() };
mock.ctx.createMediaElementSource = vi.fn(() => node) as never;
await transport.scheduleMediaElementPlayback(mockEl, 0, 0, 0, 1, gen, 1);
node.connect.mockClear();
node.disconnect.mockClear();

transport.releaseRouted(mockEl);

expect(node.disconnect).toHaveBeenCalled();
expect(node.connect).not.toHaveBeenCalled();
});

it("sounds a handed-over element through the output again once playback stops", async () => {
const { transport, mock, gen } = setupTransport(100);
const node = { connect: vi.fn(), disconnect: vi.fn() };
mock.ctx.createMediaElementSource = vi.fn(() => node) as never;
await transport.scheduleMediaElementPlayback(mockEl, 0, 0, 0, 1, gen, 1);
transport.releaseRouted(mockEl);
node.connect.mockClear();

transport.stopAll();

expect(node.connect).toHaveBeenCalledWith(mock.ctx.destination);
});

it("creates one MediaElementAudioSourceNode per element and context", async () => {
const { transport, mock } = setupTransport(100);

Expand Down
18 changes: 15 additions & 3 deletions packages/core/src/runtime/webAudioTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export class WebAudioTransport {
private _pendingDecodeBytes = new Map<string, number>();
private _failedSrcs = new Set<string>();
private _mediaElementSources = new WeakMap<HTMLMediaElement, MediaElementAudioSourceNode>();
private _handedOver = new WeakSet<HTMLMediaElement>();
private _handedOver = new Set<HTMLMediaElement>();
private _activeSources: ScheduledSource[] = [];
private _masterGain: GainNode | null = null;
/** Where the master bus chain lands; the program meter taps here. */
Expand Down Expand Up @@ -1052,12 +1052,12 @@ export class WebAudioTransport {
}
}

/** Drops the routed element's source once a decoded buffer plays in its place; the element then sounds natively. */
/** Drops the routed element's source once a decoded buffer plays in its place; it stays silent until a stop. */
releaseRouted(el: HTMLMediaElement): void {
const routed = this._activeSources.find((s) => s.el === el && s.sourceKind === "media-element");
if (!routed) return;
this._activeSources.splice(this._activeSources.indexOf(routed), 1);
this.dispose(routed);
this.dispose(routed, false);
this._handedOver.add(el);
}

Expand Down Expand Up @@ -1132,11 +1132,23 @@ export class WebAudioTransport {
stopAll(): void {
for (const source of this._activeSources) this.dispose(source);
this._activeSources = [];
this.reconnectHandedOver();
this._paused = true;
this._playGeneration += 1;
this.rest();
}

private reconnectHandedOver(): void {
for (const el of this._handedOver) {
try {
if (this._ctx) this._mediaElementSources.get(el)?.connect(this._ctx.destination);
} catch (err) {
swallow("webAudioTransport.reconnectHandedOver", err);
}
}
this._handedOver.clear();
}

/** Stops what plays from media gone from the page, without sending it to the destination as a stop does. */
releaseDetached(): void {
this._activeSources = this._activeSources.filter((source) => {
Expand Down
Loading