Событийные звуки звонка (сбросить, мутинг, и прочее...)

This commit is contained in:
RoyceDa
2026-03-18 18:27:39 +02:00
parent 7b9936dcc4
commit 88288317ab
10 changed files with 153 additions and 40 deletions

View File

@@ -10,7 +10,8 @@ import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/pac
import { PacketIceServers } from "../ProtocolProvider/protocol/packets/packet.ice.servers"; import { PacketIceServers } from "../ProtocolProvider/protocol/packets/packet.ice.servers";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { Button, Flex, Text } from "@mantine/core"; import { Button, Flex, Text } from "@mantine/core";
import { useSound } from "@/app/hooks/useSound";
import useWindow from "@/app/hooks/useWindow";
export interface CallContextValue { export interface CallContextValue {
call: (callable: string) => void; call: (callable: string) => void;
@@ -59,8 +60,9 @@ export interface CallProviderProps {
export function CallProvider(props : CallProviderProps) { export function CallProvider(props : CallProviderProps) {
const [activeCall, setActiveCall] = useState<string>(""); const [activeCall, setActiveCall] = useState<string>("");
const [callState, setCallState] = useState<CallState>(CallState.ENDED); const [callState, setCallState] = useState<CallState>(CallState.ENDED);
const [muted, setMuted] = useState<boolean>(false); const [muted, setMutedState] = useState<boolean>(false);
const [sound, setSound] = useState<boolean>(true); const [sound, setSoundState] = useState<boolean>(true);
const durationIntervalRef = useRef<NodeJS.Timeout | null>(null);
const [duration, setDuration] = useState<number>(0); const [duration, setDuration] = useState<number>(0);
const [showCallView, setShowCallView] = useState<boolean>(callState == CallState.INCOMING); const [showCallView, setShowCallView] = useState<boolean>(callState == CallState.INCOMING);
const {info} = useConsoleLogger("CallProvider"); const {info} = useConsoleLogger("CallProvider");
@@ -73,8 +75,26 @@ export function CallProvider(props : CallProviderProps) {
const roleRef = useRef<CallRole | null>(null); const roleRef = useRef<CallRole | null>(null);
const [sharedSecret, setSharedSecret] = useState<string>(""); const [sharedSecret, setSharedSecret] = useState<string>("");
const iceServersRef = useRef<RTCIceServer[]>([]); const iceServersRef = useRef<RTCIceServer[]>([]);
const remoteAudioRef = useRef<HTMLAudioElement>(null); const remoteAudioRef = useRef<HTMLAudioElement | null>(null);
const iceCandidatesBufferRef = useRef<RTCIceCandidate[]>([]); const iceCandidatesBufferRef = useRef<RTCIceCandidate[]>([]);
const mutedRef = useRef<boolean>(false);
const soundRef = useRef<boolean>(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(() => { useEffect(() => {
/** /**
@@ -83,6 +103,16 @@ export function CallProvider(props : CallProviderProps) {
*/ */
let packet = new PacketIceServers(); let packet = new PacketIceServers();
send(packet); 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) => { usePacket(28, async (packet: PacketIceServers) => {
@@ -168,43 +198,11 @@ export function CallProvider(props : CallProviderProps) {
usePacket(26, async (packet: PacketSignalPeer) => { usePacket(26, async (packet: PacketSignalPeer) => {
const signalType = packet.getSignalType(); const signalType = packet.getSignalType();
if(signalType == SignalType.END_CALL_BECAUSE_BUSY) { if(signalType == SignalType.END_CALL_BECAUSE_BUSY) {
modals.open({ openCallsModal("Line is busy, the user is currently on another call. Please try again later.");
title: 'Busy',
centered: true,
children: (
<>
<Text size="sm">
Line is busy, the user is currently on another call. Please try again later.
</Text>
<Flex align={'center'} justify={'flex-end'}>
<Button color={'red'} variant={'subtle'} onClick={() => modals.closeAll()} mt="md">
Close
</Button>
</Flex>
</>
),
withCloseButton: false
});
end(); end();
} }
if(signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) { if(signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) {
modals.open({ openCallsModal("The connection with the user was lost. The call has ended.")
title: 'Connection lost',
centered: true,
children: (
<>
<Text size="sm">
The connection with the user was lost. The call has ended.
</Text>
<Flex align={'center'} justify={'flex-end'}>
<Button color={'red'} variant={'subtle'} onClick={() => modals.closeAll()} mt="md">
Close
</Button>
</Flex>
</>
),
withCloseButton: false
});
end(); end();
} }
if(activeCall){ if(activeCall){
@@ -228,6 +226,8 @@ export function CallProvider(props : CallProviderProps) {
/** /**
* Нам поступает звонок * Нам поступает звонок
*/ */
setWindowPriority(true);
playSound("ringtone.mp3", true);
setActiveCall(packet.getSrc()); setActiveCall(packet.getSrc());
setCallState(CallState.INCOMING); setCallState(CallState.INCOMING);
setShowCallView(true); setShowCallView(true);
@@ -325,10 +325,13 @@ export function CallProvider(props : CallProviderProps) {
/** /**
* При получении медиа-трека с другой стороны * При получении медиа-трека с другой стороны
*/ */
console.info("TRACK RECV!!!!!"); if(remoteAudioRef.current && event.streams[0]){
if(remoteAudioRef.current){
console.info(event.streams); console.info(event.streams);
remoteAudioRef.current.srcObject = event.streams[0]; 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]); }, [activeCall, sessionKeys]);
const openCallsModal = (text : string) => {
modals.open({
centered: true,
children: (
<>
<Text size="sm">
{text}
</Text>
<Flex align={'center'} justify={'flex-end'}>
<Button color={'red'} variant={'subtle'} onClick={() => modals.closeAll()} mt="md">
Close
</Button>
</Flex>
</>
),
withCloseButton: false
});
}
const generateSessionKeys = () => { const generateSessionKeys = () => {
const sessionKeys = nacl.box.keyPair(); const sessionKeys = nacl.box.keyPair();
info("Generated keys for call session, len: " + sessionKeys.publicKey.length); info("Generated keys for call session, len: " + sessionKeys.publicKey.length);
@@ -363,6 +385,14 @@ export function CallProvider(props : CallProviderProps) {
} }
const call = (dialog: string) => { 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); setActiveCall(dialog);
setCallState(CallState.CONNECTING); setCallState(CallState.CONNECTING);
setShowCallView(true); setShowCallView(true);
@@ -372,6 +402,7 @@ export function CallProvider(props : CallProviderProps) {
signalPacket.setSignalType(SignalType.CALL); signalPacket.setSignalType(SignalType.CALL);
send(signalPacket); send(signalPacket);
roleRef.current = CallRole.CALLER; roleRef.current = CallRole.CALLER;
playSound("calling.mp3", true);
} }
const close = () => { const close = () => {
@@ -384,14 +415,28 @@ export function CallProvider(props : CallProviderProps) {
} }
const end = () => { 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?.close();
peerConnectionRef.current = null; peerConnectionRef.current = null;
roomIdRef.current = ""; roomIdRef.current = "";
mutedRef.current = false;
soundRef.current = true;
setActiveCall(""); setActiveCall("");
setCallState(CallState.ENDED); setCallState(CallState.ENDED);
setShowCallView(false); setShowCallView(false);
setSessionKeys(null); setSessionKeys(null);
setDuration(0); setDuration(0);
setMutedState(false);
setSoundState(true);
roleRef.current = null; roleRef.current = null;
} }
@@ -402,6 +447,9 @@ export function CallProvider(props : CallProviderProps) {
*/ */
return; return;
} }
setWindowPriority(false);
stopLoopSound();
stopSound();
/** /**
* Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи * Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи
*/ */
@@ -428,6 +476,46 @@ export function CallProvider(props : CallProviderProps) {
return sharedSecret; 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 = { const context = {
call, call,
close, close,

View File

@@ -1,8 +1,23 @@
import { contextBridge, ipcRenderer, shell } from 'electron' import { contextBridge, ipcRenderer, shell } from 'electron'
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import api from './api' 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 () => { const exposeContext = async () => {
if (process.contextIsolated) { if (process.contextIsolated) {
try { try {
@@ -16,6 +31,11 @@ const exposeContext = async () => {
ipcRenderer.invoke('ipcCore:showItemInFolder', fullPath); ipcRenderer.invoke('ipcCore:showItemInFolder', fullPath);
} }
}); });
contextBridge.exposeInMainWorld("mediaApi", {
getSoundUrl: (fileName: string) => {
return resolveSound(fileName);
}
});
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
@@ -23,6 +43,11 @@ const exposeContext = async () => {
window.electron = electronAPI window.electron = electronAPI
window.api = api; window.api = api;
window.shell = shell; window.shell = shell;
window.mediaApi = {
getSoundUrl: (fileName: string) => {
return resolveSound(fileName);
}
}
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.