diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 1c7191f..36202f1 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -10,7 +10,8 @@ import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/pac import { PacketIceServers } from "../ProtocolProvider/protocol/packets/packet.ice.servers"; import { modals } from "@mantine/modals"; import { Button, Flex, Text } from "@mantine/core"; - +import { useSound } from "@/app/hooks/useSound"; +import useWindow from "@/app/hooks/useWindow"; export interface CallContextValue { call: (callable: string) => void; @@ -59,8 +60,9 @@ export interface CallProviderProps { export function CallProvider(props : CallProviderProps) { const [activeCall, setActiveCall] = useState(""); const [callState, setCallState] = useState(CallState.ENDED); - const [muted, setMuted] = useState(false); - const [sound, setSound] = useState(true); + const [muted, setMutedState] = useState(false); + const [sound, setSoundState] = useState(true); + const durationIntervalRef = useRef(null); const [duration, setDuration] = useState(0); const [showCallView, setShowCallView] = useState(callState == CallState.INCOMING); const {info} = useConsoleLogger("CallProvider"); @@ -73,8 +75,26 @@ export function CallProvider(props : CallProviderProps) { const roleRef = useRef(null); const [sharedSecret, setSharedSecret] = useState(""); const iceServersRef = useRef([]); - const remoteAudioRef = useRef(null); + const remoteAudioRef = useRef(null); const iceCandidatesBufferRef = useRef([]); + const mutedRef = useRef(false); + const soundRef = useRef(true); + + const {playSound, stopSound, stopLoopSound} = useSound(); + const {setWindowPriority} = useWindow(); + + + useEffect(() => { + if(callState == CallState.ACTIVE){ + stopLoopSound(); + stopSound(); + playSound("connected.mp3"); + setWindowPriority(false); + durationIntervalRef.current = setInterval(() => { + setDuration(prev => prev + 1); + }, 1000); + } + }, [callState]); useEffect(() => { /** @@ -83,6 +103,16 @@ export function CallProvider(props : CallProviderProps) { */ let packet = new PacketIceServers(); send(packet); + + return () => { + stopSound(); + if (remoteAudioRef.current) { + remoteAudioRef.current.pause(); + remoteAudioRef.current.srcObject = null; + } + peerConnectionRef.current?.close(); + peerConnectionRef.current = null; + }; }, []); usePacket(28, async (packet: PacketIceServers) => { @@ -168,43 +198,11 @@ export function CallProvider(props : CallProviderProps) { usePacket(26, async (packet: PacketSignalPeer) => { const signalType = packet.getSignalType(); if(signalType == SignalType.END_CALL_BECAUSE_BUSY) { - modals.open({ - title: 'Busy', - centered: true, - children: ( - <> - - Line is busy, the user is currently on another call. Please try again later. - - - - - - ), - withCloseButton: false - }); + openCallsModal("Line is busy, the user is currently on another call. Please try again later."); end(); } if(signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) { - modals.open({ - title: 'Connection lost', - centered: true, - children: ( - <> - - The connection with the user was lost. The call has ended. - - - - - - ), - withCloseButton: false - }); + openCallsModal("The connection with the user was lost. The call has ended.") end(); } if(activeCall){ @@ -228,6 +226,8 @@ export function CallProvider(props : CallProviderProps) { /** * Нам поступает звонок */ + setWindowPriority(true); + playSound("ringtone.mp3", true); setActiveCall(packet.getSrc()); setCallState(CallState.INCOMING); setShowCallView(true); @@ -325,10 +325,13 @@ export function CallProvider(props : CallProviderProps) { /** * При получении медиа-трека с другой стороны */ - console.info("TRACK RECV!!!!!"); - if(remoteAudioRef.current){ + if(remoteAudioRef.current && event.streams[0]){ console.info(event.streams); remoteAudioRef.current.srcObject = event.streams[0]; + remoteAudioRef.current.muted = !soundRef.current; + void remoteAudioRef.current.play().catch((e) => { + console.error("Failed to play remote audio:", e); + }); } } @@ -355,6 +358,25 @@ export function CallProvider(props : CallProviderProps) { } }, [activeCall, sessionKeys]); + const openCallsModal = (text : string) => { + modals.open({ + centered: true, + children: ( + <> + + {text} + + + + + + ), + withCloseButton: false + }); + } + const generateSessionKeys = () => { const sessionKeys = nacl.box.keyPair(); info("Generated keys for call session, len: " + sessionKeys.publicKey.length); @@ -363,6 +385,14 @@ export function CallProvider(props : CallProviderProps) { } const call = (dialog: string) => { + if(callState == CallState.ACTIVE + || callState == CallState.CONNECTING + || callState == CallState.KEY_EXCHANGE + || callState == CallState.WEB_RTC_EXCHANGE){ + openCallsModal("You are already on a call, please end the current call before starting a new one."); + return; + } + setWindowPriority(false); setActiveCall(dialog); setCallState(CallState.CONNECTING); setShowCallView(true); @@ -372,6 +402,7 @@ export function CallProvider(props : CallProviderProps) { signalPacket.setSignalType(SignalType.CALL); send(signalPacket); roleRef.current = CallRole.CALLER; + playSound("calling.mp3", true); } const close = () => { @@ -384,14 +415,28 @@ export function CallProvider(props : CallProviderProps) { } const end = () => { + stopLoopSound(); + stopSound(); + if (remoteAudioRef.current) { + remoteAudioRef.current.pause(); + remoteAudioRef.current.srcObject = null; + } + setDuration(0); + durationIntervalRef.current && clearInterval(durationIntervalRef.current); + setWindowPriority(false); + playSound("end_call.mp3"); peerConnectionRef.current?.close(); peerConnectionRef.current = null; roomIdRef.current = ""; + mutedRef.current = false; + soundRef.current = true; setActiveCall(""); setCallState(CallState.ENDED); setShowCallView(false); setSessionKeys(null); setDuration(0); + setMutedState(false); + setSoundState(true); roleRef.current = null; } @@ -402,6 +447,9 @@ export function CallProvider(props : CallProviderProps) { */ return; } + setWindowPriority(false); + stopLoopSound(); + stopSound(); /** * Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи */ @@ -428,6 +476,46 @@ export function CallProvider(props : CallProviderProps) { return sharedSecret; } + + const setMuted = (nextMuted: boolean) => { + if (mutedRef.current === nextMuted) { + return; + } + + mutedRef.current = nextMuted; + playSound(nextMuted ? "micro_enable.mp3" : "micro_disable.mp3"); + + if(peerConnectionRef.current){ + peerConnectionRef.current.getSenders().forEach(sender => { + if(sender.track?.kind == "audio"){ + sender.track.enabled = !nextMuted; + } + }); + } + + setMutedState(nextMuted); + } + + const setSound = (nextSound: boolean) => { + if (soundRef.current === nextSound) { + return; + } + + soundRef.current = nextSound; + playSound(nextSound ? "sound_enable.mp3" : "sound_disable.mp3"); + + if(remoteAudioRef.current){ + remoteAudioRef.current.muted = !nextSound; + if (nextSound) { + void remoteAudioRef.current.play().catch((e) => { + console.error("Failed to resume remote audio:", e); + }); + } + } + + setSoundState(nextSound); + } + const context = { call, close, diff --git a/lib/preload/preload.ts b/lib/preload/preload.ts index ce67cae..dbe848a 100644 --- a/lib/preload/preload.ts +++ b/lib/preload/preload.ts @@ -1,8 +1,23 @@ import { contextBridge, ipcRenderer, shell } from 'electron' import { electronAPI } from '@electron-toolkit/preload' import api from './api' +import { pathToFileURL } from 'node:url' +import path from 'node:path' +import fs from "node:fs"; +function resolveSound(fileName: string) { + const isDev = !process.env.APP_PACKAGED; // или свой флаг dev + const fullPath = isDev + ? path.join(process.cwd(), "resources", "sounds", fileName) + : path.join(process.resourcesPath, "resources", "sounds", fileName); + + if (!fs.existsSync(fullPath)) { + throw new Error(`Sound not found: ${fullPath}`); + } + return pathToFileURL(fullPath).toString(); +} + const exposeContext = async () => { if (process.contextIsolated) { try { @@ -16,6 +31,11 @@ const exposeContext = async () => { ipcRenderer.invoke('ipcCore:showItemInFolder', fullPath); } }); + contextBridge.exposeInMainWorld("mediaApi", { + getSoundUrl: (fileName: string) => { + return resolveSound(fileName); + } + }); } catch (error) { console.error(error) } @@ -23,6 +43,11 @@ const exposeContext = async () => { window.electron = electronAPI window.api = api; window.shell = shell; + window.mediaApi = { + getSoundUrl: (fileName: string) => { + return resolveSound(fileName); + } + } } } diff --git a/resources/sounds/calling.mp3 b/resources/sounds/calling.mp3 new file mode 100644 index 0000000..5a00cd0 Binary files /dev/null and b/resources/sounds/calling.mp3 differ diff --git a/resources/sounds/connected.mp3 b/resources/sounds/connected.mp3 new file mode 100644 index 0000000..3e030fa Binary files /dev/null and b/resources/sounds/connected.mp3 differ diff --git a/resources/sounds/end_call.mp3 b/resources/sounds/end_call.mp3 new file mode 100644 index 0000000..c58e9ed Binary files /dev/null and b/resources/sounds/end_call.mp3 differ diff --git a/resources/sounds/micro_disable.mp3 b/resources/sounds/micro_disable.mp3 new file mode 100644 index 0000000..b87de43 Binary files /dev/null and b/resources/sounds/micro_disable.mp3 differ diff --git a/resources/sounds/micro_enable.mp3 b/resources/sounds/micro_enable.mp3 new file mode 100644 index 0000000..fd78422 Binary files /dev/null and b/resources/sounds/micro_enable.mp3 differ diff --git a/resources/sounds/ringtone.mp3 b/resources/sounds/ringtone.mp3 new file mode 100644 index 0000000..6c576c2 Binary files /dev/null and b/resources/sounds/ringtone.mp3 differ diff --git a/resources/sounds/sound_disable.mp3 b/resources/sounds/sound_disable.mp3 new file mode 100644 index 0000000..8168123 Binary files /dev/null and b/resources/sounds/sound_disable.mp3 differ diff --git a/resources/sounds/sound_enable.mp3 b/resources/sounds/sound_enable.mp3 new file mode 100644 index 0000000..797c56e Binary files /dev/null and b/resources/sounds/sound_enable.mp3 differ