366 lines
14 KiB
Python
366 lines
14 KiB
Python
"""
|
||
API views для работы с LiveKit.
|
||
"""
|
||
import json
|
||
import logging
|
||
import uuid
|
||
from rest_framework import status
|
||
from rest_framework.decorators import api_view, permission_classes
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from rest_framework.response import Response
|
||
from django.conf import settings
|
||
|
||
from .livekit_service import LiveKitService
|
||
from .models import VideoRoom
|
||
from apps.board.models import Board
|
||
from apps.schedule.models import Lesson
|
||
from apps.users.utils import format_datetime_for_user
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@api_view(['POST'])
|
||
@permission_classes([IsAuthenticated])
|
||
def create_livekit_room(request):
|
||
"""
|
||
Получение данных LiveKit комнаты для занятия.
|
||
Комната создаётся автоматически при создании урока.
|
||
|
||
POST /api/video/livekit/create-room/
|
||
Body: {
|
||
"lesson_id": 123
|
||
}
|
||
|
||
Returns:
|
||
{
|
||
"room_name": "uuid",
|
||
"access_token": "token",
|
||
"video_room_id": 1,
|
||
"server_url": "ws://livekit:7880",
|
||
"ice_servers": [...]
|
||
}
|
||
"""
|
||
lesson_id = request.data.get('lesson_id')
|
||
|
||
if not lesson_id:
|
||
return Response(
|
||
{'error': 'lesson_id обязателен'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
try:
|
||
lesson = Lesson.objects.get(id=lesson_id)
|
||
except Lesson.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Занятие не найдено'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверка прав доступа
|
||
user = request.user
|
||
if user != lesson.mentor and (not hasattr(user, 'client_profile') or user.client_profile != lesson.client):
|
||
return Response(
|
||
{'error': 'Нет доступа к этому занятию'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Проверка времени подключения
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
now = timezone.now()
|
||
|
||
# Можно подключиться за 10 минут до начала урока
|
||
allowed_start_time = lesson.start_time - timedelta(minutes=10)
|
||
if now < allowed_start_time:
|
||
minutes_until_allowed = int((allowed_start_time - now).total_seconds() / 60)
|
||
return Response(
|
||
{'error': f'Подключение к уроку будет доступно за 10 минут до начала ({allowed_start_time.strftime("%H:%M")}). Осталось {minutes_until_allowed} минут.'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Нельзя подключиться позже 15 минут после окончания урока
|
||
if lesson.end_time:
|
||
# Используем фактическое время завершения, если оно есть, иначе запланированное
|
||
actual_end_time = lesson.completed_at if (lesson.status == 'completed' and lesson.completed_at) else lesson.end_time
|
||
allowed_end_time = actual_end_time + timedelta(minutes=15)
|
||
|
||
if now > allowed_end_time:
|
||
return Response(
|
||
{'error': 'Доступ к видеокомнате закрыт. Прошло более 15 минут после завершения урока.'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Проверяем, есть ли LiveKit комната для этого занятия
|
||
if not lesson.livekit_room_name:
|
||
return Response(
|
||
{'error': 'LiveKit комната не создана для этого урока. Обратитесь к администратору.'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
room_name = lesson.livekit_room_name
|
||
|
||
# Получаем или создаем VideoRoom запись
|
||
try:
|
||
video_room = VideoRoom.objects.get(lesson=lesson)
|
||
except VideoRoom.DoesNotExist:
|
||
client_user = lesson.client.user if hasattr(lesson.client, 'user') else lesson.client
|
||
video_room = VideoRoom.objects.create(
|
||
lesson=lesson,
|
||
mentor=lesson.mentor,
|
||
client=client_user,
|
||
room_id=room_name,
|
||
is_recording=True,
|
||
max_participants=10 if lesson.group else 2
|
||
)
|
||
|
||
# Определяем роль пользователя
|
||
is_admin = user == lesson.mentor
|
||
|
||
# Получаем или создаём доску для пары ментор–студент (одна доска на пару, в metadata токена)
|
||
mentor_id = lesson.mentor_id
|
||
client_obj = lesson.client
|
||
student_user = client_obj.user if hasattr(client_obj, 'user') else client_obj
|
||
student_id = getattr(student_user, 'id', getattr(student_user, 'pk', None))
|
||
token_metadata = None
|
||
if mentor_id and student_id:
|
||
board, created = Board.objects.get_or_create(
|
||
mentor_id=mentor_id,
|
||
student_id=student_id,
|
||
defaults={
|
||
'title': 'Доска для совместной работы',
|
||
'description': 'Интерактивная доска для занятия',
|
||
'access_type': 'mentor_student',
|
||
'owner': lesson.mentor,
|
||
}
|
||
)
|
||
if created:
|
||
board.participants.add(lesson.mentor, student_user)
|
||
token_metadata = json.dumps({
|
||
'board_id': str(board.board_id),
|
||
'is_mentor': is_admin,
|
||
})
|
||
|
||
participant_identity = str(user.pk) if hasattr(user, 'pk') and user.pk else str(user.id)
|
||
participant_name = user.get_full_name() or user.email or f"User {participant_identity}"
|
||
|
||
access_token = LiveKitService.generate_access_token(
|
||
room_name=room_name,
|
||
participant_name=participant_name,
|
||
participant_identity=participant_identity,
|
||
is_admin=is_admin,
|
||
expires_in_minutes=180, # 3 часа
|
||
metadata=token_metadata,
|
||
)
|
||
|
||
logger.info(f'LiveKit token generated for user {user.id} (identity={participant_identity}) in room {room_name}')
|
||
|
||
# Получаем ICE серверы
|
||
ice_servers = LiveKitService.get_ice_servers()
|
||
server_url = LiveKitService.get_server_url(request=request)
|
||
|
||
return Response({
|
||
'room_name': str(room_name),
|
||
'access_token': access_token,
|
||
'server_url': server_url,
|
||
'ice_servers': ice_servers,
|
||
'video_room_id': video_room.id,
|
||
'is_admin': is_admin,
|
||
'lesson': {
|
||
'id': lesson.id,
|
||
'title': lesson.title,
|
||
'start_time': format_datetime_for_user(lesson.start_time, request.user.timezone) if lesson.start_time else None,
|
||
'end_time': format_datetime_for_user(lesson.end_time, request.user.timezone) if lesson.end_time else None,
|
||
}
|
||
})
|
||
|
||
|
||
@api_view(['GET'])
|
||
@permission_classes([IsAuthenticated])
|
||
def get_livekit_config(request):
|
||
"""
|
||
Получение конфигурации LiveKit для фронтенда.
|
||
|
||
GET /api/video/livekit/config/
|
||
|
||
Returns:
|
||
{
|
||
"server_url": "ws://livekit:7880",
|
||
"ice_servers": [...]
|
||
}
|
||
"""
|
||
return Response({
|
||
'server_url': LiveKitService.get_server_url(request=request),
|
||
'ice_servers': LiveKitService.get_ice_servers(),
|
||
})
|
||
|
||
|
||
@api_view(['DELETE'])
|
||
@permission_classes([IsAuthenticated])
|
||
def delete_livekit_room_by_lesson(request, lesson_id):
|
||
"""
|
||
Удаление LiveKit видеокомнаты по ID занятия.
|
||
|
||
DELETE /api/video/livekit/rooms/lesson/{lesson_id}/
|
||
|
||
Проверяет, что прошло минимум 10 минут после окончания занятия.
|
||
Только ментор может удалять видеокомнату.
|
||
"""
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
|
||
try:
|
||
lesson = Lesson.objects.get(id=lesson_id)
|
||
except Lesson.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Занятие не найдено'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверка прав доступа - только ментор может удалять
|
||
user = request.user
|
||
if user != lesson.mentor:
|
||
return Response(
|
||
{'error': 'Только ментор может удалять видеокомнату'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Проверка времени - можно удалять только через 10 минут после фактического окончания
|
||
if lesson.end_time:
|
||
now = timezone.now()
|
||
|
||
# Используем фактическое время завершения, если оно есть, иначе запланированное
|
||
actual_end_time = lesson.completed_at if (lesson.status == 'completed' and lesson.completed_at) else lesson.end_time
|
||
allowed_delete_time = actual_end_time + timedelta(minutes=10)
|
||
|
||
if now < allowed_delete_time:
|
||
minutes_remaining = int((allowed_delete_time - now).total_seconds() / 60)
|
||
return Response(
|
||
{
|
||
'error': f'Видеокомнату можно удалить только через 10 минут после окончания занятия. Осталось {minutes_remaining} минут.'
|
||
},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
else:
|
||
return Response(
|
||
{'error': 'Время окончания занятия не указано'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# Проверяем, есть ли VideoRoom для этого занятия
|
||
try:
|
||
video_room = VideoRoom.objects.get(lesson=lesson)
|
||
|
||
# Удаляем комнату из SFU сервера (если используется)
|
||
from .services import get_sfu_client, SFUClientError
|
||
sfu_client = get_sfu_client()
|
||
try:
|
||
sfu_client.delete_room(str(video_room.room_id))
|
||
except SFUClientError as e:
|
||
# Логируем ошибку, но продолжаем удаление из Django
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.warning(f'Не удалось удалить комнату из SFU сервера: {e}')
|
||
|
||
# Удаляем комнату из базы данных
|
||
video_room.delete()
|
||
|
||
return Response(
|
||
{'message': 'Видеокомната успешно удалена'},
|
||
status=status.HTTP_200_OK
|
||
)
|
||
except VideoRoom.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Видеокомната для этого занятия не найдена'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
|
||
@api_view(['POST'])
|
||
@permission_classes([IsAuthenticated])
|
||
def update_livekit_participant_media_state(request):
|
||
"""
|
||
Обновление состояния медиа (камера/микрофон) участника LiveKit комнаты.
|
||
|
||
POST /api/video/livekit/update-media-state/
|
||
Body: {
|
||
"room_name": "uuid",
|
||
"audio_enabled": true,
|
||
"video_enabled": false
|
||
}
|
||
"""
|
||
room_name = request.data.get('room_name')
|
||
audio_enabled = request.data.get('audio_enabled')
|
||
video_enabled = request.data.get('video_enabled')
|
||
|
||
if not room_name:
|
||
return Response(
|
||
{'error': 'room_name обязателен'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
if audio_enabled is None and video_enabled is None:
|
||
return Response(
|
||
{'error': 'Необходимо указать хотя бы одно из: audio_enabled, video_enabled'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
try:
|
||
# Находим комнату по room_name (это строка room_id)
|
||
try:
|
||
import uuid
|
||
room_uuid = uuid.UUID(str(room_name))
|
||
video_room = VideoRoom.objects.get(room_id=room_uuid)
|
||
except (ValueError, VideoRoom.DoesNotExist):
|
||
return Response(
|
||
{'error': 'Видеокомната не найдена'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверяем права доступа
|
||
user = request.user
|
||
client_user = video_room.client.user if hasattr(video_room.client, 'user') else video_room.client
|
||
if user != video_room.mentor and user != client_user:
|
||
return Response(
|
||
{'error': 'Нет доступа к этой видеокомнате'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Находим или создаем участника
|
||
from .models import VideoParticipant
|
||
participant, created = VideoParticipant.objects.get_or_create(
|
||
room=video_room,
|
||
user=user,
|
||
defaults={
|
||
'is_connected': True,
|
||
'is_audio_enabled': audio_enabled if audio_enabled is not None else True,
|
||
'is_video_enabled': video_enabled if video_enabled is not None else True,
|
||
}
|
||
)
|
||
|
||
if not created:
|
||
# Обновляем существующего участника
|
||
if audio_enabled is not None:
|
||
participant.is_audio_enabled = audio_enabled
|
||
if video_enabled is not None:
|
||
participant.is_video_enabled = video_enabled
|
||
if not participant.is_connected:
|
||
participant.is_connected = True
|
||
participant.save()
|
||
|
||
return Response({
|
||
'success': True,
|
||
'participant_id': participant.id,
|
||
'audio_enabled': participant.is_audio_enabled,
|
||
'video_enabled': participant.is_video_enabled,
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.error(f'Ошибка обновления медиа-состояния участника: {str(e)}')
|
||
return Response(
|
||
{'error': f'Ошибка обновления медиа-состояния: {str(e)}'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|