Compare commits

...

2 Commits

7 changed files with 305 additions and 103 deletions

31
Makefile Normal file
View File

@ -0,0 +1,31 @@
#DEV
build:
docker compose build
dev: build
docker compose up -d --force-recreate
devf: dev
docker compose logs -f
up:
docker compose up -d --force-recreate
upf: up
docker compose logs -f
logs:
docker compose logs -f
stop:
"!!!!!!!!!!!!!!!!!EXTERMINATUS!!!!!!!!!!!!!!!!!"
docker compose stop
start:
docker compose start
drop:
docker-compose down --volumes

View File

@ -20,11 +20,11 @@ services:
- "8100:8100" - "8100:8100"
volumes: volumes:
- .:/app - .:/app
deploy: # deploy:
resources: # resources:
limits: # limits:
cpus: "0.5" # cpus: "0.5"
memory: "512M" # memory: "512M"
app2: app2:
build: . build: .
@ -36,8 +36,8 @@ services:
- "8101:8100" - "8101:8100"
volumes: volumes:
- .:/app - .:/app
deploy: # deploy:
resources: # resources:
limits: # limits:
cpus: "0.5" # cpus: "0.5"
memory: "512M" # memory: "512M"

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 MiB

View File

@ -0,0 +1,4 @@
#full-screen-modal {
--height: 100%;
--width: 100%;
}

View File

@ -1,16 +1,70 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
// import './ExploreContainer.css'; import {
IonButton,
IonModal,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
} from '@ionic/react';
import './CallContainer.css';
import Peer from 'peerjs';
interface InputContainerPerr {
peer: Peer;
isEnable: boolean;
}
// setCalling => bool : true - звонок включен, false - звонок выключен
//
const CallContainer: React.FC = () => { const CallContainer: React.FC = () => {
// Растягивался на весь экран const [isModalOpen, setIsModalOpen] = useState(false);
// По середине: кнопки: завершить звонок, вывод сколько времени идет звонок (обнулять при завершении) const [callTime, setCallTime] = useState(0);
// Тебе поможет ionic modal const [callInterval, setCallInterval] = useState<NodeJS.Timeout | null>(null);
const handleCallStart = () => {
setIsModalOpen(true);
setCallTime(0);
const interval = setInterval(() => {
setCallTime((prevTime) => prevTime + 1);
}, 1000);
setCallInterval(interval);
};
const handleCallEnd = () => {
setIsModalOpen(false);
if (callInterval) {
clearInterval(callInterval);
}
setCallInterval(null);
};
useEffect(() => {
// Очистка интервала при размонтировании компонента
return () => {
if (callInterval) {
clearInterval(callInterval);
}
};
}, [callInterval]);
return ( return (
<div id="container"> <div id="container">
<IonButton onClick={handleCallStart}>Звонок...</IonButton>
<IonModal isOpen={isModalOpen} onDidDismiss={handleCallEnd} id="full-screen-modal">
<IonHeader>
<IonToolbar>
<IonTitle>Звонок идет...</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
<div style={{ textAlign: 'center', marginTop: '20%' }}>
<h2>Время звонка: {callTime} секунд</h2>
<IonButton color="danger" onClick={handleCallEnd}>
Завершить звонок
</IonButton>
</div>
</IonContent>
</IonModal>
</div> </div>
); );
}; };

View File

@ -1,23 +1,88 @@
#container {
}
#container strong {
font-size: 20px;
line-height: 26px;
}
#container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
#container a {
text-decoration: none;
}
/* ExploreContainer.css */ /* ExploreContainer.css */
.call-button { #container {
margin-left: auto; display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.peer-id-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.peer-id-text {
margin-right: 10px;
font-size: 16px;
color: #fff;
}
.copy-button {
background: none;
border: none;
color: #007aff;
cursor: pointer;
font-size: 16px;
}
.input-container {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
width: 100%;
}
.call-info-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.call-info-text {
font-size: 18px;
margin-bottom: 10px;
color: #ffffff;
}
.call-duration-text {
font-size: 18px;
margin-bottom: 20px;
color: #ffffff;
}
.hang-up-button {
background: none;
border: none;
color: #ff3b30; /* Красная трубка */
font-size: 24px;
}
.call-button {
background: none;
border: none;
color: #007aff;
cursor: pointer;
font-size: 24px;
margin-left: 10px;
}
.call-button ion-icon {
font-size: 30px;
color: #007aff;
}
.panel-optimizer {
display: flex;
align-items: center;
justify-content: center;
}
.drone-gif {
width: 100px; /* Задайте нужный размер */
height: auto;
margin-bottom: 20px;
} }

