286 lines
12 KiB
Python
286 lines
12 KiB
Python
"""
|
||
Views для работы с токенами доступа к видеокомнатам.
|
||
"""
|
||
from rest_framework import viewsets, status
|
||
from rest_framework.decorators import action
|
||
from rest_framework.response import Response
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from django.shortcuts import get_object_or_404
|
||
|
||
from .models import VideoRoom
|
||
from .serializers import VideoRoomSerializer
|
||
from apps.users.utils import format_datetime_for_user
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class VideoRoomTokenViewSet(viewsets.ViewSet):
|
||
"""
|
||
ViewSet для работы с токенами доступа к видеокомнатам.
|
||
"""
|
||
|
||
@action(detail=False, methods=['get'], url_path='join/(?P<token>[^/.]+)', permission_classes=[AllowAny])
|
||
def join_by_token(self, request, token=None):
|
||
"""
|
||
Получить информацию о комнате по токену доступа.
|
||
|
||
GET /api/video/token/join/{token}/
|
||
|
||
Возвращает информацию для подключения к комнате.
|
||
Доступно без авторизации - по токену.
|
||
"""
|
||
try:
|
||
# Находим комнату по токену с оптимизацией запросов
|
||
room = get_object_or_404(
|
||
VideoRoom.objects.select_related('lesson', 'mentor', 'client', 'client__user'),
|
||
access_token=token
|
||
)
|
||
|
||
# Проверяем статус занятия
|
||
if room.lesson.status == 'cancelled':
|
||
return Response(
|
||
{'error': 'Занятие отменено'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# Проверяем, что занятие скоро начнется или уже идет
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
|
||
now = timezone.now()
|
||
lesson_start = room.lesson.start_time
|
||
lesson_end = lesson_start + timedelta(minutes=room.lesson.duration)
|
||
|
||
# Разрешаем подключиться за 15 минут до начала
|
||
can_join_from = lesson_start - timedelta(minutes=15)
|
||
|
||
if now < can_join_from:
|
||
return Response(
|
||
{
|
||
'error': 'Слишком рано для подключения',
|
||
'message': f'Занятие начнется {lesson_start.strftime("%d.%m.%Y в %H:%M")}. Подключиться можно за 15 минут до начала.'
|
||
},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# Формируем ответ в зависимости от типа SFU
|
||
if room.sfu_type == 'janus':
|
||
return self._get_janus_join_info(room)
|
||
else:
|
||
return self._get_ion_sfu_join_info(room)
|
||
|
||
except VideoRoom.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Комната не найдена или токен недействителен'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error in join_by_token: {e}")
|
||
return Response(
|
||
{'error': 'Произошла ошибка при подключении к комнате'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
def _get_janus_join_info(self, room: VideoRoom):
|
||
"""Получить информацию для подключения к Janus комнате"""
|
||
from django.conf import settings
|
||
|
||
# ICE серверы
|
||
ice_servers = [
|
||
{'urls': 'stun:stun.l.google.com:19302'},
|
||
{'urls': 'stun:stun1.l.google.com:19302'}
|
||
]
|
||
|
||
# Janus room ID из router_id
|
||
janus_room_id = int(room.router_id) if room.router_id else None
|
||
|
||
# Информация об участниках
|
||
mentor_name = f"{room.mentor.first_name} {room.mentor.last_name}".strip() or room.mentor.email
|
||
|
||
if hasattr(room.lesson.client, 'user'):
|
||
client_name = f"{room.lesson.client.user.first_name} {room.lesson.client.user.last_name}".strip() or room.lesson.client.user.email
|
||
else:
|
||
client_name = "Ученик"
|
||
|
||
return Response({
|
||
'success': True,
|
||
'room_id': str(room.room_id),
|
||
'janus_room_id': janus_room_id,
|
||
'sfu_type': 'janus',
|
||
'http_url': getattr(settings, 'JANUS_CLIENT_HTTP_URL', 'http://localhost:8088/janus'),
|
||
'ws_url': getattr(settings, 'JANUS_CLIENT_WS_URL', 'ws://localhost:8188'),
|
||
'ice_servers': ice_servers,
|
||
'lesson': {
|
||
'id': room.lesson.id,
|
||
'title': room.lesson.title,
|
||
'subject': room.lesson.subject,
|
||
'start_time': format_datetime_for_user(room.lesson.start_time, request.user.timezone) if room.lesson.start_time else None,
|
||
'duration': room.lesson.duration,
|
||
},
|
||
'participants': {
|
||
'mentor': {
|
||
'id': room.mentor.id,
|
||
'name': mentor_name,
|
||
},
|
||
'client': {
|
||
'name': client_name,
|
||
}
|
||
}
|
||
})
|
||
|
||
def _get_ion_sfu_join_info(self, room: VideoRoom):
|
||
"""Получить информацию для подключения к ion-sfu комнате"""
|
||
from django.conf import settings
|
||
|
||
# ICE серверы
|
||
ice_servers = [
|
||
{'urls': 'stun:stun.l.google.com:19302'},
|
||
{'urls': 'stun:stun1.l.google.com:19302'}
|
||
]
|
||
|
||
# WebSocket URL для ion-sfu
|
||
sfu_ws_url = getattr(settings, 'SFU_WS_URL', 'ws://localhost:7001')
|
||
if 'sfu-server' in sfu_ws_url:
|
||
sfu_ws_url = sfu_ws_url.replace('sfu-server', 'localhost')
|
||
|
||
ws_url = f"{sfu_ws_url}/ws/{room.room_id}"
|
||
|
||
# Информация об участниках
|
||
mentor_name = f"{room.mentor.first_name} {room.mentor.last_name}".strip() or room.mentor.email
|
||
|
||
if hasattr(room.lesson.client, 'user'):
|
||
client_name = f"{room.lesson.client.user.first_name} {room.lesson.client.user.last_name}".strip() or room.lesson.client.user.email
|
||
else:
|
||
client_name = "Ученик"
|
||
|
||
return Response({
|
||
'success': True,
|
||
'room_id': str(room.room_id),
|
||
'sfu_type': 'ion-sfu',
|
||
'ws_url': ws_url,
|
||
'ice_servers': ice_servers,
|
||
'lesson': {
|
||
'id': room.lesson.id,
|
||
'title': room.lesson.title,
|
||
'subject': room.lesson.subject,
|
||
'start_time': format_datetime_for_user(room.lesson.start_time, request.user.timezone) if room.lesson.start_time else None,
|
||
'duration': room.lesson.duration,
|
||
},
|
||
'participants': {
|
||
'mentor': {
|
||
'id': room.mentor.id,
|
||
'name': mentor_name,
|
||
},
|
||
'client': {
|
||
'name': client_name,
|
||
}
|
||
}
|
||
})
|
||
|
||
@action(detail=False, methods=['post'], url_path='create-for-lesson', permission_classes=[IsAuthenticated])
|
||
def create_for_lesson(self, request):
|
||
"""
|
||
Создать видеокомнату для занятия.
|
||
|
||
POST /api/video/token/create-for-lesson/
|
||
|
||
Body:
|
||
{
|
||
"lesson_id": 123,
|
||
"sfu_type": "janus" // или "ion-sfu"
|
||
}
|
||
"""
|
||
lesson_id = request.data.get('lesson_id')
|
||
sfu_type = request.data.get('sfu_type', 'janus')
|
||
|
||
if not lesson_id:
|
||
return Response(
|
||
{'error': 'lesson_id обязателен'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
try:
|
||
from apps.schedule.models import Lesson
|
||
lesson = Lesson.objects.select_related('mentor', 'client', 'client__user').get(id=lesson_id)
|
||
except Lesson.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Занятие не найдено'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверяем права (только ментор может создать комнату)
|
||
if request.user != lesson.mentor:
|
||
return Response(
|
||
{'error': 'Только ментор может создать видеокомнату для занятия'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Проверяем, есть ли уже комната
|
||
if hasattr(lesson, 'video_room'):
|
||
room = lesson.video_room
|
||
return Response({
|
||
'success': True,
|
||
'room_id': str(room.room_id),
|
||
'access_token': room.access_token,
|
||
'join_url': room.get_join_url(request),
|
||
'message': 'Комната уже существует'
|
||
})
|
||
|
||
room = None
|
||
try:
|
||
# Создаем комнату
|
||
# lesson.client - это Client объект, нужен User
|
||
client_user = lesson.client.user if hasattr(lesson.client, 'user') else lesson.client
|
||
|
||
room = VideoRoom.objects.create(
|
||
lesson=lesson,
|
||
mentor=lesson.mentor,
|
||
client=client_user,
|
||
sfu_type=sfu_type,
|
||
max_participants=6 if lesson.group else 2
|
||
)
|
||
|
||
# Если используем Janus, создаем комнату на сервере
|
||
if sfu_type == 'janus':
|
||
from .janus_client import get_janus_client
|
||
from django.conf import settings
|
||
|
||
with get_janus_client() as janus:
|
||
janus_room_id = int(str(room.room_id).replace('-', '')[:12], 16) % (2**31 - 1)
|
||
room_secret = getattr(settings, 'JANUS_ROOM_SECRET', 'adminpwd')
|
||
|
||
janus.create_room(
|
||
room_id=janus_room_id,
|
||
description=f"Занятие: {lesson.title}",
|
||
publishers=room.max_participants,
|
||
secret=room_secret
|
||
)
|
||
|
||
room.router_id = str(janus_room_id)
|
||
room.save()
|
||
|
||
logger.info(f"Video room created for lesson {lesson_id}: {room.room_id}")
|
||
|
||
return Response({
|
||
'success': True,
|
||
'room_id': str(room.room_id),
|
||
'access_token': room.access_token,
|
||
'join_url': room.get_join_url(request),
|
||
'sfu_type': sfu_type
|
||
}, status=status.HTTP_201_CREATED)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating video room: {e}")
|
||
# Удаляем комнату из Django, если она была создана
|
||
if room is not None:
|
||
try:
|
||
room.delete()
|
||
except Exception as delete_error:
|
||
logger.error(f"Error deleting room after failed creation: {delete_error}")
|
||
return Response(
|
||
{'error': str(e)},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|