diff --git a/src/lib/components/calling/CallScreen.svelte b/src/lib/components/calling/CallScreen.svelte index b58bab845..bc8e433b3 100644 --- a/src/lib/components/calling/CallScreen.svelte +++ b/src/lib/components/calling/CallScreen.svelte @@ -12,9 +12,10 @@ import { _ } from "svelte-i18n" import type { Chat } from "$lib/types" import VolumeMixer from "./VolumeMixer.svelte" - import { onDestroy, onMount } from "svelte" - import { callTimeout, TIME_TO_SHOW_CONNECTING, VoiceRTCInstance } from "$lib/media/Voice" + import { createEventDispatcher, onDestroy, onMount } from "svelte" + import { callTimeout, connectionOpened, TIME_TO_SHOW_CONNECTING, usersAcceptedTheCall, VoiceRTCInstance } from "$lib/media/Voice" import { log } from "$lib/utils/Logger" + import { playSound, SoundHandler, Sounds } from "../utils/SoundHandler" export let expanded: boolean = false function toggleExanded() { @@ -30,6 +31,8 @@ export let deafened: boolean = get(Store.state.devices.deafened) export let chat: Chat + let dispatch = createEventDispatcher() + function toggleFullscreen() { const elem = document.getElementById("call-screen") @@ -117,15 +120,26 @@ let showAnimation = true let message = $_("settings.calling.connecting") let timeout: NodeJS.Timeout | undefined + let callSound: SoundHandler | undefined = undefined + + $: if ($usersAcceptedTheCall.length > 0) { + callSound?.stop() + callSound = undefined + } onMount(async () => { + callTimeout.set(false) + usersAcceptedTheCall.set([]) document.addEventListener("mousedown", handleClickOutside) await VoiceRTCInstance.setVideoElements(localVideoCurrentSrc) /// HACK: To make sure the video elements are loaded before we start the call if (VoiceRTCInstance.localVideoCurrentSrc && VoiceRTCInstance.remoteVideoCreator) { if (VoiceRTCInstance.toCall && VoiceRTCInstance.toCall.find(did => did !== "") !== undefined) { + callSound = await playSound(Sounds.OutgoingCall) await VoiceRTCInstance.makeCall() timeout = setTimeout(() => { + callSound?.stop() + callSound = undefined showAnimation = false message = $_("settings.calling.noResponse") }, TIME_TO_SHOW_CONNECTING) @@ -138,6 +152,7 @@ }) onDestroy(() => { + callTimeout.set(false) document.removeEventListener("mousedown", handleClickOutside) subscribeOne() subscribeTwo() @@ -146,6 +161,8 @@ if (timeout) { clearTimeout(timeout) } + callSound?.stop() + callSound = undefined }) @@ -185,13 +202,18 @@ {#if user === get(Store.state.user).key && !userCallOptions.video.enabled} {:else if $userCache[user] && $userCache[user].key !== get(Store.state.user).key && VoiceRTCInstance.toCall && !$remoteStreams[user]} - {#if showAnimation} + {#if showAnimation && !$usersAcceptedTheCall.includes(user)}

{message}

+ {:else if $usersAcceptedTheCall.includes(user)} +
+ +

{$_("settings.calling.acceptedCall")}

+
{:else}
@@ -322,6 +344,7 @@ on:click={_ => { Store.endCall() VoiceRTCInstance.leaveCall() + dispatch("endCall") }}> diff --git a/src/lib/components/calling/IncomingCall.svelte b/src/lib/components/calling/IncomingCall.svelte index 6cc789ec8..80a5b7561 100644 --- a/src/lib/components/calling/IncomingCall.svelte +++ b/src/lib/components/calling/IncomingCall.svelte @@ -35,8 +35,7 @@ Store.state.pendingCall.subscribe(async _ => { if (VoiceRTCInstance.incomingCallFrom && !VoiceRTCInstance.toCall && $connectionOpened) { if (callSound === null || callSound === undefined) { - callSound = playSound(Sounds.IncomingCall) - callSound.play() + callSound = await playSound(Sounds.IncomingCall) } pending = true let chat = UIStore.getChat(VoiceRTCInstance.incomingCallFrom[1].metadata.channel) @@ -48,11 +47,11 @@ user = Store.getUser(VoiceRTCInstance.incomingCallFrom[1].metadata.did) } else if (!$connectionOpened) { cancelledCall = true + callSound?.stop() + callSound = undefined timeOutToCancel = setTimeout(() => { cancelledCall = false pending = false - callSound?.stop() - callSound = undefined }, 4000) } else { pending = false diff --git a/src/lib/components/utils/SoundHandler.ts b/src/lib/components/utils/SoundHandler.ts index 2ef6ffc06..16808f8ba 100644 --- a/src/lib/components/utils/SoundHandler.ts +++ b/src/lib/components/utils/SoundHandler.ts @@ -1,3 +1,4 @@ +import { log } from "$lib/utils/Logger" import { Howl } from "howler" export enum Sounds { @@ -23,15 +24,17 @@ export type SoundOption = { export class SoundHandler { private sound: Howl + public soundID: number = -1 private muted: boolean = false - constructor(sound: Howl) { + constructor(sound: Howl, soundID: number) { this.sound = sound + this.soundID = soundID } mute() { this.muted = !this.muted - this.sound.mute(!this.muted) + this.sound.mute(!this.muted, this.soundID) } isMuted(): boolean { @@ -39,30 +42,46 @@ export class SoundHandler { } pause() { - this.sound.pause() + this.sound.pause(this.soundID) } play() { - this.sound.play() + this.sound.play(undefined, true) } paused(): boolean { - return !this.sound.playing() + return !this.sound.playing(this.soundID) } stop() { - this.sound.stop() + this.sound.stop(0, true) this.sound.unload() } } -export function playSound(src: string | Sounds, opt?: SoundOption): SoundHandler { - var sound = new Howl({ - src: [src.toString()], - html5: opt?.large, - volume: opt?.volume ? opt?.volume : 1, - loop: opt?.loop, - }) - sound.play() - return new SoundHandler(sound) +export async function playSound(src: string | Sounds, opt?: SoundOption): Promise { + try { + // Ask for permission to play sound + if (Notification.permission !== "granted") { + const permission = await Notification.requestPermission() + if (permission !== "granted") { + log.warn("Permission to play sound not granted") + return undefined + } + } + + const sound = new Howl({ + src: [src.toString()], + html5: opt?.large || false, + volume: opt?.volume || 1, + loop: opt?.loop || false, + }) + + let soundID = sound.play(undefined, false) + + return new SoundHandler(sound, soundID) + } catch (error) { + log.error("Error to play callsound:", error) + return undefined + } } diff --git a/src/lib/lang/en.json b/src/lib/lang/en.json index c5ea93788..d4d53c904 100644 --- a/src/lib/lang/en.json +++ b/src/lib/lang/en.json @@ -347,6 +347,7 @@ "disconnecting": "Disconnecting...", "userInviteToAGroupCall": "is inviting you to join a group call", "noAnswer": "No answer, leaving the call...", + "acceptedCall": "Joined, loading...", "connecting": "Connecting...", "noResponse": "No response" }, diff --git a/src/lib/media/Voice.ts b/src/lib/media/Voice.ts index b8f1bfa99..9eda0a83f 100644 --- a/src/lib/media/Voice.ts +++ b/src/lib/media/Voice.ts @@ -1,12 +1,11 @@ import { CallDirection } from "$lib/enums" -import { SettingsStore } from "$lib/state" import { Store } from "$lib/state/Store" import { create_cancellable_handler, type Cancellable } from "$lib/utils/CancellablePromise" import { log } from "$lib/utils/Logger" import { RaygunStoreInstance } from "$lib/wasm/RaygunStore" import Peer, { DataConnection } from "peerjs" import { _ } from "svelte-i18n" -import { get, writable } from "svelte/store" +import { get, writable, type Writable } from "svelte/store" import type { Room } from "trystero" import { joinRoom } from "trystero/ipfs" @@ -17,6 +16,7 @@ const TIME_TO_SHOW_END_CALL_FEEDBACK = 3500 export const TIME_TO_SHOW_CONNECTING = 30000 let timeOuts: NodeJS.Timeout[] = [] +export const usersAcceptedTheCall: Writable = writable([]) export enum VoiceRTCMessageType { UpdateUser = "UPDATE_USER", @@ -507,6 +507,8 @@ export class VoiceRTC { }) conn.once("data", d => { if (d === CALL_ACK) { + callTimeout.set(false) + usersAcceptedTheCall.set([...get(usersAcceptedTheCall), did]) accepted = true } }) @@ -591,7 +593,9 @@ export class VoiceRTC { } async leaveCall(sendEndCallMessage = false) { + callTimeout.set(false) connectionOpened.set(false) + usersAcceptedTheCall.set([]) timeOuts.forEach(t => clearTimeout(t)) sendEndCallMessage = sendEndCallMessage && this.channel !== undefined && this.call != null if (sendEndCallMessage && this.call?.start) { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e1b2b149f..c3a5b5d9c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -231,6 +231,33 @@ console.log("Arriving here on +layout") + window.addEventListener( + "click", + () => { + initializeAudioContext() + log.debug("Audio context unlocked after click.") + }, + { once: true } + ) + + window.addEventListener( + "touchstart", + () => { + initializeAudioContext() + log.debug("Audio context unlocked after touch.") + }, + { once: true } + ) + + function initializeAudioContext() { + const audioContext = new window.AudioContext() + if (audioContext.state === "suspended") { + audioContext.resume().then(() => { + log.info("Audio context unlocked.") + }) + } + } + let isLocaleSet = false $: if ($locale) { diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte index 5611cc130..e467c7e92 100644 --- a/src/routes/chat/+page.svelte +++ b/src/routes/chat/+page.svelte @@ -655,7 +655,12 @@ {/if} {#if activeCallInProgress && activeCallDid === $activeChat.id} - + { + activeCallInProgress = false + activeCallDid = "" + }} /> {/if}