diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index fa662946..da91be31 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -3,6 +3,7 @@ import uuid from django.conf import settings +from django.core.exceptions import PermissionDenied from django.db.models import Q from django.http import Http404 from django.shortcuts import get_object_or_404 @@ -213,6 +214,34 @@ class RoomViewSet( return drf_response.Response(data) + @decorators.action( + detail=True, + methods=["get"], + url_name="start_recording", + url_path="start-recording", + # permission_classes=[permissions.IsAuthenticated], + ) + def start_room_recording(self, request, pk=None): + """Start room recording.""" + room = self.get_object() + if not room.is_administrator(request.user) and not room.is_owner(request.user): + raise PermissionDenied( + "You must be an admin or owner to start a recording." + ) + + # TODO - check if no active recording is already present, we limit room recording to one + + try: + response = utils.start_egress(room) + except (RuntimeError, ValueError) as e: + return drf_response.Response({"error": str(e)}) + + # TODO - persist response + + return drf_response.Response( + {"message": f"Recording started for room {room.slug}", "egress": response} + ) + def list(self, request, *args, **kwargs): """Limit listed rooms to the ones related to the authenticated user.""" user = self.request.user diff --git a/src/backend/core/utils.py b/src/backend/core/utils.py index 304b5de7..1fc73aee 100644 --- a/src/backend/core/utils.py +++ b/src/backend/core/utils.py @@ -3,6 +3,7 @@ Utils functions used in the core app """ # ruff: noqa:S311 +# pylint: disable=no-member import hashlib import json @@ -12,7 +13,11 @@ from uuid import uuid4 from django.conf import settings -from livekit.api import AccessToken, VideoGrants +import aiohttp +from asgiref.sync import async_to_sync +from livekit.api import AccessToken, TwirpError, VideoGrants +from livekit.api.egress_service import EgressService +from livekit.protocol import egress as proto_egress def generate_color(identity: str) -> str: @@ -81,3 +86,44 @@ def generate_token(room: str, user, username: Optional[str] = None) -> str: ) return token.to_jwt() + + +@async_to_sync +async def start_egress(room): + """Start room recording using LiveKit's Egress service.""" + + if not room or not room.id: + raise ValueError("Invalid room object") + + # FIXME - Temporary, liveKit removes dashes in room's name + room_name = str(room.id).replace("-", "") + + try: + async with aiohttp.ClientSession() as session: + egress_service = EgressService( + session=session, **settings.LIVEKIT_CONFIGURATION + ) + + # TODO - discuss whether we organize recordings in subfolder + response = await egress_service.start_room_composite_egress( + start=proto_egress.RoomCompositeEgressRequest( + room_name=room_name, + file_outputs=[ + proto_egress.EncodedFileOutput( + file_type=proto_egress.EncodedFileType.MP4, + filepath="{room_name}{time}.mp4", + ) + ], + ) + ) + + return { + "egress_id": response.egress_id, + "filename": response.file.filename, + } + + except TwirpError as e: + # TODO - log error + raise RuntimeError( + "An error occurred while starting the room recording." + ) from e diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index c71c5e78..5efc131f 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -58,6 +58,7 @@ dependencies = [ "whitenoise==6.7.0", "mozilla-django-oidc==4.0.1", "livekit-api==0.7.0", + "aiohttp==3.10.10", ] [project.urls]