feat(devicemanager): Introduce a UniversalDevice architecture with composable Feature system; add extensive new device/protocol support and discovery/refactors
This commit is contained in:
256
ts/features/feature.volume.ts
Normal file
256
ts/features/feature.volume.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user