Merge pull request 'uaaa_monkey' (#2) from front2 into main
Reviewed-on: https://maktraher.ru/moxitech/Drone-Rtc-Application/pulls/2
This commit is contained in:
commit
3c3bbe3eeb
31
Makefile
Normal file
31
Makefile
Normal 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
|
@ -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"
|
BIN
public/monkey-monkey-with-drone.gif
Normal file
BIN
public/monkey-monkey-with-drone.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 MiB |
4
src/components/CallContainer.css
Normal file
4
src/components/CallContainer.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#full-screen-modal {
|
||||||
|
--height: 100%;
|
||||||
|
--width: 100%;
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
||||||
|
console.log(`Peer успешно открыт с ID: ${id}`);
|
||||||
newPeer.on('open', (id) => {
|
setConnectionInfo(id);
|
||||||
console.log(`Peer успешно открыт с ID: ${id}`);
|
|
||||||
setConnectionInfo(`Ваш Peer ID: ${id}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
newPeer.on('call', (call) => {
|
|
||||||
console.log('Получен входящий звонок');
|
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
|
|
||||||
setLocalStream(stream);
|
|
||||||
call.answer(stream);
|
|
||||||
call.on('stream', (remoteStream) => {
|
|
||||||
console.log('Получен удалённый поток');
|
|
||||||
playAudio(remoteStream);
|
|
||||||
});
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error('Не удалось получить локальный поток для ответа', err);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
newPeer.on('error', (err) => {
|
newPeer.on('call', (call: MediaConnection) => handleIncomingCall(call));
|
||||||
console.error('Ошибка Peer:', err);
|
newPeer.on('error', (err) => console.error('Ошибка Peer:', err));
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (peer) {
|
newPeer.destroy();
|
||||||
console.log('Уничтожение Peer');
|
};
|
||||||
peer.destroy();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cleanup = initializePeer();
|
||||||
|
return cleanup;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const makeCall = () => {
|
const handleIncomingCall = useCallback((call: MediaConnection) => {
|
||||||
|
console.log('Получен входящий звонок');
|
||||||
|
setCurrentCallId(call.peer);
|
||||||
|
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
|
||||||
|
setLocalStream(stream);
|
||||||
|
call.answer(stream);
|
||||||
|
call.on('stream', (remoteStream: MediaStream) => {
|
||||||
|
console.log('Получен удалённый поток');
|
||||||
|
playAudio(remoteStream);
|
||||||
|
startCallTimer();
|
||||||
|
setIsCallActive(true);
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('Не удалось получить локальный поток для ответа', err);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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'>
|
||||||
<InputContainer
|
<div className="input-container">
|
||||||
callString={callString}
|
<InputContainer
|
||||||
setCallString={setCallString}
|
callString={callString}
|
||||||
isEnable={isEnable}
|
setCallString={setCallString}
|
||||||
/>
|
isEnable={isEnable}
|
||||||
<IonButton className="call-button" onClick={makeCall} disabled={!isEnable || !callString || isLoading} slot="end">
|
/>
|
||||||
{isLoading ? 'Звонок...' : 'Вызов'}
|
<IonButton className="call-button" onClick={makeCall} disabled={!isEnable || !callString || isLoading}>
|
||||||
</IonButton>
|
<IonIcon icon={callIcon} />
|
||||||
|
</IonButton>
|
||||||
|
</div>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonFooter>
|
</IonFooter>
|
||||||
</>
|
</>
|
||||||
@ -120,4 +169,3 @@ const ExploreContainer: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ExploreContainer;
|
export default ExploreContainer;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user