uchill/backend/apps/video/views.py

418 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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