fix(player): harden secondary-device audio playback guard
All checks were successful
CI / test-and-quality (push) Successful in 3m20s
CI / test-and-quality (pull_request) Successful in 3m16s

This commit is contained in:
2026-03-02 00:34:22 +00:00
parent 60e58f6214
commit edf9460ceb
2 changed files with 31 additions and 8 deletions

View File

@@ -400,7 +400,14 @@ describe('PlayerShellComponent gameplay wiring', () => {
const component = new PlayerShellComponent();
component.ngOnInit();
await expect(mediaPrototype.play()).resolves.toBeUndefined();
const pause = vi.fn();
const audioElement = { muted: false, defaultMuted: false, volume: 1, pause };
await expect(mediaPrototype.play.call(audioElement)).resolves.toBeUndefined();
expect(audioElement.muted).toBe(true);
expect(audioElement.defaultMuted).toBe(true);
expect(audioElement.volume).toBe(0);
expect(pause).toHaveBeenCalledTimes(1);
component.ngOnDestroy();

View File

@@ -16,18 +16,21 @@ interface SessionDetail {
type ConnectionState = 'online' | 'reconnecting' | 'offline';
type LoadingTransition = 'refresh' | 'join' | 'submit-lie' | 'submit-guess' | null;
type GuardableMediaElement = {
muted?: boolean;
defaultMuted?: boolean;
volume?: number;
pause?: () => void;
};
type MediaPrototypeWithGuardState = {
play?: () => Promise<void>;
play?: (this: GuardableMediaElement) => Promise<void>;
__wppSecondaryDeviceAudioGuard__?: {
originalPlay: () => Promise<void>;
originalPlay: (this: GuardableMediaElement) => Promise<void>;
installs: number;
};
};
type GuardableMediaElement = {
muted?: boolean;
pause?: () => void;
};
function resolveLocalStorage(): Storage | undefined {
if (typeof window === 'undefined') {
@@ -221,7 +224,15 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
guardState.installs += 1;
} else {
const originalPlay = mediaPrototype.play;
mediaPrototype.play = () => Promise.resolve();
mediaPrototype.play = function mediaGuardedPlay(this: GuardableMediaElement): Promise<void> {
this.muted = true;
this.defaultMuted = true;
if (typeof this.volume === 'number') {
this.volume = 0;
}
this.pause?.();
return Promise.resolve();
};
mediaPrototype.__wppSecondaryDeviceAudioGuard__ = {
originalPlay,
installs: 1,
@@ -242,6 +253,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
};
}
private silenceExistingMediaElements(): void {
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') {
return;
@@ -258,6 +270,10 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
activeElements.forEach((element) => {
element.muted = true;
element.defaultMuted = true;
if (typeof element.volume === 'number') {
element.volume = 0;
}
element.pause?.();
});
}