TeaWeb/web/app/audio/Player.ts

148 lines
4.4 KiB
TypeScript

import {AudioBackend, AudioBackendEvents, OutputDevice} from "tc-shared/audio/Player";
import {LogCategory, logDebug, logError, logInfo, logWarn} from "tc-shared/log";
import {tr} from "tc-shared/i18n/localize";
import {Registry} from "tc-events";
const kWebDevice: OutputDevice = {
deviceId: "default",
name: "default playback",
driver: 'Web Audio'
};
export class WebAudioBackend implements AudioBackend {
private readonly events: Registry<AudioBackendEvents>;
private readonly audioContext: AudioContext;
private state: "initializing" | "running" | "closed";
private masterVolume: number;
private gestureListener: () => void;
constructor() {
this.events = new Registry<AudioBackendEvents>();
this.state = "initializing";
this.masterVolume = 1;
this.audioContext = new (window.webkitAudioContext || window.AudioContext)();
this.audioContext.onstatechange = () => this.handleAudioContextStateChanged(false);
this.handleAudioContextStateChanged(true);
}
destroy() {
this.state = "closed";
document.removeEventListener("click", this.gestureListener);
this.gestureListener = undefined;
this.audioContext.close().catch(error => {
logWarn(LogCategory.AUDIO, tr("Failed to close AudioContext: %o"), error);
});
}
executeWhenInitialized(callback: () => void) {
if(this.state === "running") {
callback();
} else {
this.events.one("notify_initialized", callback);
}
}
isInitialized(): boolean {
return this.state === "running";
}
getAudioContext(): AudioContext | undefined {
return this.audioContext;
}
async getAvailableDevices(): Promise<OutputDevice[]> {
return [ kWebDevice ];
}
getDefaultDeviceId(): string {
return kWebDevice.deviceId;
}
getMasterVolume(): number {
return this.masterVolume;
}
setMasterVolume(volume: number) {
if(this.masterVolume === volume) {
return;
}
const oldVolume = this.masterVolume;
this.masterVolume = volume;
this.events.fire("notify_volume_changed", {
oldVolume: oldVolume,
newVolume: volume
});
}
isDeviceRefreshAvailable(): boolean {
return false;
}
refreshDevices(): Promise<void> {
return Promise.resolve(undefined);
}
private handleAudioContextStateChanged(initialState: boolean) {
switch (this.audioContext.state) {
case "suspended":
if(initialState) {
logDebug(LogCategory.AUDIO, tr("Created new AudioContext but user hasn't yet allowed audio playback. Awaiting his gesture."));
this.awaitGesture();
return;
} else {
logWarn(LogCategory.AUDIO, tr("AudioContext state changed to 'suspended'. Trying to resume it."));
this.tryResume();
}
break;
case "closed":
if(this.state === "closed") {
return;
}
logError(LogCategory.AUDIO, tr("AudioContext state changed to 'closed'. No audio will be payed."));
return;
case "running":
logDebug(LogCategory.AUDIO, tr("Successfully initialized the AudioContext."));
this.state = "running";
this.events.fire("notify_initialized");
return;
}
}
private tryResume() {
this.audioContext.resume().then(() => {
logInfo(LogCategory.AUDIO, tr("Successfully resumed AudioContext."));
}).catch(error => {
logError(LogCategory.AUDIO, tr("Failed to resume AudioContext: %o"), error);
});
}
private awaitGesture() {
if(this.gestureListener) {
return;
}
this.gestureListener = () => {
document.removeEventListener("click", this.gestureListener);
this.gestureListener = undefined;
this.tryResume();
};
document.addEventListener("click", this.gestureListener);
}
async setCurrentDevice(targetId: string | undefined): Promise<void> {
/* TODO: Mute on "no device"? */
}
getCurrentDevice(): OutputDevice {
return kWebDevice;
}
}