338 lines
13 KiB
Python
338 lines
13 KiB
Python
"""
|
||
WebSocket consumers для видеоконференций.
|
||
Обработка WebRTC signaling через Django Channels.
|
||
"""
|
||
import json
|
||
import logging
|
||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||
from channels.db import database_sync_to_async
|
||
from django.utils import timezone
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class VideoRoomConsumer(AsyncWebsocketConsumer):
|
||
"""
|
||
WebSocket consumer для видеокомнаты.
|
||
Обрабатывает WebRTC signaling между участниками.
|
||
"""
|
||
|
||
async def connect(self):
|
||
"""Подключение к комнате."""
|
||
self.room_id = self.scope['url_route']['kwargs']['room_id']
|
||
self.room_group_name = f'video_room_{self.room_id}'
|
||
self.user = self.scope['user']
|
||
|
||
# Проверка аутентификации
|
||
if not self.user.is_authenticated:
|
||
await self.close()
|
||
return
|
||
|
||
# Проверка доступа к комнате
|
||
has_access = await self.check_room_access()
|
||
if not has_access:
|
||
await self.close()
|
||
return
|
||
|
||
# Присоединяемся к группе комнаты
|
||
await self.channel_layer.group_add(
|
||
self.room_group_name,
|
||
self.channel_name
|
||
)
|
||
|
||
await self.accept()
|
||
|
||
# Отправляем подтверждение подключения
|
||
await self.send(text_data=json.dumps({
|
||
'type': 'connection_established',
|
||
'message': 'Подключение установлено',
|
||
'user_id': self.user.id
|
||
}))
|
||
|
||
# Уведомляем других участников
|
||
await self.channel_layer.group_send(
|
||
self.room_group_name,
|
||
{
|
||
'type': 'user_joined',
|
||
'user_id': self.user.id,
|
||
'username': self.user.get_full_name()
|
||
}
|
||
)
|
||
|
||
# Сохраняем информацию об участнике
|
||
await self.save_participant()
|
||
|
||
logger.info(f'User {self.user.id} connected to room {self.room_id}')
|
||
|
||
async def disconnect(self, close_code):
|
||
"""Отключение от комнаты."""
|
||
if hasattr(self, 'room_group_name'):
|
||
# Уведомляем других участников об отключении
|
||
await self.channel_layer.group_send(
|
||
self.room_group_name,
|
||
{
|
||
'type': 'user_left',
|
||
'user_id': self.user.id,
|
||
'username': self.user.get_full_name()
|
||
}
|
||
)
|
||
|
||
# Покидаем группу
|
||
await self.channel_layer.group_discard(
|
||
self.room_group_name,
|
||
self.channel_name
|
||
)
|
||
|
||
# Обновляем статус участника
|
||
await self.update_participant_disconnect()
|
||
|
||
logger.info(f'User {self.user.id} disconnected from room {self.room_id}')
|
||
|
||
async def receive(self, text_data):
|
||
"""Получение сообщения от клиента."""
|
||
try:
|
||
data = json.loads(text_data)
|
||
message_type = data.get('type')
|
||
|
||
# Обработка разных типов сообщений
|
||
if message_type == 'offer':
|
||
await self.handle_offer(data)
|
||
|
||
elif message_type == 'answer':
|
||
await self.handle_answer(data)
|
||
|
||
elif message_type == 'ice-candidate':
|
||
await self.handle_ice_candidate(data)
|
||
|
||
elif message_type == 'media-state':
|
||
await self.handle_media_state(data)
|
||
|
||
elif message_type == 'start-screen-share':
|
||
await self.handle_screen_share(data, True)
|
||
|
||
elif message_type == 'stop-screen-share':
|
||
await self.handle_screen_share(data, False)
|
||
|
||
else:
|
||
logger.warning(f'Unknown message type: {message_type}')
|
||
|
||
except json.JSONDecodeError:
|
||
logger.error('Invalid JSON received')
|
||
except Exception as e:
|
||
logger.error(f'Error in receive: {str(e)}')
|
||
|
||
async def handle_offer(self, data):
|
||
"""Обработка WebRTC offer."""
|
||
await self.channel_layer.group_send(
|
||
self.room_group_name,
|
||
{
|
||
'type': 'webrtc_offer',
|
||
'offer': data.get('offer'),
|
||
'sender_id': self.user.id
|
||
}
|
||
)
|
||
|
||
async def handle_answer(self, data):
|
||
"""Обработка WebRTC answer."""
|
||
await self.channel_layer.group_send(
|
||
self.room_group_name,
|
||
{
|
||
'type': 'webrtc_answer',
|
||
'answer': data.get('answer'),
|
||
'sender_id': self.user.id
|
||
}
|
||
)
|
||
|
||
async def handle_ice_candidate(self, data):
|
||
"""Обработка ICE candidate."""
|
||
await self.channel_layer.group_send(
|
||
self.room_group_name,
|
||
{
|
||
'type': 'ice_candidate',
|
||
'candidate': data.get('candidate'),
|
||
'sender_id': self.user.id
|
||
}
|
||
)
|
||
|
||
async def handle_media_state(self, data):
|
||
"""Обработка изменения состояния медиа (вкл/выкл камера/микрофон)."""
|
||
audio_enabled = data.get('audio_enabled')
|
||
video_enabled = data.get('video_enabled')
|
||
|
||
# Сохраняем состояние
|
||
await self.update_participant_media_state(audio_enabled, video_enabled)
|
||
|
||
# Уведомляем других участников
|
||
await self.channel_layer.group_send(
|
||
self.room_group_name,
|
||
{
|
||
'type': 'media_state_changed',
|
||
'user_id': self.user.id,
|
||
'audio_enabled': audio_enabled,
|
||
'video_enabled': video_enabled
|
||
}
|
||
)
|
||
|
||
async def handle_screen_share(self, data, is_sharing):
|
||
"""Обработка демонстрации экрана."""
|
||
await self.update_participant_screen_sharing(is_sharing)
|
||
|
||
await self.channel_layer.group_send(
|
||
self.room_group_name,
|
||
{
|
||
'type': 'screen_share_changed',
|
||
'user_id': self.user.id,
|
||
'is_sharing': is_sharing
|
||
}
|
||
)
|
||
|
||
# Обработчики событий от группы
|
||
|
||
async def user_joined(self, event):
|
||
"""Пользователь присоединился."""
|
||
if event['user_id'] != self.user.id:
|
||
await self.send(text_data=json.dumps({
|
||
'type': 'user_joined',
|
||
'user_id': event['user_id'],
|
||
'username': event['username']
|
||
}))
|
||
|
||
async def user_left(self, event):
|
||
"""Пользователь покинул комнату."""
|
||
if event['user_id'] != self.user.id:
|
||
await self.send(text_data=json.dumps({
|
||
'type': 'user_left',
|
||
'user_id': event['user_id'],
|
||
'username': event['username']
|
||
}))
|
||
|
||
async def webrtc_offer(self, event):
|
||
"""Пересылка WebRTC offer."""
|
||
if event['sender_id'] != self.user.id:
|
||
await self.send(text_data=json.dumps({
|
||
'type': 'offer',
|
||
'offer': event['offer'],
|
||
'sender_id': event['sender_id']
|
||
}))
|
||
|
||
async def webrtc_answer(self, event):
|
||
"""Пересылка WebRTC answer."""
|
||
if event['sender_id'] != self.user.id:
|
||
await self.send(text_data=json.dumps({
|
||
'type': 'answer',
|
||
'answer': event['answer'],
|
||
'sender_id': event['sender_id']
|
||
}))
|
||
|
||
async def ice_candidate(self, event):
|
||
"""Пересылка ICE candidate."""
|
||
if event['sender_id'] != self.user.id:
|
||
await self.send(text_data=json.dumps({
|
||
'type': 'ice-candidate',
|
||
'candidate': event['candidate'],
|
||
'sender_id': event['sender_id']
|
||
}))
|
||
|
||
async def media_state_changed(self, event):
|
||
"""Изменение состояния медиа."""
|
||
if event['user_id'] != self.user.id:
|
||
await self.send(text_data=json.dumps({
|
||
'type': 'media-state-changed',
|
||
'user_id': event['user_id'],
|
||
'audio_enabled': event['audio_enabled'],
|
||
'video_enabled': event['video_enabled']
|
||
}))
|
||
|
||
async def screen_share_changed(self, event):
|
||
"""Изменение демонстрации экрана."""
|
||
if event['user_id'] != self.user.id:
|
||
await self.send(text_data=json.dumps({
|
||
'type': 'screen-share-changed',
|
||
'user_id': event['user_id'],
|
||
'is_sharing': event['is_sharing']
|
||
}))
|
||
|
||
# Работа с базой данных
|
||
|
||
@database_sync_to_async
|
||
def check_room_access(self):
|
||
"""Проверка доступа к комнате."""
|
||
from .models import VideoRoom
|
||
try:
|
||
room = VideoRoom.objects.get(room_id=self.room_id)
|
||
# Проверяем что пользователь - участник
|
||
return self.user in [room.mentor, room.client]
|
||
except VideoRoom.DoesNotExist:
|
||
return False
|
||
|
||
@database_sync_to_async
|
||
def save_participant(self):
|
||
"""Сохранение информации об участнике."""
|
||
from .models import VideoRoom, VideoParticipant
|
||
try:
|
||
room = VideoRoom.objects.get(room_id=self.room_id)
|
||
participant, created = VideoParticipant.objects.get_or_create(
|
||
room=room,
|
||
user=self.user,
|
||
defaults={
|
||
'is_connected': True,
|
||
'connection_id': self.channel_name
|
||
}
|
||
)
|
||
|
||
if not created:
|
||
participant.is_connected = True
|
||
participant.reconnection_count += 1
|
||
participant.save()
|
||
|
||
# Отмечаем что участник подключился
|
||
room.mark_participant_joined(self.user)
|
||
|
||
# Если оба подключились и комната в ожидании, начинаем
|
||
if room.both_joined and room.status == 'waiting':
|
||
room.start()
|
||
|
||
except Exception as e:
|
||
logger.error(f'Error saving participant: {str(e)}')
|
||
|
||
@database_sync_to_async
|
||
def update_participant_disconnect(self):
|
||
"""Обновление при отключении участника."""
|
||
from .models import VideoRoom, VideoParticipant
|
||
try:
|
||
room = VideoRoom.objects.get(room_id=self.room_id)
|
||
participant = VideoParticipant.objects.get(room=room, user=self.user)
|
||
participant.disconnect()
|
||
except Exception as e:
|
||
logger.error(f'Error updating participant disconnect: {str(e)}')
|
||
|
||
@database_sync_to_async
|
||
def update_participant_media_state(self, audio_enabled, video_enabled):
|
||
"""Обновление состояния медиа участника."""
|
||
from .models import VideoRoom, VideoParticipant
|
||
try:
|
||
room = VideoRoom.objects.get(room_id=self.room_id)
|
||
participant = VideoParticipant.objects.get(room=room, user=self.user)
|
||
|
||
if audio_enabled is not None:
|
||
participant.is_audio_enabled = audio_enabled
|
||
if video_enabled is not None:
|
||
participant.is_video_enabled = video_enabled
|
||
|
||
participant.save()
|
||
except Exception as e:
|
||
logger.error(f'Error updating media state: {str(e)}')
|
||
|
||
@database_sync_to_async
|
||
def update_participant_screen_sharing(self, is_sharing):
|
||
"""Обновление демонстрации экрана."""
|
||
from .models import VideoRoom, VideoParticipant
|
||
try:
|
||
room = VideoRoom.objects.get(room_id=self.room_id)
|
||
participant = VideoParticipant.objects.get(room=room, user=self.user)
|
||
participant.is_screen_sharing = is_sharing
|
||
participant.save()
|
||
except Exception as e:
|
||
logger.error(f'Error updating screen sharing: {str(e)}')
|
||
|