Browse Source

online class enhancements

add hook to does not loose the last recording while class is ending

participants logic restored

syncing lessons of course logic added
master
Mohsen Taba 1 month ago
parent
commit
4019594d57
  1. 64
      apps/course/services/web_egress.py
  2. 2
      apps/course/views/admin.py
  3. 74
      apps/course/views/course.py
  4. 38
      apps/course/views/live_session.py
  5. 3
      apps/course/views/webhook.py

64
apps/course/services/web_egress.py

@ -6,6 +6,7 @@ from typing import Any, Dict, Optional
import requests import requests
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils import timezone
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -126,3 +127,66 @@ class WebEgressClient:
return response.json() return response.json()
except ValueError: except ValueError:
return None return None
def is_web_egress_recording_status(status_value: str) -> bool:
return str(status_value or "").strip().lower() in {
"starting",
"recording",
"stopping",
}
def stop_session_web_egress_if_active(session) -> bool:
"""
Best-effort stop for an active WebEgress recording.
Returns True when a stop request was sent, otherwise False.
This helper never raises and is safe to call from close-session flows.
"""
if not getattr(settings, "ONLINE_CLASS_WEB_EGRESS_ENABLED", False):
return False
if not session or not is_web_egress_recording_status(
getattr(session, "web_egress_status", "")
):
return False
try:
client = WebEgressClient()
client.stop_recording(
{
"session_id": session.id,
"room_id": session.room_id,
"egress_id": session.web_egress_id,
}
)
except (ImproperlyConfigured, WebEgressError) as exc:
logger.warning(
"[WebEgress] Failed to stop active recording during session close - session_id=%s room_id=%s error=%s",
session.id,
session.room_id,
str(exc),
)
return False
session.web_egress_status = "stopping"
if not session.web_egress_stopped_at:
session.web_egress_stopped_at = timezone.now()
session.save(
update_fields=[
"web_egress_status",
"web_egress_stopped_at",
"updated_at",
]
)
else:
session.save(update_fields=["web_egress_status", "updated_at"])
logger.info(
"[WebEgress] Stop requested during session close - session_id=%s room_id=%s egress_id=%s",
session.id,
session.room_id,
session.web_egress_id,
)
return True

2
apps/course/views/admin.py

