""" 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)}')