// WebRTC Система видеозвонков (ЯДРО - ЗАЩИЩЕНО)
class VideoCall {
constructor() {
this.peerConnection = null;
this.localStream = null;
this.remoteStream = null;
this.isCallActive = false;
this.isCaller = false;
this.targetUserId = null;
this.configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
this.initializeUI();
this.setupEventListeners();
}
initializeUI() {
// Создание модального окна видеозвонка
const modal = document.createElement('div');
modal.id = 'video-call-modal';
modal.className = 'video-call-modal';
modal.innerHTML = `
<div class="video-call-container">
<div class="video-streams">
<video id="remote-video" autoplay playsinline></video>
<video id="local-video" autoplay playsinline muted></video>
<div class="call-info">
<div class="remote-user-badge">
<img id="call-remote-avatar" src="" style="display:none;">
<div class="remote-user-details">
<h3 id="call-remote-name" style="display:none;"></h3>
<span id="call-status">${LANG.calling || 'Calling'}...</span>
</div>
</div>
<div class="call-timer-badge">
<span id="call-timer">00:00</span>
</div>
</div>
<div style="position: absolute; bottom: 15px; left: 0; width: 100%; text-align: center; color: rgba(255,255,255,0.25); font-size: 0.65rem; pointer-events: none; z-index: 1; text-transform: uppercase; letter-spacing: 2px; font-weight: 600;">
<div style="position: absolute; bottom: 15px; left: 0; width: 100%; text-align: center; color: rgba(255,255,255,0.25); font-size: 0.65rem; pointer-events: none; z-index: 1; text-transform: uppercase; letter-spacing: 2px; font-weight: 600;">
${LANG.video_license || 'SECURE CONNECTION • END-TO-END ENCRYPTED'}
<div style="margin-top:2px; font-size:0.5rem; opacity:0.6;">POWERED BY ANUS_TANGA</div>
</div>
</div>
<div class="call-controls">
<button id="toggle-camera" class="call-btn camera-on" title="${LANG.camera_on || 'Camera On'}">
📹
</button>
<button id="toggle-mic" class="call-btn mic-on" title="${LANG.mic_on || 'Microphone On'}">
🎤
</button>
<button id="end-call-btn" class="call-btn end-call" title="${LANG.end_call || 'End Call'}">
📞
</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Создание уведомления о входящем звонке
const incomingCall = document.createElement('div');
incomingCall.id = 'incoming-call-notification';
incomingCall.className = 'incoming-call-notification';
incomingCall.innerHTML = `
<div class="incoming-call-content">
<div class="caller-info">
<img id="caller-avatar" src="" alt="Avatar">
<div>
<h3 id="caller-name"></h3>
<p>${LANG.incoming_call || 'Incoming Video Call'}</p>
</div>
</div>
<div class="incoming-call-actions">
<button id="accept-call-btn" class="accept-btn">${LANG.accept_call || 'Accept'}</button>
<button id="decline-call-btn" class="decline-btn">${LANG.decline_call || 'Decline'}</button>
</div>
</div>
`;
document.body.appendChild(incomingCall);
}
setupEventListeners() {
document.getElementById('end-call-btn')?.addEventListener('click', () => this.endCall());
document.getElementById('toggle-camera')?.addEventListener('click', () => this.toggleCamera());
document.getElementById('toggle-mic')?.addEventListener('click', () => this.toggleMic());
document.getElementById('accept-call-btn')?.addEventListener('click', () => this.acceptCall());
document.getElementById('decline-call-btn')?.addEventListener('click', () => this.declineCall());
}
async startCall(userId, userName, userAvatar) {
try {
this.isCaller = true;
this.targetUserId = userId;
this.targetUserName = userName;
this.targetUserAvatar = userAvatar;
// Уведомить чат (пропустить для бота)
if (userId !== 'echo-test') {
const msg = (typeof LANG !== 'undefined' && LANG.video_call_started) ? LANG.video_call_started : 'Video call started';
this.sendChatNotification('📹 ' + msg + (userName ? ' (' + userName + ')' : ''), 0);
}
// Обновить UI информацией об удаленном пользователе
const nameEl = document.getElementById('call-remote-name');
const avatarEl = document.getElementById('call-remote-avatar');
const statusEl = document.getElementById('call-status');
if (nameEl) {
nameEl.textContent = userName || 'User';
nameEl.style.display = 'block';
}
if (avatarEl) {
avatarEl.src = userAvatar || 'assets/default_avatar.png';
avatarEl.style.display = 'block';
}
if (statusEl) {
statusEl.innerText = (LANG.calling || 'Calling') + '...';
}
// Get local media
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// Show video modal
this.showVideoModal();
// Display local video
const localVideo = document.getElementById('local-video');
localVideo.srcObject = this.localStream;
// СПЕЦИАЛЬНАЯ ОБРАБОТКА ДЛЯ ЭХО-ТЕСТА
if (userId === 'echo-test') {
const remoteVideo = document.getElementById('remote-video');
remoteVideo.srcObject = this.localStream; // Петля (Loopback)
remoteVideo.muted = true; // Избегание аудио обратной связи
// Показать статус подключения
const statusEl = document.getElementById('call-status');
if (statusEl) {
const connectedText = (typeof LANG !== 'undefined' && LANG.connected) ? LANG.connected : 'Connected';
statusEl.innerText = `${connectedText} (Video Test Bot 📹)`;
}
this.isCallActive = true;
this.startCallTimer();
return;
}
// Create peer connection
this.createPeerConnection();
// Добавить локальный поток в пир-соединение
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});
// Создать и отправить предложение (offer)
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
// Отправить предложение через сигнальный сервер (вам нужно реализовать это)
this.sendSignal({
type: 'call-offer',
to: userId,
offer: offer,
from: USER.id,
fromName: USER.username,
fromAvatar: USER.avatar || 'assets/default_avatar.png'
});
this.isCallActive = true;
this.startCallTimer();
} catch (error) {
console.error('Error starting call:', error);
alert(LANG.call_failed || 'Call failed. Please try again.');
this.endCall();
}
}
async acceptCall() {
try {
// Get local media
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// Скрыть уведомление о входящем звонке
document.getElementById('incoming-call-notification').style.display = 'none';
// Show video modal
this.showVideoModal();
// Display local video
const localVideo = document.getElementById('local-video');
localVideo.srcObject = this.localStream;
// Create peer connection
this.createPeerConnection();
// Добавить локальный поток
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});
// Установить удаленное описание (из сохраненного предложения)
if (this.pendingOffer) {
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(this.pendingOffer));
// Создать ответ
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);
// Отправить ответ
this.sendSignal({
type: 'call-answer',
to: this.targetUserId,
answer: answer
});
}
this.isCallActive = true;
this.startCallTimer();
} catch (error) {
console.error('Error accepting call:', error);
alert(LANG.call_failed || 'Call failed. Please try again.');
this.endCall();
}
}
declineCall() {
// Отправить сигнал отклонения
this.sendSignal({
type: 'call-declined',
to: this.targetUserId
});
// Скрыть уведомление
document.getElementById('incoming-call-notification').style.display = 'none';
this.targetUserId = null;
this.pendingOffer = null;
}
createPeerConnection() {
this.peerConnection = new RTCPeerConnection(this.configuration);
// Обработка ICE кандидатов
this.peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.sendSignal({
type: 'ice-candidate',
to: this.targetUserId,
candidate: event.candidate
});
}
};
// Обработка удаленного потока
this.peerConnection.ontrack = (event) => {
const remoteVideo = document.getElementById('remote-video');
if (!this.remoteStream) {
this.remoteStream = new MediaStream();
remoteVideo.srcObject = this.remoteStream;
}
this.remoteStream.addTrack(event.track);
};
// Обработка состояния подключения
this.peerConnection.onconnectionstatechange = () => {
const state = this.peerConnection.connectionState;
console.log('Connection state:', state);
if (state === 'connected') {
document.getElementById('call-status').textContent = '';
} else if (state === 'disconnected' || state === 'failed' || state === 'closed') {
this.endCall();
}
};
}
async handleSignal(data) {
try {
switch (data.type) {
case 'call-offer':
// Показать уведомление о входящем звонке
this.targetUserId = data.from;
this.pendingOffer = data.offer;
document.getElementById('caller-name').textContent = data.fromName;
document.getElementById('caller-avatar').src = data.fromAvatar;
document.getElementById('incoming-call-notification').style.display = 'flex';
// Воспроизвести рингтон (опционально)
// this.playRingtone();
break;
case 'call-answer':
if (this.peerConnection) {
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
}
break;
case 'ice-candidate':
if (this.peerConnection && data.candidate) {
await this.peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
}
break;
case 'call-declined':
alert(LANG.call_ended || 'Call declined');
this.endCall();
break;
case 'call-ended':
this.endCall();
break;
}
} catch (error) {
console.error('Error handling signal:', error);
}
}
sendSignal(data) {
// Это должно быть реализовано с вашим сигнальным сервером
// Пока мы будем использовать простой запрос к PHP эндпоинту
fetch('api/video-signal.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).catch(err => console.error('Signal error:', err));
}
toggleCamera() {
if (this.localStream) {
const videoTrack = this.localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
const btn = document.getElementById('toggle-camera');
if (videoTrack.enabled) {
btn.classList.add('camera-on');
btn.classList.remove('camera-off');
btn.title = LANG.camera_on || 'Camera On';
} else {
btn.classList.add('camera-off');
btn.classList.remove('camera-on');
btn.title = LANG.camera_off || 'Camera Off';
}
}
}
}
toggleMic() {
if (this.localStream) {
const audioTracks = this.localStream.getAudioTracks();
if (audioTracks.length > 0) {
const enabled = !audioTracks[0].enabled;
audioTracks[0].enabled = enabled;
const btn = document.getElementById('toggle-mic'); // Изменено с toggle-mic-btn на toggle-mic, чтобы соответствовать существующему HTML
if (btn) { // Добавлена проверка существования btn
btn.innerHTML = enabled ? '🎤' : 'mic_off'; // Использовать иконочный шрифт или svg
btn.style.background = enabled ? 'rgba(255,255,255,0.2)' : '#ff4757';
// Подсказка
const tMicOn = (typeof LANG !== 'undefined') ? LANG.mic_on : 'Microphone On';
const tMicOff = (typeof LANG !== 'undefined') ? LANG.mic_off : 'Microphone Off';
btn.title = enabled ? tMicOn : tMicOff;
}
}
}
}
sendChatNotification(message, roomId = null) {
if (!message) return;
const targetRoom = (roomId !== null) ? roomId : (typeof currentRoomId !== 'undefined' ? currentRoomId : 1);
const formData = new FormData();
formData.append('message', message);
formData.append('type', 'system');
formData.append('room_id', targetRoom);
fetch('api/send.php', { method: 'POST', body: formData }).catch(console.error);
}
endCall() {
// Уведомить чат, если звонок был активен (пропустить для бота)
if (this.isCallActive && this.targetUserId && this.targetUserId !== 'echo-test') {
const msg = (typeof LANG !== 'undefined' && LANG.video_call_ended) ? LANG.video_call_ended : 'Video call ended';
let durationText = '';
const timerEl = document.getElementById('call-timer');
if (timerEl) durationText = ' (' + timerEl.innerText + ')';
this.sendChatNotification('📴 ' + msg + durationText, 0);
}
// Отправить сигнал завершения звонка
if (this.targetUserId) {
this.sendSignal({
type: 'call-ended',
to: this.targetUserId
});
}
// Остановить все треки
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
this.localStream = null;
}
// Закрыть пир-соединение
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection = null;
}
// Скрыть видео модальное окно
document.getElementById('video-call-modal').style.display = 'none';
// Сбросить состояние
this.isCallActive = false;
this.targetUserId = null;
this.remoteStream = null;
this.isCaller = false;
// Остановить таймер
if (this.callTimerInterval) {
clearInterval(this.callTimerInterval);
this.callTimerInterval = null;
}
}
showVideoModal() {
document.getElementById('video-call-modal').style.display = 'flex';
}
startCallTimer() {
let seconds = 0;
this.callTimerInterval = setInterval(() => {
seconds++;
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
document.getElementById('call-timer').textContent =
`${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}, 1000);
}
}
// Инициализация системы видеозвонков
let videoCall;
if (typeof USER !== 'undefined') {
videoCall = new VideoCall();
}