Files
advanced-camera-card/src/card-controller/call/ringtone.ts
T

88 lines
2.7 KiB
TypeScript

import { RingtoneConfig } from '../../config/schema/live';
import { ArpeggioTone } from './tones/arpeggio';
import { ChimeTone } from './tones/chime';
import { CustomTone } from './tones/custom';
import { MelodyTone } from './tones/melody';
import { Tone } from './tones/types';
import { WestminsterTone } from './tones/westminster';
// Module-level singleton lock: only one `Ringtone` plays at a time across all
// card instances on the page. The HA dashboard can render multiple card
// instances simultaneously (e.g. dashboard card + editor preview, or the same
// card placed twice), all of which may independently react to the same trigger
// state change -- with no lock, every instance would start its own AudioContext
// and the audio would layer. First-to-start wins; subsequent `start()` calls
// from other holders are no-ops until the active one releases via `stop()`. The
// lock auto-recovers from a holder that forgot to release (e.g. a controller
// GC'd without disconnect cleanup) via the `isPlaying()` sweep below.
const sharedLock = new Set<Ringtone>();
export class Ringtone {
private _tone: Tone | null = null;
private readonly lock: Set<Ringtone>;
// The `lock` parameter defaults to the module-level singleton so production
// callers (`new Ringtone()`) get cross-instance coordination automatically.
// Test callers can pass `new Set()` per test to isolate state without
// touching a process-wide value.
constructor(lock: Set<Ringtone> = sharedLock) {
this.lock = lock;
}
public start(config: RingtoneConfig): void {
if (this._tone) {
return;
}
// Drop any stale holders before checking the lock.
for (const other of this.lock) {
if (!other.isPlaying()) {
this.lock.delete(other);
}
}
// Another tone is already ringing.
if (this.lock.size) {
return;
}
this._tone = this._createTone(config);
if (this._tone) {
this.lock.add(this);
this._tone.start(() => this._handleToneEnd());
}
}
public stop(): void {
this._tone?.stop();
this._tone = null;
this.lock.delete(this);
}
public isPlaying(): boolean {
return !!this._tone;
}
private _handleToneEnd(): void {
this._tone = null;
this.lock.delete(this);
}
private _createTone(config: RingtoneConfig): Tone | null {
switch (config.type) {
case 'chime':
return new ChimeTone(config.repeat);
case 'westminster':
return new WestminsterTone(config.repeat);
case 'arpeggio':
return new ArpeggioTone(config.repeat);
case 'melody':
return new MelodyTone(config.repeat);
case 'custom':
return config.url ? new CustomTone(config.url, config.repeat) : null;
case 'none':
return null;
}
}
}