|
|
import os |
|
|
import uuid |
|
|
import json |
|
|
from datetime import datetime |
|
|
from flask import Flask, request, jsonify, send_file |
|
|
from flask_socketio import SocketIO, emit |
|
|
import base64 |
|
|
import io |
|
|
|
|
|
app = Flask(__name__) |
|
|
app.config['SECRET_KEY'] = 'messenger-secret-key' |
|
|
app.config['DEBUG'] = False |
|
|
|
|
|
|
|
|
socketio = SocketIO( |
|
|
app, |
|
|
cors_allowed_origins="*", |
|
|
ping_timeout=60, |
|
|
ping_interval=25, |
|
|
logger=True, |
|
|
engineio_logger=False, |
|
|
async_mode='eventlet', |
|
|
max_http_buffer_size=100 * 1024 * 1024 |
|
|
) |
|
|
|
|
|
|
|
|
users = {} |
|
|
messages = [] |
|
|
voice_messages = {} |
|
|
active_calls = {} |
|
|
|
|
|
HTML_TEMPLATE = ''' |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="utf-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
|
|
<title>Мобильный Мессенджер</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
-webkit-tap-highlight-color: transparent; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
background: #f0f2f5; |
|
|
height: 100vh; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.app-container { |
|
|
height: 100vh; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
max-width: 100%; |
|
|
margin: 0 auto; |
|
|
background: white; |
|
|
} |
|
|
|
|
|
/* Login Screen */ |
|
|
.login-screen { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
height: 100%; |
|
|
padding: 20px; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.login-form { |
|
|
background: rgba(255,255,255,0.1); |
|
|
padding: 30px; |
|
|
border-radius: 20px; |
|
|
backdrop-filter: blur(10px); |
|
|
width: 100%; |
|
|
max-width: 320px; |
|
|
} |
|
|
|
|
|
.login-input { |
|
|
width: 100%; |
|
|
padding: 15px; |
|
|
margin: 10px 0; |
|
|
border: none; |
|
|
border-radius: 25px; |
|
|
font-size: 16px; |
|
|
background: rgba(255,255,255,0.9); |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.login-btn { |
|
|
width: 100%; |
|
|
padding: 15px; |
|
|
margin: 10px 0; |
|
|
border: none; |
|
|
border-radius: 25px; |
|
|
background: #4CAF50; |
|
|
color: white; |
|
|
font-size: 16px; |
|
|
font-weight: bold; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
/* Chat Screen */ |
|
|
.chat-screen { |
|
|
display: none; |
|
|
flex-direction: column; |
|
|
height: 100%; |
|
|
} |
|
|
|
|
|
.header { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
padding: 15px; |
|
|
position: relative; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.header-info { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.header h2 { |
|
|
margin-bottom: 5px; |
|
|
font-size: 18px; |
|
|
} |
|
|
|
|
|
.online-info { |
|
|
font-size: 14px; |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
.call-buttons { |
|
|
display: flex; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.header-btn { |
|
|
background: rgba(255,255,255,0.2); |
|
|
border: none; |
|
|
border-radius: 50%; |
|
|
width: 44px; |
|
|
height: 44px; |
|
|
color: white; |
|
|
font-size: 18px; |
|
|
cursor: pointer; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.messages-container { |
|
|
flex: 1; |
|
|
overflow-y: auto; |
|
|
padding: 10px; |
|
|
background: #f8f9fa; |
|
|
-webkit-overflow-scrolling: touch; |
|
|
} |
|
|
|
|
|
.message { |
|
|
margin: 10px 0; |
|
|
padding: 12px 15px; |
|
|
border-radius: 18px; |
|
|
max-width: 85%; |
|
|
word-wrap: break-word; |
|
|
position: relative; |
|
|
animation: fadeIn 0.3s ease-in; |
|
|
} |
|
|
|
|
|
@keyframes fadeIn { |
|
|
from { opacity: 0; transform: translateY(10px); } |
|
|
to { opacity: 1; transform: translateY(0); } |
|
|
} |
|
|
|
|
|
.message.own { |
|
|
background: #007AFF; |
|
|
color: white; |
|
|
margin-left: auto; |
|
|
border-bottom-right-radius: 5px; |
|
|
} |
|
|
|
|
|
.message.other { |
|
|
background: white; |
|
|
color: #333; |
|
|
margin-right: auto; |
|
|
border-bottom-left-radius: 5px; |
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
.message-sender { |
|
|
font-weight: bold; |
|
|
font-size: 12px; |
|
|
margin-bottom: 4px; |
|
|
opacity: 0.8; |
|
|
} |
|
|
|
|
|
.message-time { |
|
|
font-size: 11px; |
|
|
opacity: 0.7; |
|
|
margin-top: 5px; |
|
|
text-align: right; |
|
|
} |
|
|
|
|
|
.voice-message { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
padding: 8px 0; |
|
|
} |
|
|
|
|
|
.voice-play-btn { |
|
|
background: #007AFF; |
|
|
border: none; |
|
|
border-radius: 50%; |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
margin-right: 10px; |
|
|
cursor: pointer; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.voice-duration { |
|
|
font-size: 14px; |
|
|
color: #666; |
|
|
} |
|
|
|
|
|
.input-area { |
|
|
display: flex; |
|
|
padding: 10px; |
|
|
background: white; |
|
|
border-top: 1px solid #e0e0e0; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.message-input { |
|
|
flex: 1; |
|
|
padding: 12px 15px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 25px; |
|
|
margin: 0 5px; |
|
|
font-size: 16px; |
|
|
outline: none; |
|
|
background: #f8f9fa; |
|
|
} |
|
|
|
|
|
.action-btn { |
|
|
background: none; |
|
|
border: none; |
|
|
font-size: 24px; |
|
|
padding: 10px; |
|
|
cursor: pointer; |
|
|
border-radius: 50%; |
|
|
transition: background 0.2s; |
|
|
min-width: 44px; |
|
|
min-height: 44px; |
|
|
color: #007AFF; |
|
|
} |
|
|
|
|
|
.action-btn:active { |
|
|
background: rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
/* Users List */ |
|
|
.users-sidebar { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
right: -300px; |
|
|
width: 300px; |
|
|
height: 100%; |
|
|
background: white; |
|
|
box-shadow: -2px 0 10px rgba(0,0,0,0.1); |
|
|
transition: right 0.3s ease; |
|
|
z-index: 1000; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.users-sidebar.open { |
|
|
right: 0; |
|
|
} |
|
|
|
|
|
.sidebar-header { |
|
|
padding: 20px; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.close-sidebar { |
|
|
background: none; |
|
|
border: none; |
|
|
color: white; |
|
|
font-size: 20px; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.users-list { |
|
|
flex: 1; |
|
|
overflow-y: auto; |
|
|
padding: 10px; |
|
|
} |
|
|
|
|
|
.user-item { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
padding: 15px; |
|
|
border-bottom: 1px solid #eee; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.user-item:active { |
|
|
background: #f5f5f5; |
|
|
} |
|
|
|
|
|
.user-avatar { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 50%; |
|
|
background: #007AFF; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
font-weight: bold; |
|
|
margin-right: 12px; |
|
|
} |
|
|
|
|
|
.user-info { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.user-name { |
|
|
font-weight: 500; |
|
|
margin-bottom: 2px; |
|
|
} |
|
|
|
|
|
.user-status { |
|
|
font-size: 12px; |
|
|
color: #4CAF50; |
|
|
} |
|
|
|
|
|
.call-icon { |
|
|
background: none; |
|
|
border: none; |
|
|
font-size: 20px; |
|
|
cursor: pointer; |
|
|
padding: 8px; |
|
|
color: #007AFF; |
|
|
} |
|
|
|
|
|
/* Call Screen */ |
|
|
.call-screen { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background: #1a1a1a; |
|
|
z-index: 2000; |
|
|
display: none; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.video-container { |
|
|
flex: 1; |
|
|
position: relative; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.remote-video { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: cover; |
|
|
} |
|
|
|
|
|
.local-video { |
|
|
position: absolute; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
width: 120px; |
|
|
height: 160px; |
|
|
border-radius: 10px; |
|
|
border: 2px solid white; |
|
|
object-fit: cover; |
|
|
} |
|
|
|
|
|
.call-info { |
|
|
position: absolute; |
|
|
top: 40px; |
|
|
left: 0; |
|
|
right: 0; |
|
|
text-align: center; |
|
|
color: white; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.call-status { |
|
|
font-size: 18px; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
.call-timer { |
|
|
font-size: 24px; |
|
|
font-weight: bold; |
|
|
} |
|
|
|
|
|
.call-controls { |
|
|
position: absolute; |
|
|
bottom: 40px; |
|
|
left: 0; |
|
|
right: 0; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
gap: 20px; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.call-control-btn { |
|
|
background: rgba(255,255,255,0.2); |
|
|
border: none; |
|
|
border-radius: 50%; |
|
|
width: 70px; |
|
|
height: 70px; |
|
|
color: white; |
|
|
font-size: 24px; |
|
|
cursor: pointer; |
|
|
backdrop-filter: blur(10px); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.end-call-btn { |
|
|
background: #FF4444; |
|
|
} |
|
|
|
|
|
.call-control-btn.active { |
|
|
background: #4CAF50; |
|
|
} |
|
|
|
|
|
/* Recording Overlay */ |
|
|
.recording-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background: rgba(0,0,0,0.8); |
|
|
display: none; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
flex-direction: column; |
|
|
z-index: 1000; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.recording-animation { |
|
|
width: 100px; |
|
|
height: 100px; |
|
|
border: 3px solid white; |
|
|
border-radius: 50%; |
|
|
animation: pulse 1.5s infinite; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0% { transform: scale(0.8); opacity: 1; } |
|
|
50% { transform: scale(1.2); opacity: 0.7; } |
|
|
100% { transform: scale(0.8); opacity: 1; } |
|
|
} |
|
|
|
|
|
/* Overlay */ |
|
|
.overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background: rgba(0,0,0,0.5); |
|
|
display: none; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
z-index: 1500; |
|
|
} |
|
|
|
|
|
.call-alert { |
|
|
background: white; |
|
|
padding: 25px; |
|
|
border-radius: 15px; |
|
|
text-align: center; |
|
|
max-width: 300px; |
|
|
width: 90%; |
|
|
} |
|
|
|
|
|
.call-alert-buttons { |
|
|
display: flex; |
|
|
gap: 10px; |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.alert-btn { |
|
|
flex: 1; |
|
|
padding: 12px; |
|
|
border: none; |
|
|
border-radius: 8px; |
|
|
font-weight: bold; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.accept-btn { |
|
|
background: #4CAF50; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.reject-btn { |
|
|
background: #FF4444; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.no-video { |
|
|
background: #333; |
|
|
color: white; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.connection-status { |
|
|
position: fixed; |
|
|
top: 10px; |
|
|
right: 10px; |
|
|
padding: 5px 10px; |
|
|
border-radius: 15px; |
|
|
font-size: 12px; |
|
|
z-index: 3000; |
|
|
} |
|
|
|
|
|
.connected { |
|
|
background: #4CAF50; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.disconnected { |
|
|
background: #FF4444; |
|
|
color: white; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<!-- Connection Status --> |
|
|
<div id="connectionStatus" class="connection-status disconnected" style="display: none;"> |
|
|
Подключение... |
|
|
</div> |
|
|
|
|
|
<!-- Login Screen --> |
|
|
<div id="loginScreen" class="login-screen"> |
|
|
<div style="text-align: center; margin-bottom: 30px;"> |
|
|
<h1 style="font-size: 2.5em; margin-bottom: 10px;">💬</h1> |
|
|
<h1>Мобильный Мессенджер</h1> |
|
|
<p style="margin-top: 10px; opacity: 0.8;">Общение, звонки, видео</p> |
|
|
</div> |
|
|
<div class="login-form"> |
|
|
<input type="text" id="usernameInput" class="login-input" placeholder="Введите ваше имя" maxlength="20"> |
|
|
<button onclick="registerUser()" class="login-btn">Начать общение</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Main Chat Screen --> |
|
|
<div id="chatScreen" class="app-container chat-screen"> |
|
|
<div class="header"> |
|
|
<div class="header-info"> |
|
|
<h2>💬 Мессенджер</h2> |
|
|
<div class="online-info"> |
|
|
<span id="onlineCount">0</span> пользователей онлайн |
|
|
</div> |
|
|
</div> |
|
|
<div class="call-buttons"> |
|
|
<button class="header-btn" onclick="toggleUsersSidebar()" title="Пользователи">👥</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="messagesContainer" class="messages-container"> |
|
|
<div style="text-align: center; color: #666; padding: 20px;"> |
|
|
Начните общение! |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="input-area"> |
|
|
<button class="action-btn" onclick="startVoiceRecording()" title="Голосовое сообщение">🎤</button> |
|
|
<input type="text" id="messageInput" class="message-input" placeholder="Введите сообщение..." maxlength="500"> |
|
|
<button class="action-btn" onclick="sendMessage()" title="Отправить">📤</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Users Sidebar --> |
|
|
<div id="usersSidebar" class="users-sidebar"> |
|
|
<div class="sidebar-header"> |
|
|
<h3>Пользователи онлайн</h3> |
|
|
<button class="close-sidebar" onclick="toggleUsersSidebar()">✕</button> |
|
|
</div> |
|
|
<div id="usersList" class="users-list"> |
|
|
<!-- Users will be populated here --> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Call Screen --> |
|
|
<div id="callScreen" class="call-screen"> |
|
|
<div class="video-container"> |
|
|
<div id="remoteVideoContainer" class="no-video remote-video"> |
|
|
<div>Ожидание подключения...</div> |
|
|
</div> |
|
|
<video id="localVideo" class="local-video" autoplay playsinline muted></video> |
|
|
<div class="call-info"> |
|
|
<div class="call-status" id="callStatus">Звонок...</div> |
|
|
<div class="call-timer" id="callTimer">00:00</div> |
|
|
</div> |
|
|
<div class="call-controls"> |
|
|
<button class="call-control-btn" id="muteBtn" onclick="toggleMute()">🎤</button> |
|
|
<button class="call-control-btn" id="videoBtn" onclick="toggleVideo()">📹</button> |
|
|
<button class="call-control-btn end-call-btn" onclick="endCall()">📞</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Recording Overlay --> |
|
|
<div id="recordingOverlay" class="recording-overlay"> |
|
|
<div class="recording-animation"></div> |
|
|
<h3>Запись голосового сообщения...</h3> |
|
|
<p>Нажмите для остановки</p> |
|
|
</div> |
|
|
|
|
|
<!-- Incoming Call Overlay --> |
|
|
<div id="incomingCallOverlay" class="overlay"> |
|
|
<div class="call-alert"> |
|
|
<h3>Входящий звонок</h3> |
|
|
<p id="callerName">...</p> |
|
|
<div class="call-alert-buttons"> |
|
|
<button class="alert-btn reject-btn" onclick="rejectCall()">Отклонить</button> |
|
|
<button class="alert-btn accept-btn" onclick="acceptCall()">Принять</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script> |
|
|
<script> |
|
|
let socket; |
|
|
let currentUser = null; |
|
|
let mediaRecorder = null; |
|
|
let audioChunks = []; |
|
|
let isRecording = false; |
|
|
let localStream = null; |
|
|
let remoteStream = null; |
|
|
let peerConnection = null; |
|
|
let currentCall = null; |
|
|
let callStartTime = null; |
|
|
let callTimerInterval = null; |
|
|
let isMuted = false; |
|
|
let isVideoEnabled = true; |
|
|
let reconnectAttempts = 0; |
|
|
const MAX_RECONNECT_ATTEMPTS = 5; |
|
|
|
|
|
// WebRTC configuration |
|
|
const rtcConfig = { |
|
|
iceServers: [ |
|
|
{ urls: 'stun:stun.l.google.com:19302' }, |
|
|
{ |
|
|
urls: 'turn:turn.anyfirewall.com:443?transport=tcp', |
|
|
username: 'webrtc', |
|
|
credential: 'webrtc' |
|
|
} |
|
|
] |
|
|
}; |
|
|
|
|
|
function initializeSocket() { |
|
|
// Получаем базовый URL для корректного подключения к WebSocket |
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
|
|
const host = window.location.host; |
|
|
|
|
|
socket = io({ |
|
|
transports: ['websocket', 'polling'], |
|
|
upgrade: true, |
|
|
rememberUpgrade: true, |
|
|
timeout: 10000, |
|
|
pingTimeout: 60000, |
|
|
pingInterval: 25000, |
|
|
reconnection: true, |
|
|
reconnectionAttempts: MAX_RECONNECT_ATTEMPTS, |
|
|
reconnectionDelay: 1000, |
|
|
reconnectionDelayMax: 5000 |
|
|
}); |
|
|
|
|
|
// Socket events |
|
|
socket.on('connect', () => { |
|
|
console.log('Connected to server'); |
|
|
updateConnectionStatus(true); |
|
|
reconnectAttempts = 0; |
|
|
|
|
|
// Re-register if we have a current user |
|
|
if (currentUser) { |
|
|
socket.emit('register', currentUser.username); |
|
|
} |
|
|
}); |
|
|
|
|
|
socket.on('disconnect', (reason) => { |
|
|
console.log('Disconnected from server:', reason); |
|
|
updateConnectionStatus(false); |
|
|
|
|
|
if (reason === 'io server disconnect') { |
|
|
// Server intentionally disconnected, try to reconnect |
|
|
setTimeout(() => { |
|
|
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { |
|
|
socket.connect(); |
|
|
reconnectAttempts++; |
|
|
} |
|
|
}, 1000); |
|
|
} |
|
|
}); |
|
|
|
|
|
socket.on('connect_error', (error) => { |
|
|
console.error('Connection error:', error); |
|
|
updateConnectionStatus(false); |
|
|
}); |
|
|
|
|
|
socket.on('registration_success', (data) => { |
|
|
currentUser = data; |
|
|
document.getElementById('loginScreen').style.display = 'none'; |
|
|
document.getElementById('chatScreen').style.display = 'flex'; |
|
|
updateConnectionStatus(true); |
|
|
}); |
|
|
|
|
|
socket.on('user_list_update', (data) => { |
|
|
document.getElementById('onlineCount').textContent = data.count; |
|
|
updateUsersList(data.users || []); |
|
|
}); |
|
|
|
|
|
socket.on('new_message', (message) => { |
|
|
displayMessage(message); |
|
|
}); |
|
|
|
|
|
socket.on('new_voice_message', (data) => { |
|
|
displayVoiceMessage(data); |
|
|
}); |
|
|
|
|
|
socket.on('incoming_call', (data) => { |
|
|
showIncomingCall(data.from_user, data.call_id, data.isVideo); |
|
|
}); |
|
|
|
|
|
socket.on('call_answered', (data) => { |
|
|
if (data.answer) { |
|
|
startCall(data.isVideo, false); |
|
|
} else { |
|
|
alert('Пользователь отклонил звонок'); |
|
|
hideCallScreen(); |
|
|
} |
|
|
}); |
|
|
|
|
|
socket.on('call_ended', () => { |
|
|
alert('Звонок завершен'); |
|
|
endCall(); |
|
|
}); |
|
|
|
|
|
socket.on('webrtc_offer', async (data) => { |
|
|
console.log('Received WebRTC offer'); |
|
|
if (!peerConnection) { |
|
|
await createPeerConnection(false); |
|
|
} |
|
|
try { |
|
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer)); |
|
|
const answer = await peerConnection.createAnswer(); |
|
|
await peerConnection.setLocalDescription(answer); |
|
|
|
|
|
socket.emit('webrtc_answer', { |
|
|
answer: answer, |
|
|
to_user: data.from_user, |
|
|
call_id: data.call_id |
|
|
}); |
|
|
} catch (error) { |
|
|
console.error('Error handling WebRTC offer:', error); |
|
|
} |
|
|
}); |
|
|
|
|
|
socket.on('webrtc_answer', async (data) => { |
|
|
console.log('Received WebRTC answer'); |
|
|
if (peerConnection) { |
|
|
try { |
|
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer)); |
|
|
} catch (error) { |
|
|
console.error('Error setting remote description:', error); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
socket.on('webrtc_ice_candidate', async (data) => { |
|
|
if (peerConnection && data.candidate) { |
|
|
try { |
|
|
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); |
|
|
} catch (error) { |
|
|
console.error('Error adding ICE candidate:', error); |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
function updateConnectionStatus(connected) { |
|
|
const statusElement = document.getElementById('connectionStatus'); |
|
|
statusElement.style.display = 'block'; |
|
|
if (connected) { |
|
|
statusElement.textContent = '✓ Подключено'; |
|
|
statusElement.className = 'connection-status connected'; |
|
|
} else { |
|
|
statusElement.textContent = '✗ Отключено'; |
|
|
statusElement.className = 'connection-status disconnected'; |
|
|
} |
|
|
} |
|
|
|
|
|
// UI Functions |
|
|
function registerUser() { |
|
|
const username = document.getElementById('usernameInput').value.trim(); |
|
|
if (username) { |
|
|
if (!socket || !socket.connected) { |
|
|
initializeSocket(); |
|
|
} |
|
|
socket.emit('register', username); |
|
|
} else { |
|
|
alert('Введите имя пользователя'); |
|
|
} |
|
|
} |
|
|
|
|
|
function sendMessage() { |
|
|
const input = document.getElementById('messageInput'); |
|
|
const text = input.value.trim(); |
|
|
|
|
|
if (text && socket && socket.connected) { |
|
|
socket.emit('message', { |
|
|
sender: currentUser.username, |
|
|
text: text |
|
|
}); |
|
|
input.value = ''; |
|
|
} else if (!socket || !socket.connected) { |
|
|
alert('Нет подключения к серверу'); |
|
|
} |
|
|
} |
|
|
|
|
|
function displayMessage(message) { |
|
|
const container = document.getElementById('messagesContainer'); |
|
|
const messageDiv = document.createElement('div'); |
|
|
|
|
|
messageDiv.className = `message ${message.sender === currentUser.username ? 'own' : 'other'}`; |
|
|
messageDiv.innerHTML = ` |
|
|
<div class="message-sender">${message.sender}</div> |
|
|
<div>${message.text}</div> |
|
|
<div class="message-time">${new Date(message.timestamp).toLocaleTimeString()}</div> |
|
|
`; |
|
|
|
|
|
container.appendChild(messageDiv); |
|
|
container.scrollTop = container.scrollHeight; |
|
|
|
|
|
// Remove placeholder if exists |
|
|
const placeholder = container.querySelector('div[style*="text-align: center"]'); |
|
|
if (placeholder) { |
|
|
placeholder.remove(); |
|
|
} |
|
|
} |
|
|
|
|
|
function displayVoiceMessage(data) { |
|
|
const container = document.getElementById('messagesContainer'); |
|
|
const messageDiv = document.createElement('div'); |
|
|
|
|
|
messageDiv.className = `message ${data.sender === currentUser.username ? 'own' : 'other'}`; |
|
|
messageDiv.innerHTML = ` |
|
|
<div class="message-sender">${data.sender}</div> |
|
|
<div class="voice-message"> |
|
|
<button class="voice-play-btn" onclick="playVoiceMessage('${data.filename}')">▶</button> |
|
|
<div> |
|
|
<div>Голосовое сообщение</div> |
|
|
<div class="voice-duration">${data.duration || '0'} сек</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</div> |
|
|
`; |
|
|
|
|
|
container.appendChild(messageDiv); |
|
|
container.scrollTop = container.scrollHeight; |
|
|
} |
|
|
|
|
|
function updateUsersList(users) { |
|
|
const usersList = document.getElementById('usersList'); |
|
|
usersList.innerHTML = ''; |
|
|
|
|
|
const otherUsers = users.filter(user => user !== currentUser.username); |
|
|
|
|
|
if (otherUsers.length === 0) { |
|
|
usersList.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;">Нет других пользователей онлайн</div>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
otherUsers.forEach(user => { |
|
|
const userItem = document.createElement('div'); |
|
|
userItem.className = 'user-item'; |
|
|
userItem.innerHTML = ` |
|
|
<div class="user-avatar">${user.charAt(0).toUpperCase()}</div> |
|
|
<div class="user-info"> |
|
|
<div class="user-name">${user}</div> |
|
|
<div class="user-status">online</div> |
|
|
</div> |
|
|
<button class="call-icon" onclick="startCallWithUser('${user}', false)" title="Аудиозвонок">📞</button> |
|
|
<button class="call-icon" onclick="startCallWithUser('${user}', true)" title="Видеозвонок">📹</button> |
|
|
`; |
|
|
usersList.appendChild(userItem); |
|
|
}); |
|
|
} |
|
|
|
|
|
function toggleUsersSidebar() { |
|
|
const sidebar = document.getElementById('usersSidebar'); |
|
|
sidebar.classList.toggle('open'); |
|
|
} |
|
|
|
|
|
// Voice Messages |
|
|
async function startVoiceRecording() { |
|
|
if (isRecording) { |
|
|
stopRecording(); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
|
audio: { |
|
|
echoCancellation: true, |
|
|
noiseSuppression: true |
|
|
} |
|
|
}); |
|
|
|
|
|
mediaRecorder = new MediaRecorder(stream, { |
|
|
mimeType: 'audio/webm;codecs=opus' |
|
|
}); |
|
|
|
|
|
audioChunks = []; |
|
|
let startTime = Date.now(); |
|
|
|
|
|
mediaRecorder.ondataavailable = (event) => { |
|
|
if (event.data.size > 0) { |
|
|
audioChunks.push(event.data); |
|
|
} |
|
|
}; |
|
|
|
|
|
mediaRecorder.onstop = async () => { |
|
|
const duration = Math.round((Date.now() - startTime) / 1000); |
|
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); |
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = function() { |
|
|
const base64Audio = reader.result.split(',')[1]; |
|
|
|
|
|
if (socket && socket.connected) { |
|
|
socket.emit('voice_message', { |
|
|
audio_data: base64Audio, |
|
|
sender: currentUser.username, |
|
|
duration: duration |
|
|
}); |
|
|
} else { |
|
|
alert('Нет подключения для отправки голосового сообщения'); |
|
|
} |
|
|
}; |
|
|
reader.readAsDataURL(audioBlob); |
|
|
|
|
|
stream.getTracks().forEach(track => track.stop()); |
|
|
}; |
|
|
|
|
|
mediaRecorder.start(); |
|
|
isRecording = true; |
|
|
|
|
|
// Show recording overlay |
|
|
document.getElementById('recordingOverlay').style.display = 'flex'; |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Error recording:', error); |
|
|
alert('Ошибка доступа к микрофону'); |
|
|
} |
|
|
} |
|
|
|
|
|
function stopRecording() { |
|
|
if (mediaRecorder && isRecording) { |
|
|
mediaRecorder.stop(); |
|
|
isRecording = false; |
|
|
document.getElementById('recordingOverlay').style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
function playVoiceMessage(filename) { |
|
|
const audio = new Audio(`/voice/${filename}`); |
|
|
audio.play().catch(e => console.error('Error playing audio:', e)); |
|
|
} |
|
|
|
|
|
// Call Functions |
|
|
function startCallWithUser(targetUser, isVideo) { |
|
|
if (!socket || !socket.connected) { |
|
|
alert('Нет подключения к серверу'); |
|
|
return; |
|
|
} |
|
|
|
|
|
currentCall = { |
|
|
id: Date.now().toString(), |
|
|
target: targetUser, |
|
|
isVideo: isVideo, |
|
|
isInitiator: true |
|
|
}; |
|
|
|
|
|
socket.emit('call_request', { |
|
|
call_id: currentCall.id, |
|
|
from_user: currentUser.username, |
|
|
to_user: targetUser, |
|
|
isVideo: isVideo |
|
|
}); |
|
|
|
|
|
showCallScreen(isVideo, `Звонок ${targetUser}...`); |
|
|
} |
|
|
|
|
|
function showIncomingCall(fromUser, callId, isVideo) { |
|
|
currentCall = { |
|
|
id: callId, |
|
|
target: fromUser, |
|
|
isVideo: isVideo, |
|
|
isInitiator: false |
|
|
}; |
|
|
|
|
|
document.getElementById('callerName').textContent = fromUser; |
|
|
document.getElementById('incomingCallOverlay').style.display = 'flex'; |
|
|
} |
|
|
|
|
|
function acceptCall() { |
|
|
document.getElementById('incomingCallOverlay').style.display = 'none'; |
|
|
if (socket && socket.connected) { |
|
|
socket.emit('call_answer', { |
|
|
call_id: currentCall.id, |
|
|
answer: true, |
|
|
to_user: currentCall.target, |
|
|
isVideo: currentCall.isVideo |
|
|
}); |
|
|
|
|
|
startCall(currentCall.isVideo, true); |
|
|
} else { |
|
|
alert('Нет подключения к серверу'); |
|
|
} |
|
|
} |
|
|
|
|
|
function rejectCall() { |
|
|
if (socket && socket.connected) { |
|
|
socket.emit('call_answer', { |
|
|
call_id: currentCall.id, |
|
|
answer: false, |
|
|
to_user: currentCall.target |
|
|
}); |
|
|
} |
|
|
|
|
|
document.getElementById('incomingCallOverlay').style.display = 'none'; |
|
|
currentCall = null; |
|
|
} |
|
|
|
|
|
async function startCall(isVideo, isInitiator) { |
|
|
try { |
|
|
showCallScreen(isVideo, isInitiator ? 'Звонок...' : 'Разговор...'); |
|
|
|
|
|
// Get local media stream |
|
|
const constraints = { |
|
|
audio: true, |
|
|
video: isVideo ? { |
|
|
width: { ideal: 640 }, |
|
|
height: { ideal: 480 }, |
|
|
frameRate: { ideal: 24 } |
|
|
} : false |
|
|
}; |
|
|
|
|
|
localStream = await navigator.mediaDevices.getUserMedia(constraints); |
|
|
document.getElementById('localVideo').srcObject = localStream; |
|
|
|
|
|
// Create peer connection |
|
|
await createPeerConnection(isInitiator); |
|
|
|
|
|
if (isInitiator) { |
|
|
// Create and send offer |
|
|
const offer = await peerConnection.createOffer(); |
|
|
await peerConnection.setLocalDescription(offer); |
|
|
|
|
|
if (socket && socket.connected) { |
|
|
socket.emit('webrtc_offer', { |
|
|
offer: offer, |
|
|
to_user: currentCall.target, |
|
|
call_id: currentCall.id |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
// Start call timer |
|
|
startCallTimer(); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Error starting call:', error); |
|
|
alert(`Ошибка начала звонка: ${error.message}`); |
|
|
endCall(); |
|
|
} |
|
|
} |
|
|
|
|
|
async function createPeerConnection(isInitiator) { |
|
|
peerConnection = new RTCPeerConnection(rtcConfig); |
|
|
|
|
|
// Add local stream tracks |
|
|
localStream.getTracks().forEach(track => { |
|
|
peerConnection.addTrack(track, localStream); |
|
|
}); |
|
|
|
|
|
// Handle incoming stream |
|
|
peerConnection.ontrack = (event) => { |
|
|
console.log('Received remote track:', event.track.kind); |
|
|
remoteStream = event.streams[0]; |
|
|
const remoteVideo = document.getElementById('remoteVideoContainer'); |
|
|
|
|
|
if (event.track.kind === 'video') { |
|
|
// Create video element for remote stream |
|
|
const videoElement = document.createElement('video'); |
|
|
videoElement.srcObject = remoteStream; |
|
|
videoElement.autoplay = true; |
|
|
videoElement.playsInline = true; |
|
|
videoElement.className = 'remote-video'; |
|
|
|
|
|
remoteVideo.innerHTML = ''; |
|
|
remoteVideo.appendChild(videoElement); |
|
|
} else if (event.track.kind === 'audio') { |
|
|
// For audio only, show message |
|
|
if (!document.querySelector('.remote-video video')) { |
|
|
remoteVideo.innerHTML = '<div>Аудиозвонок</div>'; |
|
|
remoteVideo.className = 'no-video remote-video'; |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
// Handle ICE candidates |
|
|
peerConnection.onicecandidate = (event) => { |
|
|
if (event.candidate && currentCall && socket && socket.connected) { |
|
|
socket.emit('webrtc_ice_candidate', { |
|
|
candidate: event.candidate, |
|
|
to_user: currentCall.target |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
peerConnection.onconnectionstatechange = () => { |
|
|
console.log('Connection state:', peerConnection.connectionState); |
|
|
if (peerConnection.connectionState === 'connected') { |
|
|
document.getElementById('callStatus').textContent = 'Разговор'; |
|
|
} |
|
|
}; |
|
|
|
|
|
peerConnection.oniceconnectionstatechange = () => { |
|
|
console.log('ICE connection state:', peerConnection.iceConnectionState); |
|
|
}; |
|
|
} |
|
|
|
|
|
function showCallScreen(isVideo, status) { |
|
|
document.getElementById('callScreen').style.display = 'flex'; |
|
|
document.getElementById('callStatus').textContent = status; |
|
|
|
|
|
const remoteVideo = document.getElementById('remoteVideoContainer'); |
|
|
if (!isVideo) { |
|
|
remoteVideo.innerHTML = '<div>Аудиозвонок</div>'; |
|
|
remoteVideo.className = 'no-video remote-video'; |
|
|
} |
|
|
} |
|
|
|
|
|
function hideCallScreen() { |
|
|
document.getElementById('callScreen').style.display = 'none'; |
|
|
document.getElementById('remoteVideoContainer').innerHTML = '<div>Ожидание подключения...</div>'; |
|
|
document.getElementById('remoteVideoContainer').className = 'no-video remote-video'; |
|
|
|
|
|
if (callTimerInterval) { |
|
|
clearInterval(callTimerInterval); |
|
|
callTimerInterval = null; |
|
|
} |
|
|
} |
|
|
|
|
|
function startCallTimer() { |
|
|
callStartTime = new Date(); |
|
|
callTimerInterval = setInterval(() => { |
|
|
const now = new Date(); |
|
|
const diff = Math.floor((now - callStartTime) / 1000); |
|
|
const minutes = Math.floor(diff / 60); |
|
|
const seconds = diff % 60; |
|
|
document.getElementById('callTimer').textContent = |
|
|
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
function endCall() { |
|
|
if (peerConnection) { |
|
|
peerConnection.close(); |
|
|
peerConnection = null; |
|
|
} |
|
|
|
|
|
if (localStream) { |
|
|
localStream.getTracks().forEach(track => track.stop()); |
|
|
localStream = null; |
|
|
} |
|
|
|
|
|
if (currentCall && socket && socket.connected) { |
|
|
socket.emit('call_end', { |
|
|
to_user: currentCall.target, |
|
|
call_id: currentCall.id |
|
|
}); |
|
|
} |
|
|
|
|
|
hideCallScreen(); |
|
|
currentCall = null; |
|
|
isMuted = false; |
|
|
isVideoEnabled = true; |
|
|
} |
|
|
|
|
|
function toggleMute() { |
|
|
if (localStream) { |
|
|
const audioTracks = localStream.getAudioTracks(); |
|
|
if (audioTracks.length > 0) { |
|
|
isMuted = !isMuted; |
|
|
audioTracks[0].enabled = !isMuted; |
|
|
document.getElementById('muteBtn').classList.toggle('active', isMuted); |
|
|
document.getElementById('muteBtn').textContent = isMuted ? '🎤🚫' : '🎤'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleVideo() { |
|
|
if (localStream) { |
|
|
const videoTracks = localStream.getVideoTracks(); |
|
|
if (videoTracks.length > 0) { |
|
|
isVideoEnabled = !isVideoEnabled; |
|
|
videoTracks[0].enabled = isVideoEnabled; |
|
|
document.getElementById('videoBtn').classList.toggle('active', !isVideoEnabled); |
|
|
document.getElementById('videoBtn').textContent = isVideoEnabled ? '📹' : '📹🚫'; |
|
|
|
|
|
// Hide/show local video |
|
|
document.getElementById('localVideo').style.display = isVideoEnabled ? 'block' : 'none'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
// Event listeners |
|
|
document.getElementById('recordingOverlay').addEventListener('click', stopRecording); |
|
|
|
|
|
document.getElementById('messageInput').addEventListener('keypress', (e) => { |
|
|
if (e.key === 'Enter') { |
|
|
sendMessage(); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.getElementById('usernameInput').addEventListener('keypress', (e) => { |
|
|
if (e.key === 'Enter') { |
|
|
registerUser(); |
|
|
} |
|
|
}); |
|
|
|
|
|
// Close sidebar when clicking outside |
|
|
document.addEventListener('click', (e) => { |
|
|
const sidebar = document.getElementById('usersSidebar'); |
|
|
if (sidebar.classList.contains('open') && |
|
|
!e.target.closest('.users-sidebar') && |
|
|
!e.target.closest('.header-btn')) { |
|
|
sidebar.classList.remove('open'); |
|
|
} |
|
|
}); |
|
|
|
|
|
// Initialize socket on load |
|
|
window.addEventListener('load', function() { |
|
|
initializeSocket(); |
|
|
}); |
|
|
|
|
|
// Handle page visibility changes |
|
|
document.addEventListener('visibilitychange', function() { |
|
|
if (!document.hidden && (!socket || !socket.connected)) { |
|
|
initializeSocket(); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
return HTML_TEMPLATE |
|
|
|
|
|
@socketio.on('connect') |
|
|
def handle_connect(): |
|
|
print(f'Client connected: {request.sid}') |
|
|
emit('user_list_update', { |
|
|
'count': len(users), |
|
|
'users': [user_data['username'] for user_data in users.values()] |
|
|
}) |
|
|
|
|
|
@socketio.on('disconnect') |
|
|
def handle_disconnect(): |
|
|
print(f'Client disconnected: {request.sid}') |
|
|
|
|
|
|
|
|
disconnected_user = None |
|
|
for user_id, user_data in list(users.items()): |
|
|
if user_data.get('sid') == request.sid: |
|
|
disconnected_user = user_data['username'] |
|
|
|
|
|
for call_id, call_data in list(active_calls.items()): |
|
|
if call_data['from_user'] == disconnected_user or call_data['to_user'] == disconnected_user: |
|
|
try: |
|
|
emit('call_ended', room=call_data['to_sid']) |
|
|
except: |
|
|
pass |
|
|
try: |
|
|
emit('call_ended', room=call_data['from_sid']) |
|
|
except: |
|
|
pass |
|
|
del active_calls[call_id] |
|
|
|
|
|
del users[user_id] |
|
|
break |
|
|
|
|
|
if disconnected_user: |
|
|
emit('user_list_update', { |
|
|
'count': len(users), |
|
|
'users': [user_data['username'] for user_data in users.values()] |
|
|
}, broadcast=True) |
|
|
|
|
|
@socketio.on('register') |
|
|
def handle_register(username): |
|
|
user_id = str(uuid.uuid4()) |
|
|
users[user_id] = { |
|
|
'username': username, |
|
|
'sid': request.sid, |
|
|
'joined': datetime.now().isoformat() |
|
|
} |
|
|
|
|
|
emit('registration_success', { |
|
|
'user_id': user_id, |
|
|
'username': username |
|
|
}) |
|
|
|
|
|
emit('user_list_update', { |
|
|
'count': len(users), |
|
|
'users': [user_data['username'] for user_data in users.values()] |
|
|
}, broadcast=True) |
|
|
|
|
|
@socketio.on('message') |
|
|
def handle_message(data): |
|
|
message = { |
|
|
'id': str(uuid.uuid4()), |
|
|
'sender': data['sender'], |
|
|
'text': data['text'], |
|
|
'timestamp': datetime.now().isoformat() |
|
|
} |
|
|
messages.append(message) |
|
|
emit('new_message', message, broadcast=True) |
|
|
|
|
|
@socketio.on('voice_message') |
|
|
def handle_voice_message(data): |
|
|
try: |
|
|
audio_data = base64.b64decode(data['audio_data']) |
|
|
filename = f"voice_{uuid.uuid4()}.webm" |
|
|
|
|
|
with open(filename, 'wb') as f: |
|
|
f.write(audio_data) |
|
|
|
|
|
voice_data = { |
|
|
'filename': filename, |
|
|
'sender': data['sender'], |
|
|
'duration': data.get('duration', 0), |
|
|
'timestamp': datetime.now().isoformat() |
|
|
} |
|
|
voice_messages[filename] = voice_data |
|
|
|
|
|
emit('new_voice_message', voice_data, broadcast=True) |
|
|
except Exception as e: |
|
|
print(f"Error handling voice message: {e}") |
|
|
|
|
|
@socketio.on('call_request') |
|
|
def handle_call_request(data): |
|
|
target_username = data.get('to_user') |
|
|
target_sid = None |
|
|
|
|
|
for user_data in users.values(): |
|
|
if user_data['username'] == target_username: |
|
|
target_sid = user_data['sid'] |
|
|
break |
|
|
|
|
|
if target_sid: |
|
|
active_calls[data['call_id']] = { |
|
|
'from_user': data['from_user'], |
|
|
'to_user': target_username, |
|
|
'from_sid': request.sid, |
|
|
'to_sid': target_sid, |
|
|
'is_video': data.get('isVideo', False) |
|
|
} |
|
|
|
|
|
emit('incoming_call', { |
|
|
'call_id': data['call_id'], |
|
|
'from_user': data['from_user'], |
|
|
'isVideo': data.get('isVideo', False) |
|
|
}, room=target_sid) |
|
|
|
|
|
@socketio.on('call_answer') |
|
|
def handle_call_answer(data): |
|
|
call_id = data.get('call_id') |
|
|
if call_id in active_calls: |
|
|
call_data = active_calls[call_id] |
|
|
|
|
|
if data['answer']: |
|
|
emit('call_answered', { |
|
|
'answer': True, |
|
|
'isVideo': call_data['is_video'] |
|
|
}, room=call_data['from_sid']) |
|
|
else: |
|
|
emit('call_answered', { |
|
|
'answer': False |
|
|
}, room=call_data['from_sid']) |
|
|
del active_calls[call_id] |
|
|
|
|
|
@socketio.on('call_end') |
|
|
def handle_call_end(data): |
|
|
call_id = data.get('call_id') |
|
|
if call_id in active_calls: |
|
|
call_data = active_calls[call_id] |
|
|
try: |
|
|
emit('call_ended', room=call_data['to_sid']) |
|
|
except: |
|
|
pass |
|
|
try: |
|
|
emit('call_ended', room=call_data['from_sid']) |
|
|
except: |
|
|
pass |
|
|
del active_calls[call_id] |
|
|
|
|
|
@socketio.on('webrtc_offer') |
|
|
def handle_webrtc_offer(data): |
|
|
target_username = data.get('to_user') |
|
|
target_sid = None |
|
|
|
|
|
for user_data in users.values(): |
|
|
if user_data['username'] == target_username: |
|
|
target_sid = user_data['sid'] |
|
|
break |
|
|
|
|
|
if target_sid: |
|
|
emit('webrtc_offer', { |
|
|
'offer': data['offer'], |
|
|
'from_user': data.get('from_user'), |
|
|
'call_id': data.get('call_id') |
|
|
}, room=target_sid) |
|
|
|
|
|
@socketio.on('webrtc_answer') |
|
|
def handle_webrtc_answer(data): |
|
|
target_username = data.get('to_user') |
|
|
target_sid = None |
|
|
|
|
|
for user_data in users.values(): |
|
|
if user_data['username'] == target_username: |
|
|
target_sid = user_data['sid'] |
|
|
break |
|
|
|
|
|
if target_sid: |
|
|
emit('webrtc_answer', { |
|
|
'answer': data['answer'], |
|
|
'from_user': data.get('from_user') |
|
|
}, room=target_sid) |
|
|
|
|
|
@socketio.on('webrtc_ice_candidate') |
|
|
def handle_webrtc_ice_candidate(data): |
|
|
target_username = data.get('to_user') |
|
|
target_sid = None |
|
|
|
|
|
for user_data in users.values(): |
|
|
if user_data['username'] == target_username: |
|
|
target_sid = user_data['sid'] |
|
|
break |
|
|
|
|
|
if target_sid: |
|
|
emit('webrtc_ice_candidate', { |
|
|
'candidate': data['candidate'], |
|
|
'from_user': data.get('from_user') |
|
|
}, room=target_sid) |
|
|
|
|
|
@app.route('/voice/<filename>') |
|
|
def get_voice_message(filename): |
|
|
if filename in voice_messages: |
|
|
return send_file(filename, mimetype='audio/webm') |
|
|
return 'File not found', 404 |
|
|
|
|
|
def cleanup_temp_files(): |
|
|
"""Очистка временных файлов""" |
|
|
import glob |
|
|
for filename in glob.glob("voice_*.webm"): |
|
|
try: |
|
|
os.remove(filename) |
|
|
except: |
|
|
pass |
|
|
|
|
|
if __name__ == '__main__': |
|
|
try: |
|
|
print("Запуск мобильного мессенджера...") |
|
|
print("Порт: 7860") |
|
|
socketio.run( |
|
|
app, |
|
|
host='0.0.0.0', |
|
|
port=7860, |
|
|
debug=False, |
|
|
allow_unsafe_werkzeug=True |
|
|
) |
|
|
finally: |
|
|
cleanup_temp_files() |