@ -11,6 +11,7 @@ from rest_framework.exceptions import PermissionDenied
from utils.pagination import StandardResultsSetPagination from utils.pagination import StandardResultsSetPagination
from apps.account.permissions import IsPanelUser, IsSuperAdminOrReadOnlyForProfessor from apps.account.permissions import IsPanelUser, IsSuperAdminOrReadOnlyForProfessor
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
from apps.course.services.web_egress import stop_session_web_egress_if_active
from apps.course.models import ( from apps.course.models import (
Course, Course,
CourseCategory, CourseCategory,
@ -465,6 +466,7 @@ class AdminLiveSessionViewSet(viewsets.ModelViewSet):
if self._is_room_still_active(session.room_id): if self._is_room_still_active(session.room_id):
continue continue
stop_session_web_egress_if_active(session)
now = timezone.now() now = timezone.now()
session.ended_at = now session.ended_at = now
session.save(update_fields=['ended_at', 'updated_at']) session.save(update_fields=['ended_at', 'updated_at'])

74
apps/course/views/course.py

@ -41,6 +41,7 @@ from apps.course.models import (
from apps.course.models.course import extract_text_from_json from apps.course.models.course import extract_text_from_json
from apps.course.doc import * from apps.course.doc import *
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
from apps.course.services.web_egress import stop_session_web_egress_if_active
from apps.account.serializers import UserProfileSerializer from apps.account.serializers import UserProfileSerializer
from utils.exceptions import AppAPIException from utils.exceptions import AppAPIException
from utils.redis import OnlineClassTokenManager from utils.redis import OnlineClassTokenManager
@ -909,9 +910,81 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
if not access_token: if not access_token:
return None return None
self._register_fail_safe_live_session_user(
course=course,
room_id=room_id,
user=user,
is_admin=is_admin,
)
frontend_base = get_online_class_frontend_base() frontend_base = get_online_class_frontend_base()
return f"{frontend_base}/?access_token={access_token}" return f"{frontend_base}/?access_token={access_token}"
@staticmethod
def _register_fail_safe_live_session_user(
course: Course,
room_id: str,
user,
is_admin: bool,
) -> None:
try:
session = (
CourseLiveSession.objects.filter(
course=course,
room_id=room_id,
ended_at__isnull=True,
)
.order_by('-started_at', '-id')
.first()
)
if not session:
logger.warning(
"[Online Validate] Fail-safe registration skipped - no active session found for course=%s room_id=%s user_id=%s",
course.slug,
room_id,
user.id,
)
return
role = 'moderator' if is_admin else 'participant'
LiveSessionUser.objects.update_or_create(
session=session,
user=user,
defaults={
'role': role,
'is_online': True,
'exited_at': None,
'entered_at': timezone.now(),
},
)
if is_admin:
session.last_moderator_left_at = None
session.auto_close_after_moderator_exit_at = None
session.save(
update_fields=[
'last_moderator_left_at',
'auto_close_after_moderator_exit_at',
'updated_at',
]
)
logger.info(
"[Online Validate] Fail-safe registered user entry - session_id=%s room_id=%s user_id=%s role=%s",
session.id,
room_id,
user.id,
role,
)
except Exception as exc:
logger.error(
"[Online Validate] Failed to register fail-safe user entry - course=%s room_id=%s user_id=%s error=%s",
course.slug,
room_id,
user.id,
str(exc),
)
def _generate_entry_token(self, course: Course, user) -> str: def _generate_entry_token(self, course: Course, user) -> str:
manager = OnlineClassTokenManager() manager = OnlineClassTokenManager()
user_token, _ = Token.objects.get_or_create(user=user) user_token, _ = Token.objects.get_or_create(user=user)
@ -1131,6 +1204,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
Close a live session and all related user entries. Close a live session and all related user entries.
Sets ended_at for session and exited_at/is_online for users. Sets ended_at for session and exited_at/is_online for users.
""" """
stop_session_web_egress_if_active(session)
now = timezone.now() now = timezone.now()
session.ended_at = now session.ended_at = now

38
apps/course/views/live_session.py

@ -15,9 +15,10 @@ from drf_yasg import openapi
import time import time
import jwt import jwt
from apps.course.models import Course, CourseLiveSession, Participant, LiveSessionRecording, LiveSessionUser from apps.course.models import Course, CourseLiveSession, Participant, LiveSessionRecording, LiveSessionUser
from apps.course.models.lesson import Lesson, CourseLesson
from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer, LiveSessionRecordedFileSerializer, LiveSessionRecordingSerializer from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer, LiveSessionRecordedFileSerializer, LiveSessionRecordingSerializer
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
from apps.course.services.web_egress import WebEgressClient, WebEgressError
from apps.course.services.web_egress import WebEgressClient, WebEgressError, stop_session_web_egress_if_active
from utils.exceptions import AppAPIException from utils.exceptions import AppAPIException
from django.conf import settings from django.conf import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -138,6 +139,38 @@ def build_web_egress_title(session: CourseLiveSession) -> str:
return session.recording_title or f"{session.subject} - Recording" return session.recording_title or f"{session.subject} - Recording"
def sync_session_recording_titles(session: CourseLiveSession) -> None:
base_title = session.recording_title or session.subject
recordings = list(
LiveSessionRecording.objects.filter(session=session).order_by(
"created_at",
"id",
)
)
total = len(recordings)
if not total:
return
for index, recording in enumerate(recordings, start=1):
title = base_title if total == 1 else f"{base_title} part {index}"
if recording.title != title:
recording.title = title
recording.save(update_fields=["title", "updated_at"])
if not recording.file:
continue
lesson = Lesson.objects.filter(content_file=recording.file.name).first()
if lesson and lesson.title != title:
lesson.title = title
lesson.save(update_fields=["title", "updated_at"])
CourseLesson.objects.filter(lesson=lesson).update(
title=title,
updated_at=timezone.now(),
)
class CourseLiveSessionRoomCreateAPIView(GenericAPIView): class CourseLiveSessionRoomCreateAPIView(GenericAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
authentication_classes = [TokenAuthentication] authentication_classes = [TokenAuthentication]
@ -647,6 +680,7 @@ class CourseLiveSessionTokenAPIView(GenericAPIView):
else: else:
logger.warning(f"[Room Verify] ✗ Room is NOT active - room_id={session.room_id} session_id={session.id} msg={response_msg}") logger.warning(f"[Room Verify] ✗ Room is NOT active - room_id={session.room_id} session_id={session.id} msg={response_msg}")
# Auto-close the session since room is not active # Auto-close the session since room is not active
stop_session_web_egress_if_active(session)
now = timezone.now() now = timezone.now()
session.ended_at = now session.ended_at = now
session.save(update_fields=['ended_at', 'updated_at']) session.save(update_fields=['ended_at', 'updated_at'])
@ -670,6 +704,7 @@ class CourseLiveSessionTokenAPIView(GenericAPIView):
# If room not found, close the session # If room not found, close the session
if 'not found' in error_msg.lower() or 'does not exist' in error_msg.lower(): if 'not found' in error_msg.lower() or 'does not exist' in error_msg.lower():
stop_session_web_egress_if_active(session)
now = timezone.now() now = timezone.now()
session.ended_at = now session.ended_at = now
session.save(update_fields=['ended_at', 'updated_at']) session.save(update_fields=['ended_at', 'updated_at'])
@ -1272,6 +1307,7 @@ class CourseLiveSessionSetRecordingTitleAPIView(GenericAPIView):
session.recording_title = title.strip() session.recording_title = title.strip()
session.save(update_fields=['recording_title', 'updated_at']) session.save(update_fields=['recording_title', 'updated_at'])
sync_session_recording_titles(session)
logger.info(f"[LiveSession Title] Set recording title for room_id={room_id} to '{title}'") logger.info(f"[LiveSession Title] Set recording title for room_id={room_id} to '{title}'")
return Response({'success': True, 'message': 'Recording title updated successfully.'}, status=status.HTTP_200_OK) return Response({'success': True, 'message': 'Recording title updated successfully.'}, status=status.HTTP_200_OK)

3
apps/course/views/webhook.py

@ -28,6 +28,7 @@ from drf_yasg import openapi
from apps.course.models import CourseLiveSession, LiveSessionUser, Course, Participant, LiveSessionRecording from apps.course.models import CourseLiveSession, LiveSessionUser, Course, Participant, LiveSessionRecording
from apps.account.models import User from apps.account.models import User
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
from apps.course.services.web_egress import stop_session_web_egress_if_active
from utils.exceptions import AppAPIException from utils.exceptions import AppAPIException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -208,13 +209,13 @@ class PlugNMeetWebhookAPIView(APIView):
try: try:
session = CourseLiveSession.objects.get(room_id=room_id, ended_at__isnull=True) session = CourseLiveSession.objects.get(room_id=room_id, ended_at__isnull=True)
stop_session_web_egress_if_active(session)
now = timezone.now() now = timezone.now()
session.ended_at = now session.ended_at = now
update_fields = ['ended_at', 'updated_at'] update_fields = ['ended_at', 'updated_at']
if session.web_egress_status in { if session.web_egress_status in {
CourseLiveSession.WEB_EGRESS_STATUS_STARTING, CourseLiveSession.WEB_EGRESS_STATUS_STARTING,
CourseLiveSession.WEB_EGRESS_STATUS_RECORDING, CourseLiveSession.WEB_EGRESS_STATUS_RECORDING,
CourseLiveSession.WEB_EGRESS_STATUS_STOPPING,
}: }:
session.web_egress_status = CourseLiveSession.WEB_EGRESS_STATUS_PROCESSING session.web_egress_status = CourseLiveSession.WEB_EGRESS_STATUS_PROCESSING
update_fields.append('web_egress_status') update_fields.append('web_egress_status')

Loading…
Cancel
Save