Repository: openfun/marsha

Issue:
Too short harvested video can fail
## Bug Report

**Expected behavior/code**

No matter the record length, the harvesting should not fail

**Actual Behavior**

Some short recording slice, less then 4 seconds most of the time, are not harvested and the harvest job fails.

**Steps to Reproduce**
1. Configure and start a webinar
2. Start recording and stop it immediately.
3. Stop the webinar
4. Convert it in VOD



Discussion:
The harvest job service is a black box. No information is provided when a job fails, why it fails, etc. 

I think about this solution to avoid failing job:

- do not authorize recording length lower than a segment length. The segment length is known and defined in the settings: `LIVE_SEGMENT_DURATION_SECONDS`
- To not authorize, the record button should be disabled while the recording length is lower than the segment length. So we have to expose the segment length to the front application.
- The API should reject the stop recording action if the recording length is lower than the segment length.

Morevoer, we have to define a strategy when a recording slice fails. Should we fail all ? Should we harvest all other slices ? etc.

Resolving patch:
diff --git a/src/backend/marsha/core/api/video.py b/src/backend/marsha/core/api/video.py
--- a/src/backend/marsha/core/api/video.py
+++ b/src/backend/marsha/core/api/video.py
@@ -21,6 +21,7 @@
 from marsha.websocket.utils import channel_layers_utils
 
 from .. import defaults, forms, permissions, serializers, storage
+from ..metadata import VideoMetadata
 from ..models import LivePairing, LiveSession, SharedLiveMedia, Video
 from ..services.video_participants import (
     VideoParticipantsException,
@@ -60,6 +61,7 @@ class VideoViewSet(APIViewMixin, ObjectPkMixin, viewsets.ModelViewSet):
     queryset = Video.objects.all()
     serializer_class = serializers.VideoSerializer
     permission_classes = [permissions.NotAllowed]
+    metadata_class = VideoMetadata
 
     def get_permissions(self):
         """
diff --git a/src/backend/marsha/core/metadata.py b/src/backend/marsha/core/metadata.py
new file mode 100644
--- /dev/null
+++ b/src/backend/marsha/core/metadata.py
@@ -0,0 +1,17 @@
+"""This module holds custome metadata class dedicated to Marsha."""
+from django.conf import settings
+
+from rest_framework.metadata import SimpleMetadata
+
+
+class VideoMetadata(SimpleMetadata):
+    """Metadata class dedicated to video model."""
+
+    def determine_metadata(self, request, view):
+        """Adds live info in video metadata."""
+        metadata = super().determine_metadata(request, view)
+        metadata["live"] = {
+            "segment_duration_seconds": settings.LIVE_SEGMENT_DURATION_SECONDS
+        }
+
+        return metadata
diff --git a/src/backend/marsha/core/services/video_recording.py b/src/backend/marsha/core/services/video_recording.py
--- a/src/backend/marsha/core/services/video_recording.py
+++ b/src/backend/marsha/core/services/video_recording.py
@@ -1,8 +1,9 @@
 """Services for video recording."""
+from django.conf import settings
 from django.utils import timezone
 
 from ..defaults import PENDING, RUNNING
-from ..utils.time_utils import to_timestamp
+from ..utils.time_utils import to_datetime, to_timestamp
 
 
 class VideoRecordingError(Exception):
@@ -32,6 +33,10 @@ def stop_recording(video):
         raise VideoRecordingError("Video recording is not started.")
 
     slices = video.recording_slices
-    slices[-1].update({"stop": to_timestamp(timezone.now()), "status": PENDING})
+    now = timezone.now()
+    diff = now - to_datetime(slices[-1].get("start"))
+    if diff.total_seconds() < settings.LIVE_SEGMENT_DURATION_SECONDS:
+        raise VideoRecordingError("Segment not long enough.")
+    slices[-1].update({"stop": to_timestamp(now), "status": PENDING})
     video.recording_slices = slices
     video.save()


Test patch:
diff --git a/src/backend/marsha/core/tests/test_api_video.py b/src/backend/marsha/core/tests/test_api_video.py
--- a/src/backend/marsha/core/tests/test_api_video.py
+++ b/src/backend/marsha/core/tests/test_api_video.py
@@ -6628,6 +6628,7 @@ def test_api_video_update_live_already_saved_request_id(self):
 
         self.assertEqual(response.status_code, 200)
 
+    @override_settings(LIVE_SEGMENT_DURATION_SECONDS=4)
     def test_api_video_options_as_student(self):
         """A student can fetch the video options endpoint"""
 
@@ -6665,7 +6666,9 @@ def test_api_video_options_as_student(self):
                 {"value": "NO_CC", "display_name": "All rights reserved"},
             ],
         )
+        self.assertEqual(content["live"]["segment_duration_seconds"], 4)
 
+    @override_settings(LIVE_SEGMENT_DURATION_SECONDS=4)
     def test_api_video_options_as_instructor(self):
         """An instructor can fetch the video options endpoint"""
 
@@ -6707,6 +6710,7 @@ def test_api_video_options_as_instructor(self):
                 {"value": "NO_CC", "display_name": "All rights reserved"},
             ],
         )
+        self.assertEqual(content["live"]["segment_duration_seconds"], 4)
 
     def test_api_video_options_anonymous(self):
         """Anonymous user can't fetch the video options endpoint"""
diff --git a/src/backend/marsha/core/tests/test_services_video_recording.py b/src/backend/marsha/core/tests/test_services_video_recording.py
--- a/src/backend/marsha/core/tests/test_services_video_recording.py
+++ b/src/backend/marsha/core/tests/test_services_video_recording.py
@@ -3,7 +3,7 @@
 import random
 from unittest import mock
 
-from django.test import TestCase
+from django.test import TestCase, override_settings
 from django.utils import timezone
 
 from ..defaults import HARVESTING, JITSI, LIVE_CHOICES, PENDING, RUNNING
@@ -181,3 +181,19 @@ def test_services_video_recording_stop_recording_already_started_existing_slices
                 },
             ],
         )
+
+    @override_settings(LIVE_SEGMENT_DURATION_SECONDS=10)
+    def test_services_recording_stop_recording_segment_too_short(self):
+        """When start slice is lower than LIVE_SEGMENT_DURATION_SECONDS stop_recording should
+        fails."""
+        start = timezone.now() - timedelta(seconds=1)
+        video = VideoFactory(
+            recording_slices=[
+                {"start": to_timestamp(start)},
+            ],
+        )
+
+        with self.assertRaises(VideoRecordingError) as context:
+            stop_recording(video)
+
+        self.assertEqual(str(context.exception), "Segment not long enough.")