264 lines
10 KiB
Python
264 lines
10 KiB
Python
"""
|
||
Django views для работы с Janus Gateway.
|
||
Параллельная реализация с ion-sfu.
|
||
"""
|
||
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.conf import settings
|
||
from django.utils import timezone
|
||
|
||
from .models import VideoRoom, VideoParticipant, VideoCallLog
|
||
from .serializers import VideoRoomSerializer
|
||
from .janus_client import get_janus_client, JanusClient
|
||
from .permissions import IsVideoRoomParticipant
|
||
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class JanusVideoRoomViewSet(viewsets.ViewSet):
|
||
"""
|
||
ViewSet для управления видеокомнатами через Janus Gateway.
|
||
Параллельная реализация с IonSFUVideoRoomViewSet.
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
lookup_field = 'room_id'
|
||
|
||
@action(detail=False, methods=['post'], url_path='create-room')
|
||
def create_room(self, request):
|
||
"""
|
||
Создать новую видеокомнату в Janus Gateway.
|
||
|
||
POST /api/video/janus/create-room/
|
||
|
||
Body:
|
||
{
|
||
"lesson_id": 123,
|
||
"is_recording": false,
|
||
"max_participants": 6
|
||
}
|
||
"""
|
||
lesson_id = request.data.get('lesson_id')
|
||
is_recording = request.data.get('is_recording', False)
|
||
max_participants = request.data.get('max_participants', 6)
|
||
|
||
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 not in [lesson.mentor, lesson.client]:
|
||
return Response(
|
||
{'error': 'У вас нет доступа к этому занятию'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Проверяем, есть ли уже комната
|
||
if hasattr(lesson, 'video_room'):
|
||
return Response(
|
||
{'error': 'Видеокомната для этого занятия уже существует'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
try:
|
||
# Создаем комнату в Django
|
||
video_room = VideoRoom.objects.create(
|
||
lesson=lesson,
|
||
mentor=lesson.mentor,
|
||
client=lesson.client,
|
||
is_recording=is_recording,
|
||
max_participants=max_participants,
|
||
sfu_type='janus' # Указываем тип SFU
|
||
)
|
||
|
||
# Создаем комнату в Janus Gateway
|
||
with get_janus_client() as janus:
|
||
# Используем числовой ID для Janus (преобразуем UUID в число)
|
||
janus_room_id = int(str(video_room.room_id).replace('-', '')[:12], 16) % (2**31 - 1)
|
||
|
||
# Генерируем уникальный secret для комнаты (или используем из настроек)
|
||
from django.conf import settings
|
||
room_secret = getattr(settings, 'JANUS_ROOM_SECRET', 'adminpwd')
|
||
|
||
janus.create_room(
|
||
room_id=janus_room_id,
|
||
description=f"Занятие: {lesson.title}",
|
||
publishers=max_participants,
|
||
secret=room_secret
|
||
)
|
||
|
||
# Сохраняем Janus room ID в router_id
|
||
video_room.router_id = str(janus_room_id)
|
||
video_room.save()
|
||
|
||
logger.info(f"Janus room created: Django={video_room.room_id}, Janus={janus_room_id}")
|
||
|
||
serializer = VideoRoomSerializer(video_room)
|
||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating Janus room: {e}")
|
||
# Удаляем комнату из Django, если не удалось создать в Janus
|
||
if video_room:
|
||
video_room.delete()
|
||
return Response(
|
||
{'error': str(e)},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
@action(detail=True, methods=['delete'], url_path='destroy-room', lookup_field='room_id')
|
||
def destroy_room(self, request, room_id=None):
|
||
"""
|
||
Удалить видеокомнату из Janus Gateway.
|
||
|
||
DELETE /api/video/janus/{room_id}/destroy-room/
|
||
"""
|
||
try:
|
||
video_room = VideoRoom.objects.select_related('lesson', 'mentor', 'client', 'client__user').get(room_id=room_id, sfu_type='janus')
|
||
except VideoRoom.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Видеокомната не найдена'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверяем права
|
||
client_user = video_room.client.user if hasattr(video_room.client, 'user') else video_room.client
|
||
if request.user not in [video_room.mentor, client_user]:
|
||
return Response(
|
||
{'error': 'У вас нет доступа к этой комнате'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
try:
|
||
# Удаляем комнату из Janus
|
||
if video_room.router_id:
|
||
from django.conf import settings
|
||
room_secret = getattr(settings, 'JANUS_ROOM_SECRET', 'adminpwd')
|
||
|
||
with get_janus_client() as janus:
|
||
janus.destroy_room(int(video_room.router_id), secret=room_secret)
|
||
|
||
# Удаляем из Django
|
||
video_room.delete()
|
||
|
||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error destroying Janus room: {e}")
|
||
return Response(
|
||
{'error': str(e)},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
@action(detail=True, methods=['get'], url_path='join', lookup_field='room_id')
|
||
def join(self, request, room_id=None):
|
||
"""
|
||
Получить информацию для присоединения к Janus комнате.
|
||
|
||
GET /api/video/janus/{room_id}/join/
|
||
|
||
Returns:
|
||
{
|
||
"room_id": "uuid",
|
||
"janus_room_id": 12345,
|
||
"http_url": "http://localhost:8088/janus",
|
||
"ws_url": "ws://localhost:8188",
|
||
"ice_servers": [...],
|
||
"participant_info": {...},
|
||
"room_info": {...}
|
||
}
|
||
"""
|
||
try:
|
||
video_room = VideoRoom.objects.select_related('lesson', 'mentor', 'client', 'client__user').get(room_id=room_id, sfu_type='janus')
|
||
except VideoRoom.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Видеокомната не найдена'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверяем права
|
||
client_user = video_room.client.user if hasattr(video_room.client, 'user') else video_room.client
|
||
if request.user not in [video_room.mentor, client_user]:
|
||
return Response(
|
||
{'error': 'У вас нет доступа к этой комнате'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Получаем или создаем участника
|
||
participant, created = VideoParticipant.objects.get_or_create(
|
||
room=video_room,
|
||
user=request.user,
|
||
defaults={'is_connected': False}
|
||
)
|
||
|
||
# ICE серверы (STUN/TURN)
|
||
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(video_room.router_id) if video_room.router_id else None
|
||
|
||
return Response({
|
||
'room_id': str(video_room.room_id),
|
||
'janus_room_id': janus_room_id,
|
||
'http_url': settings.JANUS_HTTP_URL,
|
||
'ws_url': settings.JANUS_WS_URL,
|
||
'ice_servers': ice_servers,
|
||
'participant_info': {
|
||
'user_id': request.user.id,
|
||
'username': request.user.get_full_name(),
|
||
'role': 'publisher', # В Janus все участники могут быть publishers
|
||
},
|
||
'room_info': VideoRoomSerializer(video_room).data,
|
||
'sfu_type': 'janus'
|
||
})
|
||
|
||
@action(detail=False, methods=['get'], url_path='health')
|
||
def health(self, request):
|
||
"""
|
||
Проверить доступность Janus Gateway.
|
||
|
||
GET /api/video/janus/health/
|
||
"""
|
||
try:
|
||
import requests
|
||
response = requests.get(
|
||
f"{settings.JANUS_HTTP_URL.replace('/janus', '')}/janus/info",
|
||
timeout=2
|
||
)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
return Response({
|
||
'status': 'healthy',
|
||
'janus_info': data
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Janus health check failed: {e}")
|
||
return Response(
|
||
{
|
||
'status': 'unhealthy',
|
||
'error': str(e)
|
||
},
|
||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||
)
|
||
|