Files
devicemanager/ts/features/feature.volume.ts

257 lines
6.8 KiB
TypeScript
Raw Permalink Normal View History

/**
* Volume Feature
* Provides volume control capability
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type {
IVolumeFeatureInfo,
IFeatureOptions,
} from '../interfaces/feature.interfaces.js';
/**
* Options for creating a VolumeFeature
*/
export interface IVolumeFeatureOptions extends IFeatureOptions {
/** Volume control protocol (usually same as device protocol) */
volumeProtocol?: string;
/** Minimum volume level */
minVolume?: number;
/** Maximum volume level */
maxVolume?: number;
/** Volume step increment */
volumeStep?: number;
/** Whether mute is supported */
supportsMute?: boolean;
/** Protocol-specific volume controller */
volumeController?: IVolumeController;
}
/**
* Interface for protocol-specific volume control
*/
export interface IVolumeController {
getVolume(): Promise<number>;
setVolume(level: number): Promise<void>;
getMute(): Promise<boolean>;
setMute(muted: boolean): Promise<void>;
}
/**
* Volume Feature - provides volume control capability
*
* Separated from PlaybackFeature because some devices have volume control
* without playback capability (e.g., amplifiers, HDMI matrix switches).
*
* @example
* ```typescript
* const volume = device.getFeature<VolumeFeature>('volume');
* if (volume) {
* const current = await volume.getVolume();
* await volume.setVolume(current + 10);
* await volume.toggleMute();
* }
* ```
*/
export class VolumeFeature extends Feature {
public readonly type = 'volume' as const;
public readonly protocol: string;
// Capabilities
public readonly minVolume: number;
public readonly maxVolume: number;
public readonly volumeStep: number;
public readonly supportsMute: boolean;
// Current state
protected _volume: number = 0;
protected _muted: boolean = false;
// Volume controller (protocol-specific)
protected volumeController: IVolumeController | null = null;
constructor(
device: TDeviceReference,
port: number,
options?: IVolumeFeatureOptions
) {
super(device, port, options);
this.protocol = options?.volumeProtocol ?? 'generic';
this.minVolume = options?.minVolume ?? 0;
this.maxVolume = options?.maxVolume ?? 100;
this.volumeStep = options?.volumeStep ?? 5;
this.supportsMute = options?.supportsMute ?? true;
if (options?.volumeController) {
this.volumeController = options.volumeController;
}
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current volume level (cached)
*/
public get volume(): number {
return this._volume;
}
/**
* Get mute state (cached)
*/
public get muted(): boolean {
return this._muted;
}
// ============================================================================
// Connection
// ============================================================================
protected async doConnect(): Promise<void> {
// Fetch initial state if we have a controller
if (this.volumeController) {
try {
this._volume = await this.volumeController.getVolume();
this._muted = await this.volumeController.getMute();
} catch {
// Ignore errors fetching initial state
}
}
}
protected async doDisconnect(): Promise<void> {
// Nothing to disconnect for volume control
}
// ============================================================================
// Volume Control
// ============================================================================
/**
* Get current volume level
*/
public async getVolume(): Promise<number> {
if (this.volumeController) {
this._volume = await this.volumeController.getVolume();
}
return this._volume;
}
/**
* Set volume level
* @param level Volume level (clamped to min/max)
*/
public async setVolume(level: number): Promise<void> {
const clampedLevel = Math.max(this.minVolume, Math.min(this.maxVolume, level));
if (this.volumeController) {
await this.volumeController.setVolume(clampedLevel);
}
const oldVolume = this._volume;
this._volume = clampedLevel;
if (oldVolume !== clampedLevel) {
this.emit('volume:changed', { oldVolume, newVolume: clampedLevel });
}
}
/**
* Increase volume by step
*/
public async volumeUp(step?: number): Promise<number> {
const increment = step ?? this.volumeStep;
const newVolume = Math.min(this.maxVolume, this._volume + increment);
await this.setVolume(newVolume);
return this._volume;
}
/**
* Decrease volume by step
*/
public async volumeDown(step?: number): Promise<number> {
const decrement = step ?? this.volumeStep;
const newVolume = Math.max(this.minVolume, this._volume - decrement);
await this.setVolume(newVolume);
return this._volume;
}
/**
* Get mute state
*/
public async getMute(): Promise<boolean> {
if (this.volumeController && this.supportsMute) {
this._muted = await this.volumeController.getMute();
}
return this._muted;
}
/**
* Set mute state
*/
public async setMute(muted: boolean): Promise<void> {
if (!this.supportsMute) {
throw new Error('Mute not supported');
}
if (this.volumeController) {
await this.volumeController.setMute(muted);
}
const oldMuted = this._muted;
this._muted = muted;
if (oldMuted !== muted) {
this.emit('mute:changed', { oldMuted, newMuted: muted });
}
}
/**
* Toggle mute state
*/
public async toggleMute(): Promise<boolean> {
const currentMuted = await this.getMute();
await this.setMute(!currentMuted);
return this._muted;
}
// ============================================================================
// Serialization
// ============================================================================
public getFeatureInfo(): IVolumeFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'volume',
minVolume: this.minVolume,
maxVolume: this.maxVolume,
volumeStep: this.volumeStep,
supportsMute: this.supportsMute,
};
}
// ============================================================================
// Static Factory
// ============================================================================
/**
* Create from discovery metadata
*/
public static fromDiscovery(
device: TDeviceReference,
port: number,
protocol: string,
metadata: Record<string, unknown>
): VolumeFeature {
return new VolumeFeature(device, port, {
volumeProtocol: protocol,
minVolume: metadata.minVolume as number ?? 0,
maxVolume: metadata.maxVolume as number ?? 100,
volumeStep: metadata.volumeStep as number ?? 5,
supportsMute: metadata.supportsMute as boolean ?? true,
});
}
}