Событийные звуки звонка (сбросить, мутинг, и прочее...)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
resources/sounds/calling.mp3
Normal file
BIN
resources/sounds/calling.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/connected.mp3
Normal file
BIN
resources/sounds/connected.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/end_call.mp3
Normal file
BIN
resources/sounds/end_call.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/micro_disable.mp3
Normal file
BIN
resources/sounds/micro_disable.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/micro_enable.mp3
Normal file
BIN
resources/sounds/micro_enable.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/ringtone.mp3
Normal file
BIN
resources/sounds/ringtone.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/sound_disable.mp3
Normal file
BIN
resources/sounds/sound_disable.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/sound_enable.mp3
Normal file
BIN
resources/sounds/sound_enable.mp3
Normal file
Binary file not shown.
Reference in New Issue
Block a user