Vidéo — détails techniques
Description et fonctionnalités de ce module : Vidéo.
Module fr.tech.openent~video (dépôt open-ent/video),
backend Vert.x + frontend React sur le socle Explorer (@open-ent/explorer, @open-ent/client),
comme Carte mentale ou Communautés. L'encodage vidéo est délégué à un service dédié dockerisé
(media-server/, Bun + ffmpeg), pas exécuté dans la JVM du module.
Architecture
Fiche vidéo (CRUD explorateur)
- Création :
POST /video(VideoController#create) — comme les autres applications explorateur, la modale générique ne crée qu'une fiche titre + description, sans fichier.DefaultVideoService#createdélègue l'indexation àVideoExplorerPlugin(extends ExplorerPluginResourceMongo, collection Mongovideos). - Attacher un fichier : l'écran de fiche (
features/Video/Video.tsx) affiche un formulaire d'envoi tant queworkspaceDocumentIdest vide.POST /video/encode?videoid=<id>— même route que l'éditeur riche (ci-dessous), avec en plus le paramètrevideoidpointant la fiche à compléter. Côté backend,VideoController#runEncodingPipelinebascule survideoService.update(...)(au lieu decreate(...)) quandvideoidest présent, pour éviter de créer un doublon. DefaultVideoService#updaterelit le document Mongo complet après écriture avant de le réindexer (plugin.notifyUpsert) : passer directement le payload partiel de la requête écraserait les champs non modifiés (ex.name) dans l'index de recherche Explorer.- Renommage :
PUT /video/:videoIdavec le seul champname. Deux points d'entrée « sur place » (features/VideoTitle/VideoTitle.tsx, monté par la fiche et par le studio) s'appuient sur cette route générique, exactement comme la modale de l'Explorer qui passe, elle, parVideoResourceService#update. L'update Mongo étant un$setchamp par champ, n'envoyer quenamelaisse intacts la miniature, le montage et les métadonnées d'encodage. Le droit exigé estvideo.manager(ActionType.RESOURCE) : le frontend n'affiche le crayon que si le loader deroutes/video-roota posémanager: truedans le storestore/rights. La décision appliquée au brouillon de titre (vide → refus, inchangé après rognage → aucune écriture) est isolée dansfeatures/VideoTitle/rename.tset couverte par un test unitaire Vitest (rename.test.ts). - Dossiers, déplacement, corbeille, suppression : entièrement pris en charge par le module
explorergénérique (FoldersControllerProxy→FoldersControllerExplorer), aucune route spécifique côté vidéo.
Contrat d'encodage (partagé avec l'éditeur riche)
POST /video/encode?captation=&duration=&videoid=(multipart) — contrat inchangé consommé aussi parMediaLibrary.tsx(bibliothèqueopenent-frontend-framework, picker vidéo de l'éditeur de texte enrichi) :videoidest omis dans ce cas, une nouvelle fiche est créée après encodage.GET /video/status/:id— statut du job (running/succeed/error), suivi en mémoire parVideoJobStore(non partagé entre instances — limite connue en cas de déploiement multi-instance).VideoEncoderClient(mirroir deorg.entcore.common.pdf.NodePdfClient) relaie le fichier brut au media-server (video-encoder.urldans la config du module) et récupère le mp4 encodé (h264/aac/faststart) ; le fichier encodé est ensuite stocké viaStorage#writeBufferdans la même collection Mongo (documents) que l'espace documentaire, pour rester lisible parGET /workspace/document/:id.- Le droit
fr.tech.openent.video.controllers.VideoController|capture(méthode nomméecapture, pas un nom métier) est en dur dansMediaLibrary.tsx— package et nom de méthode ne peuvent pas être renommés sans casser l'intégration éditeur riche.
Capture navigateur (webcam/écran)
features/VideoCapture/useMediaCapture.ts: trois modes (camera/screen/screen-camera), viagetUserMedia/getDisplayMedia+MediaRecorder. En mode composite, deux<video>hors écran alimentent un<canvas>composé (composite.ts) qui incruste la caméra dans un coin de l'écran ;canvas.captureStream()fournit la piste vidéo enregistrée. Plusieurs pistes audio (micro + audio système) sont mixées viaAudioContextquand les deux sont présentes.- Le blob enregistré (webm,
getBestSupportedMimeType()) est envoyé au même contrat que l'upload —POST /video/encode?captation=true&duration=&videoid=— aveccaptation=true:VideoController#hasAllowedExtensionest sauté (le conteneur webm deMediaRecorderne fait pas partie deaccept-videoupload-extensions), et c'estmax-videoduration-minutesqui borne la taille à la place (arrêt automatique de l'enregistrement à la durée max). - Tests e2e : la webcam/le partage d'écran n'existant pas sur le runner headless,
48_video/shared.ts#installFakeMediaCaptureremplacegetUserMedia/getDisplayMediapar un flux synthétique (canvas.captureStream()+ oscillateurAudioContext) viapage.addInitScript, posé avant toute navigation —MediaRecorder, l'encodage et l'upload restent le vrai pipeline.
Media-server (media-server/)
Conteneur autonome (Bun + ffmpeg), sur le modèle du pdf (node-pdf-generator) : pas de route
Traefik (appel serveur-à-serveur interne au réseau docker), image publiée séparément
(ghcr.io/open-ent/video-media-server). POST /encode (multipart) → mp4 ; GET /health.
Studio d'édition (trim, zoom, arrière-plan, sous-titres, export)
Conçu en s'inspirant du modèle de données de CapSoftware/Cap
(crates/project/src/configuration.rs : BackgroundSource, ZoomSegment, CaptionSegment) — le
moteur de rendu de Cap est en Rust + wgpu (GPU natif), inutilisable tel quel sur cette stack
Bun/ffmpeg ; seul le modèle de données a été repris, le rendu est entièrement réimplémenté en
filtres ffmpeg.
- Montage (EDL) : stocké dans le champ
editde la fiche Mongo (trim,zoom,background,captions), écrit via lePUT /video/:videoIdgénérique existant (pas de route dédiée à la sauvegarde du brouillon —features/VideoEditor/VideoEditor.tsxl'autosave, débouncé 1,2 s). Modèle miroir côté frontend (models/edit.ts) et media-server (lib/media-edit.ts,EditProject) — zoom et sous-titres sont horodatés en temps SOURCE (avant découpe), projetés vers le temps de sortie à travers les plages de trim conservées au moment du rendu (mapSourceSegmentsToOutput) et de la génération du VTT (CaptionsVttBuilder, même logique dupliquée côté Java pour la piste non gravée) — évite de tout retimer à chaque ajustement du trim. - Rendu ffmpeg (
media-server/src/lib/media-edit.ts, routePOST /video/render, même contrat de job asynchrone que/video/process— job créé dansjob-manager.ts, statut/résultat/ annulation réutilisés tels quels) : découpe le clip à chaque frontière de segment de zoom (buildRenderSegments), applique un filtrezoompanavec une expressionif()en escalier (montée/palier/descente) sur les segments zoomés, concatène (mêmes fonctions batch que l'existantrenderEditedVideo) ; passe éventuelle de composition du fond (pad/gblur+overlay/image importée, dégradé généré via le filtregeq) ; passe éventuelle de gravure des sous-titres via un fichier.assstylé (subtitles=). - Backend :
VideoController#renderEdit(POST /:videoId/edit/render) lit la fiche, résout le fichier source, démarre le job viaVideoEncoderClient#startRenderJob(même polling que l'encodage initial), puis remplaceworkspaceDocumentId/thumbnail/sizeBytesde la fiche à la complétion — réutilise le mécanisme de remplacement déjà validé pourvideoidsur/encode, pas de logique parallèle.POST /:videoId/edit/background-imagehéberge une image de fond importée dans le workspace.GET /:videoId/captions.vttsert la piste de sous-titres non destructive.
workspaceDocumentId ≠ id de stockageLe champ workspaceDocumentId d'une fiche est l'_id Mongo du document workspace
(collection documents), pas l'id que Storage#readFile attend — celui-ci vit dans le champ
file du même document (voir org.entcore.workspace.controllers.WorkspaceController#getPreview,
qui fait la même résolution). Appeler storage.readFile(workspaceDocumentId, …) directement ne
lève aucune erreur (Storage#readFile n'a pas de canal d'échec, il rappelle simplement le
handler avec un Buffer null) — le bug est passé inaperçu jusqu'à un NullPointerException non
rattrapé plus loin dans le pipeline (job de rendu bloqué indéfiniment en running, aucun appel à
jobStore.fail(...)). VideoController#resolveStorageFileId fait la résolution documents._id →
documents.file avant tout storage.readFile, avec échec propre du job si le document ou le
fichier est introuvable.
Paramètres (bloc du module dans ent-core.yaml)
| Paramètre | Rôle |
|---|---|
video-encoder.url | URL du media-server (VIDEO_ENCODER_URL) |
publicConf.max-videosize-mbytes | Taille max acceptée à l'envoi (défaut 50 Mo) |
publicConf.max-videoduration-minutes | Durée max pour une captation webcam (défaut 3 min) |
publicConf.accept-videoupload-extensions | Extensions autorisées pour un envoi de fichier (défaut mp4, mov, avi) |
Le contrôle porte uniquement sur l'extension du fichier (VideoController#hasAllowedExtension),
pas sur le Content-Type HTTP envoyé par le navigateur. Quel que soit le format d'origine accepté,
le media-server ré-encode systématiquement en MP4 (H.264/AAC, +faststart) avant stockage : un MOV
n'est donc jamais lu tel quel par le <video> du frontend.
Limites connues
- Formulaire d'envoi non stylé (
features/Video/Video.tsxen l'absence de fichier) : input natif + bouton brut, pas encore repris dans la charte graphique du module. - Espace de noms
explorersans traduction : les libellés génériques de la bibliothèque@open-ent/explorer(ex. l'icône vide d'illustration) retombent sur leur valeur par défaut faute de ressource i18n dédiée — gap partagé par toutes les applications explorateur, pas spécifique à Vidéo. VideoJobStoreen mémoire : le suivi des jobs d'encodage ne survit pas à un redémarrage et n'est pas partagé entre instances.- Aucun déploiement production pour le module ni le media-server : pas de chart Helm référençant
video(constaté à date, à faire sur le modèle du servicepdf/content-transformer). - Régénération de la miniature après export parfois en échec (
POST /video/thumbnaildu media-server retourne 500 sur certains clips très courts, constaté le 2026-09-01) : dégradation gracieuse — le job de rendu réussit quand même, la fiche garde simplement sa miniature précédente plutôt que d'être mise à jour. Cause racine non creusée (probablement une combinaison timestamp/durée limite pour un clip d'~1 s). - Sous-titres auto-générés (transcription) : hors périmètre du studio d'édition actuel, ajout manuel uniquement (voir plus haut) — pas de moteur de reconnaissance vocale embarqué.
Couverture de tests
10 test(s) e2e couvrent ce module, dans 9 scénario(s) — dossier
apps/open-ent-e2e/src/modules/48_video. S'y ajoute un test unitaire Vitest côté module
(frontend/src/features/VideoTitle/rename.test.ts, pnpm test).
10 test(s) e2e dans 9 scénario(s) — voir le détail
Vidéo 01_acces_au_module.spec.ts
- accès au module
Vidéo 02_ajout_video.spec.ts
- création d'une fiche puis envoi du fichier (jusqu'à la lecture)
Vidéo 03_organisation_dossiers.spec.ts
- création d'un dossier et déplacement d'une vidéo
Vidéo 04_suppression.spec.ts
- mise à la corbeille puis suppression définitive
Vidéo — capture 05_capture_webcam.spec.ts
- enregistrer une vidéo depuis la caméra (choix de source, enregistrement, relecture, envoi jusqu'à la lecture)
Vidéo — capture 06_capture_ecran.spec.ts
- enregistrer une vidéo depuis le partage d'écran
Vidéo — capture 07_capture_ecran_camera.spec.ts
- enregistrer une vidéo depuis l'écran et la caméra (bulle caméra incrustée)
Vidéo — studio d'édition 08_studio_edition.spec.ts
- découpe, zoom, arrière-plan, sous-titre puis export (jusqu'au remplacement du fichier sur la fiche)
Vidéo — renommage du titre 09_renommage_titre.spec.ts
- renomme depuis la fiche puis depuis le studio (refus du titre vide, validation au clavier, annulation sans écriture, propagation à la bibliothèque)
Les captures de la fiche fonctionnelle proviennent de ces spécifications, rejouées avec le profil enseignant et un fichier vidéo de test généré par ffmpeg (1 s, 160×90) — ou, pour la capture navigateur, un flux synthétique (voir « Capture navigateur » ci-dessus).
Le défaut
lilit.upreti001du profilenseignantne se connecte pas sur l'environnement local : y rejouer les specs demandeE2E_ENSEIGNANT_USER=amelie.martin E2E_ENSEIGNANT_PASS=amelie.martin(persona de démo, seul compte enseignant dont le groupe porte les rôles de l'application Vidéo en local). Les captures du renommage ont été produites ainsi.
Conformité
Évaluation au référentiel Open ENT NG (module video).
| Maillon de la chaîne qualité | Référence |
|---|---|
| 🎯 Fonctionnalités attendues | fiche fonctionnelle |
| 🧪 Tests réalisés | dossier e2e 48_video |