""" API views для видеоконференций. """ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.shortcuts import get_object_or_404 from django.utils import timezone from .models import VideoRoom, VideoParticipant, VideoCallLog, ScreenRecording from .serializers import ( VideoRoomSerializer, VideoRoomCreateSerializer, VideoRoomStartSerializer, VideoRoomEndSerializer, VideoParticipantSerializer, VideoCallLogSerializer, ScreenRecordingSerializer, ScreenRecordingCreateSerializer ) from .permissions import IsVideoRoomParticipant from .services import get_sfu_client, SFUClientError import logging logger = logging.getLogger(__name__) class VideoRoomViewSet(viewsets.ModelViewSet): """ ViewSet для управления видеокомнатами. list: Список видеокомнат пользователя create: Создать видеокомнату retrieve: Получить информацию о комнате update: Обновить комнату destroy: Удалить комнату start: Начать видеозвонок end: Завершить видеозвонок join: Присоединиться к комнате """ permission_classes = [IsAuthenticated] lookup_field = 'room_id' def get_queryset(self): """Получение комнат пользователя.""" user = self.request.user if user.role == 'mentor': queryset = VideoRoom.objects.filter(mentor=user).select_related( 'lesson', 'mentor', 'client', 'client__user' ).prefetch_related('participants__user') else: queryset = VideoRoom.objects.filter( participants__user=user ).select_related( 'lesson', 'mentor', 'client', 'client__user' ).prefetch_related('participants__user').distinct() # Оптимизация: для списка используем only() для ограничения полей if self.action == 'list': queryset = queryset.only( 'id', 'room_id', 'lesson_id', 'mentor_id', 'client_id', 'status', 'started_at', 'ended_at', 'duration', 'is_recording', 'recording_url', 'quality_rating', 'quality_issues', 'created_at' ) return queryset.order_by('-created_at') def get_serializer_class(self): """Выбор сериализатора.""" if self.action == 'create': return VideoRoomCreateSerializer elif self.action == 'start': return VideoRoomStartSerializer elif self.action == 'end': return VideoRoomEndSerializer return VideoRoomSerializer def create(self, request, *args, **kwargs): """Создание видеокомнаты.""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) video_room = serializer.save() # Создаем комнату в sfu-server sfu_client = get_sfu_client() try: sfu_client.create_room(str(video_room.room_id)) logger.info(f"Video room {video_room.room_id} created in SFU server") except SFUClientError as e: logger.error(f"Failed to create room in SFU server: {e}") # Не прерываем создание комнаты в Django, но логируем ошибку # Комната будет создана в Django, но может не работать в SFU # Возвращаем полную информацию о комнате response_serializer = VideoRoomSerializer(video_room) return Response( response_serializer.data, status=status.HTTP_201_CREATED ) def destroy(self, request, *args, **kwargs): """Удаление видеокомнаты.""" room = self.get_object() room_id = str(room.room_id) # Удаляем комнату из sfu-server sfu_client = get_sfu_client() try: sfu_client.delete_room(room_id) logger.info(f"Video room {room_id} deleted from SFU server") except SFUClientError as e: logger.error(f"Failed to delete room from SFU server: {e}") # Продолжаем удаление из Django даже если SFU недоступен # Удаляем комнату из Django return super().destroy(request, *args, **kwargs) @action(detail=True, methods=['post']) def start(self, request, room_id=None): """ Начать видеозвонок. POST /api/video/rooms/{room_id}/start/ """ room = self.get_object() # Проверяем права (оптимизация: проверяем client.user если есть) client_user = room.client.user if hasattr(room.client, 'user') else room.client if request.user not in [room.mentor, client_user]: return Response( {'error': 'У вас нет доступа к этой комнате'}, status=status.HTTP_403_FORBIDDEN ) serializer = self.get_serializer(room, data={}) serializer.is_valid(raise_exception=True) serializer.save() response_serializer = VideoRoomSerializer(room) return Response(response_serializer.data) @action(detail=True, methods=['post']) def end(self, request, room_id=None): """ Завершить видеозвонок. POST /api/video/rooms/{room_id}/end/ Body: { "quality_rating": 5, // опционально "quality_issues": "" // опционально } """ room = self.get_object() # Проверяем права (может завершить только ментор) if request.user != room.mentor: return Response( {'error': 'Только ментор может завершить звонок'}, status=status.HTTP_403_FORBIDDEN ) serializer = self.get_serializer(room, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() response_serializer = VideoRoomSerializer(room) return Response(response_serializer.data) @action(detail=True, methods=['get']) def join(self, request, room_id=None): """ Получить информацию для присоединения к комнате. GET /api/video/rooms/{room_id}/join/ Возвращает: - room_id: UUID комнаты - ws_url: WebSocket URL для подключения - ice_servers: STUN/TURN серверы - participant_info: информация об участнике """ room = self.get_object() # Проверяем права (оптимизация: проверяем client.user если есть) client_user = room.client.user if hasattr(room.client, 'user') else room.client if request.user not in [room.mentor, client_user]: return Response( {'error': 'У вас нет доступа к этой комнате'}, status=status.HTTP_403_FORBIDDEN ) # Получаем или создаем участника participant, created = VideoParticipant.objects.get_or_create( room=room, user=request.user, defaults={'is_connected': False} ) # Убеждаемся, что комната существует в sfu-server sfu_client = get_sfu_client() try: # Проверяем, существует ли комната sfu_client.get_room(str(room.room_id)) except SFUClientError: # Комната не существует, создаем её try: sfu_client.create_room(str(room.room_id)) logger.info(f"Video room {room.room_id} created in SFU server during join") except SFUClientError as e: logger.error(f"Failed to create room in SFU server during join: {e}") return Response( {'error': 'Не удалось создать комнату в SFU сервере'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # WebSocket URL для sfu-server from django.conf import settings sfu_ws_url = getattr(settings, 'SFU_WS_URL', 'ws://sfu-server:7001') # Заменяем внутренний адрес на внешний для клиента if 'sfu-server' in sfu_ws_url: # В production нужно использовать правильный внешний адрес sfu_ws_url = sfu_ws_url.replace('sfu-server', 'localhost') ws_url = f"{sfu_ws_url}/ws/{room.room_id}" # ICE серверы (STUN/TURN) ice_servers = [ { 'urls': 'stun:stun.l.google.com:19302' }, { 'urls': 'stun:stun1.l.google.com:19302' } ] return Response({ 'room_id': str(room.room_id), 'ws_url': ws_url, 'ice_servers': ice_servers, 'participant_info': VideoParticipantSerializer(participant).data, 'room_info': VideoRoomSerializer(room).data }) @action(detail=False, methods=['get']) def active(self, request): """ Получить список активных комнат пользователя. GET /api/video/rooms/active/ """ queryset = self.get_queryset().filter(status='active') serializer = VideoRoomSerializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=['get']) def history(self, request): """ Получить историю видеозвонков. GET /api/video/rooms/history/ """ queryset = self.get_queryset().filter(status='ended').order_by('-ended_at') # Пагинация page = self.paginate_queryset(queryset) if page is not None: serializer = VideoRoomSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = VideoRoomSerializer(queryset, many=True) return Response(serializer.data) class VideoParticipantViewSet(viewsets.ReadOnlyModelViewSet): """ ViewSet для просмотра участников видео. list: Список участников retrieve: Информация об участнике """ serializer_class = VideoParticipantSerializer permission_classes = [IsAuthenticated, IsVideoRoomParticipant] def get_queryset(self): """Получение участников.""" user = self.request.user # Оптимизация: получаем только ID комнат для фильтрации if user.role == 'mentor': room_ids = VideoRoom.objects.filter(mentor=user).values_list('id', flat=True) else: room_ids = VideoRoom.objects.filter(client=user).values_list('id', flat=True) queryset = VideoParticipant.objects.filter( room_id__in=room_ids ).select_related('user', 'room', 'room__mentor', 'room__client') # Оптимизация: для списка используем only() для ограничения полей if self.action == 'list': queryset = queryset.only( 'id', 'room_id', 'user_id', 'is_connected', 'is_audio_enabled', 'is_video_enabled', 'is_screen_sharing', 'joined_at', 'left_at', 'reconnection_count', 'created_at', 'updated_at' ) return queryset class VideoCallLogViewSet(viewsets.ReadOnlyModelViewSet): """ ViewSet для просмотра логов видеозвонков. list: Список логов retrieve: Детали лога """ serializer_class = VideoCallLogSerializer permission_classes = [IsAuthenticated] def get_queryset(self): """Получение логов.""" user = self.request.user # Оптимизация: получаем только ID комнат для фильтрации if user.role == 'mentor': room_ids = VideoRoom.objects.filter(mentor=user).values_list('id', flat=True) else: room_ids = VideoRoom.objects.filter(client=user).values_list('id', flat=True) queryset = VideoCallLog.objects.filter( room_id__in=room_ids ).select_related('room', 'room__lesson', 'room__mentor', 'room__client', 'room__client__user') # Оптимизация: для списка используем only() для ограничения полей if self.action == 'list': queryset = queryset.only( 'id', 'room_id', 'call_duration', 'quality_rating', 'quality_issues', 'participants_count', 'created_at', 'updated_at' ) return queryset.order_by('-created_at') class ScreenRecordingViewSet(viewsets.ModelViewSet): """ ViewSet для управления записями видео. list: Список записей create: Начать запись retrieve: Получить запись update: Обновить запись destroy: Удалить запись """ serializer_class = ScreenRecordingSerializer permission_classes = [IsAuthenticated] def get_queryset(self): """Получение записей.""" user = self.request.user # Оптимизация: получаем только ID комнат для фильтрации if user.role == 'mentor': room_ids = VideoRoom.objects.filter(mentor=user).values_list('id', flat=True) else: room_ids = VideoRoom.objects.filter(client=user).values_list('id', flat=True) queryset = ScreenRecording.objects.filter( room_id__in=room_ids ).select_related('room', 'room__lesson', 'room__mentor', 'room__client') # Оптимизация: для списка используем only() для ограничения полей if self.action == 'list': queryset = queryset.only( 'id', 'room_id', 'file_path', 'file_size', 'duration', 'status', 'started_at', 'ended_at', 'created_at', 'updated_at' ) return queryset.order_by('-created_at') def get_serializer_class(self): """Выбор сериализатора.""" if self.action == 'create': return ScreenRecordingCreateSerializer return ScreenRecordingSerializer def create(self, request, *args, **kwargs): """Начать запись.""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) recording = serializer.save() # Возвращаем полную информацию response_serializer = ScreenRecordingSerializer(recording) return Response( response_serializer.data, status=status.HTTP_201_CREATED ) @action(detail=False, methods=['get']) def available(self, request): """ Получить доступные записи (готовые и не истекшие). GET /api/video/recordings/available/ """ queryset = self.get_queryset().filter( status='ready' ).exclude( expires_at__lt=timezone.now() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data)