162 lines
4.1 KiB
TypeScript
162 lines
4.1 KiB
TypeScript
/* eslint-disable no-console */
|
|
|
|
/**
|
|
* Sound types that can be played during a call
|
|
*/
|
|
export enum CallSoundType {
|
|
MicMute = 'mic_mute',
|
|
MicUnmute = 'mic_unmute',
|
|
Deafen = 'deafen',
|
|
Undeafen = 'undeafen',
|
|
CallJoined = 'call_joined',
|
|
CallDepart = 'call_depart',
|
|
CallDialing = 'call_dialing',
|
|
}
|
|
|
|
const SOUND_FILES: Record<CallSoundType, string> = {
|
|
[CallSoundType.MicMute]: 'Mic_Mute.ogg',
|
|
[CallSoundType.MicUnmute]: 'Mic_Unmuted.ogg',
|
|
[CallSoundType.Deafen]: 'Speakers_Deafen.ogg',
|
|
[CallSoundType.Undeafen]: 'Speakers_Undeafen.ogg',
|
|
[CallSoundType.CallJoined]: 'Call_Joined.ogg',
|
|
[CallSoundType.CallDepart]: 'Call_Depart.ogg',
|
|
[CallSoundType.CallDialing]: 'Call_Dialing.ogg',
|
|
};
|
|
|
|
/**
|
|
* Resolves the base URL for sounds.
|
|
* In Electron production, sounds are in resources/sound/ as plain files.
|
|
* In dev or browser, sounds are served from public/sound/.
|
|
*/
|
|
async function resolveSoundBaseUrl(): Promise<string> {
|
|
const electronAudio = (window as any).electron?.audio;
|
|
if (electronAudio?.getSoundBaseUrl) {
|
|
const base: string | null = await electronAudio.getSoundBaseUrl();
|
|
if (base) return base;
|
|
}
|
|
return './sound';
|
|
}
|
|
|
|
/**
|
|
* Manages call-related sound effects.
|
|
* Handles playing local sounds and coordinating with remote participants.
|
|
*/
|
|
export class CallSounds {
|
|
private audioElements: Map<CallSoundType, HTMLAudioElement> = new Map();
|
|
|
|
private dialingLoopElement: HTMLAudioElement | null = null;
|
|
|
|
private isDeafened = false;
|
|
|
|
constructor() {
|
|
resolveSoundBaseUrl().then((base) => this.preloadSounds(base));
|
|
}
|
|
|
|
/**
|
|
* Preloads all call sounds for instant playback
|
|
*/
|
|
private preloadSounds(base: string): void {
|
|
Object.entries(SOUND_FILES).forEach(([type, file]) => {
|
|
const audio = new Audio(`${base}/${file}`);
|
|
audio.preload = 'auto';
|
|
this.audioElements.set(type as CallSoundType, audio);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Plays a call sound locally
|
|
* @param soundType - The type of sound to play
|
|
* @param loop - Whether to loop the sound (e.g., for dialing)
|
|
*/
|
|
playSound(soundType: CallSoundType, loop = false): void {
|
|
// Don't play sounds if deafened (except deafen/undeafen sounds for feedback)
|
|
if (this.isDeafened &&
|
|
soundType !== CallSoundType.Deafen &&
|
|
soundType !== CallSoundType.Undeafen) {
|
|
return;
|
|
}
|
|
|
|
const audio = this.audioElements.get(soundType);
|
|
if (!audio) {
|
|
console.warn(`Sound not found: ${soundType}`);
|
|
return;
|
|
}
|
|
|
|
// Stop any currently playing instance and reset
|
|
audio.currentTime = 0;
|
|
audio.loop = loop;
|
|
|
|
// Track looping dialing sound separately
|
|
if (soundType === CallSoundType.CallDialing && loop) {
|
|
this.dialingLoopElement = audio;
|
|
}
|
|
|
|
audio.play().catch((err) => {
|
|
console.warn(`Failed to play sound ${soundType}:`, err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stops the dialing loop sound if playing
|
|
*/
|
|
stopDialingLoop(): void {
|
|
if (this.dialingLoopElement) {
|
|
this.dialingLoopElement.pause();
|
|
this.dialingLoopElement.currentTime = 0;
|
|
this.dialingLoopElement.loop = false;
|
|
this.dialingLoopElement = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the deafened state - affects whether sounds are played
|
|
* @param deafened - Whether the user is deafened
|
|
*/
|
|
setDeafened(deafened: boolean): void {
|
|
this.isDeafened = deafened;
|
|
}
|
|
|
|
/**
|
|
* Gets whether sounds are currently deafened
|
|
*/
|
|
getIsDeafened(): boolean {
|
|
return this.isDeafened;
|
|
}
|
|
|
|
/**
|
|
* Cleans up audio resources
|
|
*/
|
|
dispose(): void {
|
|
this.stopDialingLoop();
|
|
this.audioElements.forEach((audio) => {
|
|
audio.pause();
|
|
});
|
|
this.audioElements.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Singleton instance of CallSounds for app-wide use
|
|
*/
|
|
let callSoundsInstance: CallSounds | null = null;
|
|
|
|
/**
|
|
* Gets or creates the CallSounds singleton instance
|
|
*/
|
|
export function getCallSounds(): CallSounds {
|
|
if (!callSoundsInstance) {
|
|
callSoundsInstance = new CallSounds();
|
|
}
|
|
return callSoundsInstance;
|
|
}
|
|
|
|
/**
|
|
* Disposes the CallSounds singleton
|
|
*/
|
|
export function disposeCallSounds(): void {
|
|
if (callSoundsInstance) {
|
|
callSoundsInstance.dispose();
|
|
callSoundsInstance = null;
|
|
}
|
|
}
|