[MVP][Issue #223] Telefon-klient guard: ingen lydafspilning på secondary device #246

Merged
integrator-bot merged 1 commits from wpp-dev-lane-223 into main 2026-03-02 01:39:57 +01:00
2 changed files with 31 additions and 8 deletions

View File

@@ -400,7 +400,14 @@ describe('PlayerShellComponent gameplay wiring', () => {
const component = new PlayerShellComponent(); const component = new PlayerShellComponent();
component.ngOnInit(); 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(); component.ngOnDestroy();

View File

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