feat(call): add sound effects and incoming call notification UI
- Implemented sound management for call events including mute, unmute, deafen, undeafen, call joined, call depart, and dialing sounds. - Created a new CallSounds class to handle sound playback and state management. - Added IncomingCallNotification component to display incoming call alerts with caller information and action buttons. - Styled the incoming call notification using vanilla-extract CSS for animations and layout. - Integrated sound playback for incoming calls with a 15-second threshold for dialing sound.
This commit is contained in:
157
src/app/features/call/CallSounds.ts
Normal file
157
src/app/features/call/CallSounds.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable no-console */
|
||||
import MicMuteSound from '../../../../public/sound/Mic_Mute.ogg';
|
||||
import MicUnmuteSound from '../../../../public/sound/Mic_Unmuted.ogg';
|
||||
import SpeakersDeafenSound from '../../../../public/sound/Speakers_Deafen.ogg';
|
||||
import SpeakersUndeafenSound from '../../../../public/sound/Speakers_Undeafen.ogg';
|
||||
import CallJoinedSound from '../../../../public/sound/Call_Joined.ogg';
|
||||
import CallDepartSound from '../../../../public/sound/Call_Depart.ogg';
|
||||
import CallDialingSound from '../../../../public/sound/Call_Dialing.ogg';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps sound types to their audio sources
|
||||
*/
|
||||
const SOUND_SOURCES: Record<CallSoundType, string> = {
|
||||
[CallSoundType.MicMute]: MicMuteSound,
|
||||
[CallSoundType.MicUnmute]: MicUnmuteSound,
|
||||
[CallSoundType.Deafen]: SpeakersDeafenSound,
|
||||
[CallSoundType.Undeafen]: SpeakersUndeafenSound,
|
||||
[CallSoundType.CallJoined]: CallJoinedSound,
|
||||
[CallSoundType.CallDepart]: CallDepartSound,
|
||||
[CallSoundType.CallDialing]: CallDialingSound,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
this.preloadSounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads all call sounds for instant playback
|
||||
*/
|
||||
private preloadSounds(): void {
|
||||
Object.entries(SOUND_SOURCES).forEach(([type, src]) => {
|
||||
const audio = new Audio(src);
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user