418 lines
17 KiB
Python
418 lines
17 KiB
Python
"""
|
||
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)
|