View File

@ -1,8 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import './ExploreContainer.css'; import './ExploreContainer.css';
import InputContainer from './Input'; import InputContainer from './Input';
import { IonButton, IonContent, IonFooter, IonToolbar, IonPage, IonHeader, IonTitle, IonToast, IonText } from '@ionic/react'; import {
import Peer from 'peerjs'; IonButton, IonContent, IonFooter, IonToolbar, IonText, IonModal, IonIcon, IonHeader, IonTitle
} from '@ionic/react';
import { call as callIcon, close as hangUpIcon } from 'ionicons/icons';
import Peer, { MediaConnection } from 'peerjs';
const ExploreContainer: React.FC = () => { const ExploreContainer: React.FC = () => {
const [callString, setCallString] = useState(""); const [callString, setCallString] = useState("");
@ -11,58 +14,64 @@ const ExploreContainer: React.FC = () => {
const [localStream, setLocalStream] = useState<MediaStream | null>(null); const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [connectionInfo, setConnectionInfo] = useState<string | null>(null); const [connectionInfo, setConnectionInfo] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isCallActive, setIsCallActive] = useState(false);
const [callDuration, setCallDuration] = useState<number>(0);
const [callInterval, setCallInterval] = useState<NodeJS.Timeout | null>(null);
const [currentCallId, setCurrentCallId] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
// Инициализация Peer без сервера, только для локальной сети const initializePeer = () => {
const newPeer = new Peer({ const newPeer = new Peer({ debug: 3 });
debug: 3,
});
setPeer(newPeer); setPeer(newPeer);
newPeer.on('open', (id) => { newPeer.on('open', (id) => {
console.log(`Peer успешно открыт с ID: ${id}`); console.log(`Peer успешно открыт с ID: ${id}`);
setConnectionInfo(`Ваш Peer ID: ${id}`); setConnectionInfo(id);
}); });
newPeer.on('call', (call) => { newPeer.on('call', (call: MediaConnection) => handleIncomingCall(call));
newPeer.on('error', (err) => console.error('Ошибка Peer:', err));
return () => {
newPeer.destroy();
};
};
const cleanup = initializePeer();
return cleanup;
}, []);
const handleIncomingCall = useCallback((call: MediaConnection) => {
console.log('Получен входящий звонок'); console.log('Получен входящий звонок');
setCurrentCallId(call.peer);
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
setLocalStream(stream); setLocalStream(stream);
call.answer(stream); call.answer(stream);
call.on('stream', (remoteStream) => { call.on('stream', (remoteStream: MediaStream) => {
console.log('Получен удалённый поток'); console.log('Получен удалённый поток');
playAudio(remoteStream); playAudio(remoteStream);
startCallTimer();
setIsCallActive(true);
}); });
}).catch((err) => { }).catch((err) => {
console.error('Не удалось получить локальный поток для ответа', err); console.error('Не удалось получить локальный поток для ответа', err);
}); });
});
newPeer.on('error', (err) => {
console.error('Ошибка Peer:', err);
});
return () => {
if (peer) {
console.log('Уничтожение Peer');
peer.destroy();
}
};
}, []); }, []);
const makeCall = () => { const makeCall = useCallback(() => {
if (peer && callString) { if (peer && callString) {
setIsLoading(true); setIsLoading(true);
console.log(`Попытка звонка на: ${callString}`); console.log(`Попытка звонка на: ${callString}`);
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
setLocalStream(stream); setLocalStream(stream);
const call = peer.call(callString, stream); const call = peer.call(callString, stream);
call.on('stream', (remoteStream) => { setCurrentCallId(callString);
call.on('stream', (remoteStream: MediaStream) => {
console.log('Получен удалённый поток во время вызова'); console.log('Получен удалённый поток во время вызова');
playAudio(remoteStream); playAudio(remoteStream);
setIsLoading(false); setIsLoading(false);
startCallTimer();
setIsCallActive(true);
}); });
}).catch((err) => { }).catch((err) => {
console.error('Не удалось получить локальный поток', err); console.error('Не удалось получить локальный поток', err);
@ -71,9 +80,29 @@ const ExploreContainer: React.FC = () => {
} else { } else {
console.warn('Звонок невозможен: отсутствует Peer или callString'); console.warn('Звонок невозможен: отсутствует Peer или callString');
} }
}; }, [peer, callString]);
const playAudio = (stream: MediaStream) => { const endCall = useCallback(() => {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
setIsCallActive(false);
setIsEnable(true);
if (callInterval) {
clearInterval(callInterval);
}
setCallDuration(0);
setCurrentCallId(null);
}, [localStream, callInterval]);
const startCallTimer = useCallback(() => {
const interval = setInterval(() => {
setCallDuration(prevDuration => prevDuration + 1);
}, 1000);
setCallInterval(interval);
}, []);
const playAudio = useCallback((stream: MediaStream) => {
const audioElement = document.createElement('audio'); const audioElement = document.createElement('audio');
audioElement.srcObject = stream; audioElement.srcObject = stream;
audioElement.play().then(() => { audioElement.play().then(() => {
@ -81,38 +110,58 @@ const ExploreContainer: React.FC = () => {
}).catch((error) => { }).catch((error) => {
console.error('Ошибка воспроизведения аудио', error); console.error('Ошибка воспроизведения аудио', error);
}); });
}; }, []);
const copyPeerId = useCallback(() => {
if (connectionInfo) {
navigator.clipboard.writeText(connectionInfo)
.then(() => {
console.log('Peer ID скопирован в буфер обмена');
})
.catch((err) => {
console.error('Не удалось скопировать Peer ID', err);
});
}
}, [connectionInfo]);
return ( return (
<> <>
<IonContent fullscreen={true} className="ion-padding"> <IonContent fullscreen={true} className="ion-padding">
<IonHeader> <div id="container">
<IonToolbar> <div className="peer-id-container">
<IonTitle> <IonText className="peer-id-text">Ваш Peer ID: {connectionInfo}</IonText>
DEBUG MODE <button className="copy-button" onClick={copyPeerId}>Копировать</button>
</IonTitle>
</IonToolbar>
</IonHeader>
<IonText>{connectionInfo}</IonText>
{isLoading && (
<div className="loading-animation">
<span>.</span>
<span>.</span>
<span>.</span>
</div> </div>
)} </div>
{/* Модальное окно для звонка */}
<IonModal isOpen={isCallActive}>
<IonContent className="ion-padding">
<div className="call-info-container">
{/* Добавляем гифку */}
<img src="/monkey-monkey-with-drone.gif" alt="Monkey with Drone" className="drone-gif" />
<IonText className="call-info-text">Peer ID: {currentCallId}</IonText>
<IonText className="call-duration-text"> {Math.floor(callDuration / 60)}:{callDuration % 60}</IonText>
<IonButton color="danger" onClick={endCall} className="hang-up-button">
<IonIcon icon={hangUpIcon} />
</IonButton>
</div>
</IonContent>
</IonModal>
</IonContent> </IonContent>
<IonFooter> <IonFooter>
<IonToolbar className='panel-optimizer'> <IonToolbar className='panel-optimizer'>
<div className="input-container">
<InputContainer <InputContainer
callString={callString} callString={callString}
setCallString={setCallString} setCallString={setCallString}
isEnable={isEnable} isEnable={isEnable}
/> />
<IonButton className="call-button" onClick={makeCall} disabled={!isEnable || !callString || isLoading} slot="end"> <IonButton className="call-button" onClick={makeCall} disabled={!isEnable || !callString || isLoading}>
{isLoading ? 'Звонок...' : 'Вызов'} <IonIcon icon={callIcon} />
</IonButton> </IonButton>
</div>
</IonToolbar> </IonToolbar>
</IonFooter> </IonFooter>
</> </>
@ -120,4 +169,3 @@ const ExploreContainer: React.FC = () => {
}; };
export default ExploreContainer; export default ExploreContainer;