View file protected/video-call-core.js

File size: 19.35Kb
// 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();
}