Skip to content

Commit ee5850a

Browse files
committed
✨(back) add celery task to copy BBB video recordings
To replace AWS lambda for video conversion, adding a Celery task that fetches the videos from the Scalelite API, and uploads them to Scaleway S3. This allow the Peertube pipeline to be used instead of the AWS-based pipeline.
1 parent 299d536 commit ee5850a

File tree

11 files changed

+522
-46
lines changed

11 files changed

+522
-46
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ Versioning](https://semver.org/spec/v2.0.0.html).
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- Add Celery task to fetch recording from Scalelite API
14+
1115
### Fixed
1216

13-
- Restore download, transcription and shared media icons on the video player
17+
- Restore download, transcription and shared media icons on the video player
1418

1519
## [5.6.0] - 2025-03-06
1620

src/aws/lambda-convert/testfiles/bbb-video-template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ <h2 class="visually-hidden">Chat Messages</h2>
4141

4242
<footer>Recorded with <a href="http://bigbluebutton.org">BigBlueButton</a>.</footer>
4343
</body>
44-
</html>
44+
</html>

src/backend/marsha/bbb/api.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.utils import timezone
1111
from django.utils.translation import gettext_lazy as _
1212

13+
from celery import chain
1314
import django_filters
1415
import jwt
1516
from rest_framework import filters, mixins, viewsets
@@ -38,7 +39,8 @@
3839
from marsha.core.api import APIViewMixin, BulkDestroyModelMixin, ObjectPkMixin
3940
from marsha.core.defaults import VOD_CONVERT
4041
from marsha.core.models import ADMINISTRATOR, INSTRUCTOR, Video
41-
from marsha.core.utils.convert_lambda_utils import invoke_lambda_convert
42+
from marsha.core.tasks.recording import copy_video_recording
43+
from marsha.core.tasks.video import launch_video_transcoding
4244
from marsha.core.utils.s3_utils import create_presigned_post
4345
from marsha.core.utils.time_utils import to_timestamp
4446

@@ -763,7 +765,7 @@ def create_vod(self, request, pk=None, classroom_id=None):
763765
classroom_recording.vod = Video.objects.create(
764766
title=request.data.get("title"),
765767
playlist=classroom_recording.classroom.playlist,
766-
transcode_pipeline=defaults.AWS_PIPELINE,
768+
transcode_pipeline=defaults.PEERTUBE_PIPELINE,
767769
)
768770
classroom_recording.save()
769771

@@ -774,12 +776,26 @@ def create_vod(self, request, pk=None, classroom_id=None):
774776
now = timezone.now()
775777
stamp = to_timestamp(now)
776778

777-
# we need an used url to convert the record in VOD
778-
invoke_lambda_convert(
779-
get_recording_url(record_id=classroom_recording.record_id),
780-
classroom_recording.vod.get_source_s3_key(stamp=stamp),
779+
if settings.TRANSCODING_CALLBACK_DOMAIN:
780+
domain = settings.TRANSCODING_CALLBACK_DOMAIN
781+
else:
782+
domain = f"{request.scheme}://{request.get_host()}"
783+
784+
process_chain = chain(
785+
copy_video_recording.si(
786+
record_url=get_recording_url(record_id=classroom_recording.record_id),
787+
video_pk=classroom_recording.vod.pk,
788+
stamp=stamp,
789+
),
790+
launch_video_transcoding.si(
791+
video_pk=classroom_recording.vod.pk,
792+
stamp=stamp,
793+
domain=domain,
794+
),
781795
)
782796

797+
process_chain.delay()
798+
783799
return Response(serializer.data, status=201)
784800

785801
def destroy(self, request, *args, **kwargs):

src/backend/marsha/bbb/tests/api/classroom/recordings/test_create_vod.py

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
UserAccessTokenFactory,
1818
)
1919
from marsha.core.tests.testing_utils import reload_urlconf
20-
from marsha.core.utils.time_utils import to_timestamp
2120

2221

2322
# We don't enforce arguments documentation in tests
@@ -46,7 +45,7 @@ def test_api_classroom_recording_create_anonymous(self):
4645
"""An anonymous should not be able to convert a recording to a VOD."""
4746
recording = ClassroomRecordingFactory()
4847

49-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
48+
with mock.patch("marsha.bbb.api.chain"):
5049
response = self.client.post(
5150
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
5251
)
@@ -63,7 +62,7 @@ def test_api_classroom_recording_create_anonymous_unknown_recording(self):
6362
"""An anonymous should not be able to convert an unknown recording to a VOD."""
6463
recording = ClassroomRecordingFactory()
6564

66-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
65+
with mock.patch("marsha.bbb.api.chain"):
6766
response = self.client.post(
6867
f"/api/classrooms/{recording.classroom.id}"
6968
f"/recordings/{recording.classroom.id}/create-vod/",
@@ -82,7 +81,7 @@ def test_api_classroom_recording_create_vod_student(self):
8281
recording = ClassroomRecordingFactory()
8382
jwt_token = StudentLtiTokenFactory(playlist=recording.classroom.playlist)
8483

85-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
84+
with mock.patch("marsha.bbb.api.chain"):
8685
response = self.client.post(
8786
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
8887
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
@@ -163,12 +162,14 @@ def test_api_classroom_recording_create_vod_instructor_or_admin(self):
163162
status=200,
164163
)
165164

166-
with mock.patch(
167-
"marsha.bbb.api.invoke_lambda_convert"
168-
) as mock_invoke_lambda_convert, mock.patch.object(
169-
timezone, "now", return_value=now
170-
), self.assertNumQueries(
171-
9
165+
with (
166+
mock.patch("marsha.bbb.api.chain") as mock_chain,
167+
mock.patch("marsha.bbb.api.copy_video_recording.si") as mock_copy,
168+
mock.patch(
169+
"marsha.bbb.api.launch_video_transcoding.si"
170+
) as mock_transcoding,
171+
mock.patch.object(timezone, "now", return_value=now),
172+
self.assertNumQueries(9),
172173
):
173174
response = self.client.post(
174175
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
@@ -177,7 +178,7 @@ def test_api_classroom_recording_create_vod_instructor_or_admin(self):
177178
)
178179

179180
self.assertEqual(Video.objects.count(), 1)
180-
self.assertEqual(Video.objects.first().transcode_pipeline, "AWS")
181+
self.assertEqual(Video.objects.first().transcode_pipeline, "peertube")
181182
self.assertEqual(response.status_code, 201)
182183

183184
recording.refresh_from_db()
@@ -202,13 +203,19 @@ def test_api_classroom_recording_create_vod_instructor_or_admin(self):
202203

203204
self.assertEqual(Video.objects.first(), recording.vod)
204205
self.assertEqual(recording.vod.upload_state, PENDING)
205-
mock_invoke_lambda_convert.assert_called_once_with(
206-
(
206+
stamp = str(int(now.timestamp()))
207+
mock_copy.assert_called_once_with(
208+
record_url=(
207209
"https://10.7.7.1/presentation/"
208210
"c62c9c205d37815befe1b75ae6ef5878d8da5bb6-1673282694493/meeting.mp4"
209211
),
210-
recording.vod.get_source_s3_key(stamp=to_timestamp(now)),
212+
video_pk=recording.vod.pk,
213+
stamp=stamp,
211214
)
215+
mock_transcoding.assert_called_once_with(
216+
video_pk=recording.vod.pk, stamp=stamp, domain="http://testserver"
217+
)
218+
mock_chain.assert_called_once_with(mock_copy(), mock_transcoding())
212219

213220
def test_api_classroom_recording_create_vod_instructor_or_admin_unknown_recording(
214221
self,
@@ -225,9 +232,10 @@ def test_api_classroom_recording_create_vod_instructor_or_admin_unknown_recordin
225232

226233
now = timezone.now()
227234

228-
with mock.patch.object(
229-
timezone, "now", return_value=now
230-
), self.assertNumQueries(1):
235+
with (
236+
mock.patch.object(timezone, "now", return_value=now),
237+
self.assertNumQueries(1),
238+
):
231239
response = self.client.post(
232240
f"/api/classrooms/{recording.classroom.id}"
233241
f"/recordings/{recording.classroom.id}/create-vod/",
@@ -251,7 +259,7 @@ def test_api_classroom_recording_create_vod_user_access_token(self):
251259
)
252260
jwt_token = UserAccessTokenFactory(user=organization_access.user)
253261

254-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
262+
with mock.patch("marsha.bbb.api.chain"):
255263
response = self.client.post(
256264
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
257265
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
@@ -323,15 +331,15 @@ def test_api_classroom_recording_create_vod_user_access_token_organization_admin
323331
status=200,
324332
)
325333

326-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
334+
with mock.patch("marsha.bbb.api.chain"):
327335
response = self.client.post(
328336
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
329337
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
330338
)
331339

332340
self.assertEqual(response.status_code, 201)
333341
self.assertEqual(Video.objects.count(), 1)
334-
self.assertEqual(Video.objects.first().transcode_pipeline, "AWS")
342+
self.assertEqual(Video.objects.first().transcode_pipeline, "peertube")
335343

336344
@responses.activate
337345
def test_api_classroom_recording_create_vod_from_standalone_site_no_consumer_site(
@@ -402,15 +410,15 @@ def test_api_classroom_recording_create_vod_from_standalone_site_no_consumer_sit
402410
status=200,
403411
)
404412

405-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
413+
with mock.patch("marsha.bbb.api.chain"):
406414
response = self.client.post(
407415
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
408416
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
409417
)
410418

411419
self.assertEqual(response.status_code, 201)
412420
self.assertEqual(Video.objects.count(), 1)
413-
self.assertEqual(Video.objects.first().transcode_pipeline, "AWS")
421+
self.assertEqual(Video.objects.first().transcode_pipeline, "peertube")
414422

415423
def test_api_classroom_recording_create_vod_from_standalone_site_inactive_conversion(
416424
self,
@@ -430,7 +438,7 @@ def test_api_classroom_recording_create_vod_from_standalone_site_inactive_conver
430438
)
431439
jwt_token = UserAccessTokenFactory(user=organization_access.user)
432440

433-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
441+
with mock.patch("marsha.bbb.api.chain"):
434442
response = self.client.post(
435443
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
436444
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
@@ -502,15 +510,15 @@ def test_api_classroom_recording_create_vod_user_access_token_playlist_admin(
502510
status=200,
503511
)
504512

505-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
513+
with mock.patch("marsha.bbb.api.chain"):
506514
response = self.client.post(
507515
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
508516
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
509517
)
510518

511519
self.assertEqual(response.status_code, 201)
512520
self.assertEqual(Video.objects.count(), 1)
513-
self.assertEqual(Video.objects.first().transcode_pipeline, "AWS")
521+
self.assertEqual(Video.objects.first().transcode_pipeline, "peertube")
514522

515523
@responses.activate
516524
def test_api_classroom_recording_create_vod_user_access_token_playlist_instructor(
@@ -576,15 +584,15 @@ def test_api_classroom_recording_create_vod_user_access_token_playlist_instructo
576584
status=200,
577585
)
578586

579-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
587+
with mock.patch("marsha.bbb.api.chain"):
580588
response = self.client.post(
581589
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
582590
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
583591
)
584592

585593
self.assertEqual(response.status_code, 201)
586594
self.assertEqual(Video.objects.count(), 1)
587-
self.assertEqual(Video.objects.first().transcode_pipeline, "AWS")
595+
self.assertEqual(Video.objects.first().transcode_pipeline, "peertube")
588596

589597
def test_api_classroom_recording_create_vod_user_access_token_playlist_student(
590598
self,
@@ -617,7 +625,7 @@ def test_api_classroom_recording_create_vod_user_access_token_other_playlist_adm
617625
classroom_other = ClassroomFactory(playlist=playlist_access.playlist)
618626
jwt_token = UserAccessTokenFactory(user=playlist_access.user)
619627

620-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
628+
with mock.patch("marsha.bbb.api.chain"):
621629
response = self.client.post(
622630
f"/api/classrooms/{classroom_other.id}/recordings/{recording.id}/create-vod/",
623631
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
@@ -639,7 +647,7 @@ def test_api_classroom_recording_create_vod_admin_other_playlist_access(
639647
)
640648
jwt_token = UserAccessTokenFactory(user=other_playlist_access.user)
641649

642-
with mock.patch("marsha.bbb.api.invoke_lambda_convert"):
650+
with mock.patch("marsha.bbb.api.chain"):
643651
response = self.client.post(
644652
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
645653
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
@@ -666,12 +674,10 @@ def test_api_classroom_recording_create_vod_instructor_or_admin_inactive_convers
666674

667675
now = timezone.now()
668676

669-
with mock.patch(
670-
"marsha.bbb.api.invoke_lambda_convert"
671-
) as mock_invoke_lambda_convert, mock.patch.object(
672-
timezone, "now", return_value=now
673-
), self.assertNumQueries(
674-
1
677+
with (
678+
mock.patch("marsha.bbb.api.chain") as mock_chain,
679+
mock.patch.object(timezone, "now", return_value=now),
680+
self.assertNumQueries(1),
675681
):
676682
response = self.client.post(
677683
f"/api/classrooms/{recording.classroom.id}/recordings/{recording.id}/create-vod/",
@@ -688,4 +694,4 @@ def test_api_classroom_recording_create_vod_instructor_or_admin_inactive_convers
688694
{"error": "VOD conversion is disabled."},
689695
)
690696
self.assertEqual(Video.objects.count(), 0)
691-
mock_invoke_lambda_convert.assert_not_called()
697+
mock_chain.assert_not_called()

src/backend/marsha/core/defaults.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,12 @@
7373

7474
# UPLOAD ERROR REASONS
7575
MAX_RESOLUTION_EXCEDEED = "max_resolution_excedeed"
76+
RECORDING_SOURCE_ERROR = "recording_source_error"
7677

77-
UPLOAD_ERROR_REASON_CHOICES = ((MAX_RESOLUTION_EXCEDEED, _("max_resolution_excedeed")),)
78+
UPLOAD_ERROR_REASON_CHOICES = (
79+
(MAX_RESOLUTION_EXCEDEED, _("max_resolution_excedeed")),
80+
(RECORDING_SOURCE_ERROR, _("recording_source_error")),
81+
)
7882

7983
# LIVE TYPE
8084
(RAW, JITSI) = ("raw", "jitsi")

0 commit comments

Comments
 (0)