diff --git a/src/AbstractPlayerBridge.ts b/src/AbstractPlayerBridge.ts index d6ab5fb..aef7757 100644 --- a/src/AbstractPlayerBridge.ts +++ b/src/AbstractPlayerBridge.ts @@ -11,6 +11,7 @@ import type { Qualities, StreamPhase, UserFeedback, + Volume, } from './util/schema'; import { validateBoolean, @@ -119,6 +120,9 @@ export abstract class AbstractPlayerBridge extends LiveryBridge { if (name === 'setMuted') { return this.setMuted(validateBoolean(arg)); } + if (name === 'setVolume') { + return this.setVolume(validateNumber(arg)); + } if (name === 'submitUserFeedback') { return this.submitUserFeedback(validateUserFeedback(arg)); } @@ -137,9 +141,6 @@ export abstract class AbstractPlayerBridge extends LiveryBridge { if (name === 'subscribeMode') { return this.subscribeMode(listener); } - if (name === 'subscribeMuted') { - return this.subscribeMuted(listener); - } if (name === 'subscribeOrientation') { return this.subscribeOrientation(listener); } @@ -155,6 +156,9 @@ export abstract class AbstractPlayerBridge extends LiveryBridge { if (name === 'subscribeStreamPhase') { return this.subscribeStreamPhase(listener); } + if (name === 'subscribeVolume') { + return this.subscribeVolume(listener); + } return super.handleCommand(name, arg, listener); } @@ -269,6 +273,8 @@ export abstract class AbstractPlayerBridge extends LiveryBridge { protected abstract setMuted(muted: boolean): void; + protected abstract setVolume(volume: number): void; + protected abstract submitUserFeedback(value: UserFeedback): void; protected abstract subscribeConfig( @@ -287,10 +293,6 @@ export abstract class AbstractPlayerBridge extends LiveryBridge { listener: (mode: PlaybackMode) => void, ): PlaybackMode; - protected abstract subscribeMuted( - listener: (value: boolean) => void, - ): boolean; - protected abstract subscribePlaybackState( listener: (playbackState: PlaybackState) => void, ): PlaybackState; @@ -298,4 +300,8 @@ export abstract class AbstractPlayerBridge extends LiveryBridge { protected abstract subscribeQualities( listener: (qualities?: Qualities) => void, ): Qualities | undefined; + + protected abstract subscribeVolume( + listener: (volume: Volume) => void, + ): Volume; } diff --git a/src/InteractiveBridge.ts b/src/InteractiveBridge.ts index e76440e..65690a5 100644 --- a/src/InteractiveBridge.ts +++ b/src/InteractiveBridge.ts @@ -12,6 +12,7 @@ import type { Qualities, StreamPhase, UserFeedback, + Volume, } from './util/schema'; import { validateBoolean, @@ -28,6 +29,7 @@ import { validateStreamPhase, validateString, validateStringOrUndefined, + validateVolume, } from './util/schema'; /** @@ -148,7 +150,7 @@ export class InteractiveBridge extends LiveryBridge { * Attempt to start or resume playback. * * Can fail if not allowed by the browser, e.g: when not called directly from a click event listener. - * In that case it can fall back to muted playback, changing {@link subscribeMuted} to true. + * In that case it can fall back to muted playback, changing {@link subscribeVolume}.muted to true. * Or if that also fails then {@link subscribePaused} will remain true. */ play() { @@ -254,12 +256,24 @@ export class InteractiveBridge extends LiveryBridge { * * Unmuting can fail if not allowed by the browser, e.g: when not called directly from a click event listener. * The specified state is kept track of by the player though and respected on reload when possible. - * Look at {@link subscribeMuted} state to track actual unmuting. + * Look at {@link subscribeVolume}.muted state to track actual unmuting. */ setMuted(muted: boolean) { return this.sendCommand('setMuted', muted); } + /** + * Change `volume` to specified value. + * + * When a player starts unmuted at volume `0` and this is changed to a higher volume later, + * that can be disallowed by the browser, e.g: when not called directly from a click event listener. + * In that case the player will fall back to changing {@link subscribeVolume}.muted to `true` + * to allow the volume change to persist. + */ + setVolume(volume: number) { + return this.sendCommand('setVolume', volume); + } + /** * Submit user feedback. * @@ -325,16 +339,6 @@ export class InteractiveBridge extends LiveryBridge { ).then((mode) => validatePlaybackMode(mode)); } - /** - * Returns promise of current player muted state - * and calls back `listener` with any subsequent muted changes. - */ - subscribeMuted(listener: (muted: boolean) => void) { - return this.sendCommand('subscribeMuted', undefined, (value) => - listener(validateBoolean(value)), - ).then(validateBoolean); - } - /** * Returns promise of current player window orientation (`'landscape' \| 'portrait'`) * and calls back `listener` with any subsequent orientations. @@ -442,6 +446,16 @@ export class InteractiveBridge extends LiveryBridge { ).then(validateStreamPhase); } + /** + * Returns promise of current player volume state + * and calls back `listener` with any subsequent volume changes. + */ + subscribeVolume(listener: (volume: Volume) => void) { + return this.sendCommand('subscribeVolume', undefined, (value) => + listener(validateVolume(value)), + ).then(validateVolume); + } + /** * Unregister custom command by name. * diff --git a/src/MockPlayerBridge.ts b/src/MockPlayerBridge.ts index 8414185..50ccd7e 100644 --- a/src/MockPlayerBridge.ts +++ b/src/MockPlayerBridge.ts @@ -6,6 +6,7 @@ import type { PlaybackState, Qualities, UserFeedback, + Volume, } from './util/schema'; const buildQuality = (index: number) => ({ @@ -53,8 +54,6 @@ export class MockPlayerBridge extends AbstractPlayerBridge { private muted = true; - private mutedListeners: ((value: boolean) => void)[] = []; - private playbackMode: PlaybackMode = 'LIVE'; private playbackState: PlaybackState = 'PLAYING'; @@ -69,6 +68,10 @@ export class MockPlayerBridge extends AbstractPlayerBridge { private qualitiesListeners: ((value?: Qualities) => void)[] = []; + private volume = 1; + + private volumeListeners: ((value: Volume) => void)[] = []; + private zeroTimestamp = Date.now(); constructor(target?: ConstructorParameters[0]) { @@ -176,7 +179,19 @@ export class MockPlayerBridge extends AbstractPlayerBridge { return; } this.muted = muted; - this.mutedListeners.forEach((listener) => listener(this.muted)); + this.volumeListeners.forEach((listener) => + listener({ muted: this.muted, volume: this.volume }), + ); + } + + protected setVolume(volume: number) { + if (volume === this.volume) { + return; + } + this.volume = volume; + this.volumeListeners.forEach((listener) => + listener({ muted: this.muted, volume: this.volume }), + ); } protected submitUserFeedback(value: UserFeedback) { @@ -235,11 +250,6 @@ export class MockPlayerBridge extends AbstractPlayerBridge { return this.playbackMode; } - protected subscribeMuted(listener: (value: boolean) => void) { - this.mutedListeners.push(listener); - return this.muted; - } - protected subscribePlaybackState( listener: (playbackState: PlaybackState) => void, ) { @@ -252,6 +262,11 @@ export class MockPlayerBridge extends AbstractPlayerBridge { return this.qualities; } + protected subscribeVolume(listener: (value: Volume) => void) { + this.volumeListeners.push(listener); + return { muted: this.muted, volume: this.volume }; + } + private setPlaybackState(playbackState: PlaybackState) { if (playbackState === this.playbackState) { return; diff --git a/src/livery-bridge-interactive/LiveryBridgeInteractive.ts b/src/livery-bridge-interactive/LiveryBridgeInteractive.ts index b9c671c..a704fae 100644 --- a/src/livery-bridge-interactive/LiveryBridgeInteractive.ts +++ b/src/livery-bridge-interactive/LiveryBridgeInteractive.ts @@ -32,6 +32,7 @@ const BRIDGE_GET_NAMES = [ 'setControlsDisabled', 'setDisplay', 'setMuted', + 'setVolume', 'submitUserFeedback', ] as const; const BRIDGE_SUBSCRIBE_NAMES = [ @@ -40,7 +41,6 @@ const BRIDGE_SUBSCRIBE_NAMES = [ 'subscribeError', 'subscribeFullscreen', 'subscribeMode', - 'subscribeMuted', 'subscribeOrientation', 'subscribePaused', 'subscribePlaybackState', @@ -49,6 +49,7 @@ const BRIDGE_SUBSCRIBE_NAMES = [ 'subscribeQuality', 'subscribeStalled', 'subscribeStreamPhase', + 'subscribeVolume', ] as const; type BridgeGetName = (typeof BRIDGE_GET_NAMES)[number]; @@ -279,6 +280,7 @@ export class LiveryBridgeInteractive extends LitElement { + @@ -337,7 +339,6 @@ export class LiveryBridgeInteractive extends LitElement { subscribeFullscreen - @@ -354,6 +355,7 @@ export class LiveryBridgeInteractive extends LitElement { + @@ -456,6 +458,7 @@ export class LiveryBridgeInteractive extends LitElement { switch (methodName) { case 'seek': + case 'setVolume': case 'selectQuality': { const inputElement = this.renderRoot.querySelector( '#getCommandNameInput', @@ -467,6 +470,10 @@ export class LiveryBridgeInteractive extends LitElement { inputElement.setAttribute('style', 'display: inline-block'); inputElement.setAttribute('type', 'number'); + inputElement.setAttribute( + 'step', + methodName === 'setVolume' ? 'any' : '1', + ); inputElement.value = ''; break; } @@ -478,6 +485,7 @@ export class LiveryBridgeInteractive extends LitElement { if (inputElement) { inputElement.setAttribute('style', 'display: none'); inputElement.setAttribute('type', 'text'); + inputElement.removeAttribute('step'); } } } @@ -567,9 +575,10 @@ export class LiveryBridgeInteractive extends LitElement { switch (methodName) { case 'seek': - case 'selectQuality': { + case 'selectQuality': + case 'setVolume': { const inputValue = getInputValue('Input'); - this.interactiveBridge[methodName](parseInt(inputValue, 10)).then( + this.interactiveBridge[methodName](parseFloat(inputValue)).then( setText, setText, ); diff --git a/src/util/schema.ts b/src/util/schema.ts index 84e9110..1e8ce6f 100644 --- a/src/util/schema.ts +++ b/src/util/schema.ts @@ -205,6 +205,15 @@ export type StreamPhase = z.infer; export const validateStreamPhase = createValidate(zStreamPhase); +const zVolume = z.object({ + muted: zBoolean, + volume: zNumber, +}); + +export type Volume = z.infer; + +export const validateVolume = createValidate(zVolume); + const zConfig = z.object({ /** Registry of controls that should be shown to the user. */ controls: z.object({