4 ans après s'être lancé dans une aventure passionnante de création de SaaS, c'est le bon moment pour reconstruire l'un des composants clés de notre application.
Éditeur vidéo simple pour les vidéos de réseaux sociaux écrites en JavaScript.
Voici la stack que j'ai décidé d'utiliser pour cette réécriture, qui est maintenant un travail en cours.
Étant donné que notre interface est écrite en SvelteKit, c'est la meilleure option pour notre cas d'utilisation.
L'éditeur vidéo est une bibliothèque npm privée distincte que je peux simplement ajouter à notre interface. Il s'agit d'une bibliothèque sans tête, donc l'interface utilisateur de l'éditeur vidéo est complètement isolée.
La bibliothèque de l'éditeur vidéo est responsable de la synchronisation des éléments vidéo et audio avec la chronologie, du rendu des animations et des transitions, du rendu des textes HTML dans le canevas, et bien plus encore.
SceneBuilderFactory prend un objet JSON de scène comme argument pour créer une scène. StateManager.svelte.ts conserve ensuite l'état actuel de l'éditeur vidéo en temps réel.
C'est très utile pour dessiner et mettre à jour la position de la tête de lecture dans la timeline, et bien plus encore.
Pixi.js est une bibliothèque de canevas JavaScript exceptionnelle.
Au départ, j'ai commencé à construire ce projet avec Pixi v8, mais pour certaines raisons que je mentionnerai plus loin dans cet article, j'ai décidé d'opter pour Pixi v7.
Cependant, la bibliothèque de l'éditeur vidéo n'est étroitement couplée à aucune dépendance, il est donc facile de les remplacer si nécessaire ou de tester différents outils.
Pour la gestion des timelines et des animations complexes, j'ai décidé d'utiliser GSAP.
À ma connaissance, il n'existe aucun autre outil dans l'écosystème JavaScript permettant de créer des chronologies imbriquées, des animations combinées ou des animations de texte complexes d'une manière aussi simple.
J'ai une licence commerciale GSAP, je peux donc également utiliser des outils supplémentaires pour simplifier davantage les choses.
Avant de plonger dans les éléments que j'utilise dans le backend, voyons quelques défis que vous devez résoudre lors de la création d'un éditeur vidéo en javascript.
Cette question est souvent posée sur le forum GSAP.
Peu importe si vous utilisez GSAP pour la gestion du calendrier ou non, ce que vous devez faire est plusieurs choses.
À chaque coche de rendu :
Obtenez le temps relatif de la vidéo par rapport à la chronologie. Supposons que la lecture de votre vidéo commence depuis le début, à 10 secondes de la chronologie.
Eh bien, avant 10 secondes, vous ne vous souciez pas de l'élément vidéo, mais dès qu'il entre dans la timeline, vous devez le garder synchronisé.
Vous pouvez le faire en calculant l'heure relative de la vidéo, qui doit être calculée à partir de l'heure actuelle de l'élément vidéo, comparée à l'heure actuelle de la scène et dans une période de « décalage » acceptable.
Si le décalage est supérieur à, disons, 0,3 seconde, vous devez rechercher automatiquement l'élément vidéo pour corriger sa synchronisation avec la chronologie principale. Cela s'applique également aux éléments audio.
Autres éléments à prendre en compte :
La lecture et la pause sont simples à mettre en œuvre. Pour la recherche, j'ajoute l'identifiant du composant de recherche vidéo dans notre svelte StateManager, qui changera automatiquement l'état en "chargement".
StateManager a une dépendance EventManager et à chaque changement d'état, il déclenche automatiquement un événement "changestate", afin que nous puissions écouter ces événements sans utiliser $effect.
La même chose se produit une fois la recherche terminée et la vidéo prête à être lue.
De cette façon, nous pouvons afficher un indicateur de chargement au lieu du bouton lecture/pause dans notre interface utilisateur lorsque certains composants sont en cours de chargement.
CSS, GSAP et TextSplitter de GSAP me permettent de faire des choses vraiment étonnantes avec des éléments de texte.
Les éléments de texte natifs du canevas sont limités et, comme le principal cas d'utilisation de notre application est de créer des vidéos courtes pour les réseaux sociaux, ils ne conviennent pas.
Heureusement, j'ai trouvé un moyen de restituer presque n'importe quel texte HTML dans un canevas, ce qui est crucial pour le rendu de la sortie vidéo.
Texte HTML Pixi
Cela aurait été la solution la plus simple ; malheureusement, cela n'a pas fonctionné pour moi.
Lorsque j'animais du texte HTML avec GSAP, il était considérablement en retard et il ne prenait pas non plus en charge de nombreuses polices Google que j'avais essayées avec.
Satori
Satori est incroyable, et je peux imaginer qu'il soit utilisé dans des cas d'utilisation plus simples. Malheureusement, certaines animations GSAP changent de style qui ne sont pas compatibles avec Satori, ce qui entraîne une erreur.
SVG avec objet étranger
Enfin, j'ai créé une solution personnalisée pour résoudre ce problème.
La partie la plus délicate était la prise en charge des emojis et des polices personnalisées, mais j'ai réussi à résoudre ce problème.
J'ai créé une classe SVGGenerator qui a une méthode generateSVG, qui produit un SVG comme celui-ci :
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
Le styleTag ressemble alors à ceci :
<style>@font-face { font-family: ${fontFamilyName}; src: url('${fontData}') }</style>
Pour que cela fonctionne, le code HTML que nous transmettons doit avoir la famille de polices correcte définie dans le style en ligne. Les données de police doivent être une chaîne de données codées en base64, quelque chose comme data:font/ttf;base64,longboringstring
Composition sur héritage, dit-on.
Pour me salir les mains, j'ai refactorisé une approche basée sur l'héritage vers un système basé sur les hooks.
Dans mon éditeur vidéo, j'appelle des éléments comme VIDÉO, AUDIO, TEXTE, SOUS-TITRES, IMAGE, FORME, etc. composants.
Avant de réécrire ceci, il existait une classe abstraite BaseComponent, et chaque classe de composant l'étendait, donc VideoComponent avait une logique pour les vidéos, etc.
Le problème était que c'est devenu un désastre assez rapidement.
Les composants étaient responsables de la façon dont ils sont rendus, de la façon dont ils gèrent leur texture Pixi, de la façon dont ils sont animés, et plus encore.
Maintenant, il n'y a qu'une seule classe de composants, ce qui est très simple.
Ceci comporte désormais quatre événements de cycle de vie :
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
Cette classe de composant a une méthode appelée addHook qui modifie son comportement.
Les hooks peuvent se connecter aux événements du cycle de vie des composants et effectuer des actions.
Par exemple, il existe un MediaHook que j'utilise pour les composants vidéo et audio.
MediaHook crée l'élément audio ou vidéo sous-jacent et le maintient automatiquement synchronisé avec la chronologie principale.
Pour les composants de construction, j'ai utilisé le modèle de constructeur avec le modèle de directeur (voir référence).
De cette façon, lors de la création d'un composant audio, j'y ajoute MediaHook, que j'ajoute également aux composants vidéo. Cependant, les vidéos ont également besoin de crochets supplémentaires pour :
Cette approche permet de changer, d'étendre ou de modifier très facilement la logique de rendu ou le comportement des composants dans la scène.
J'ai essayé plusieurs approches différentes pour rendre des vidéos de la manière la plus rapide et la plus rentable.
En 2020, j'ai commencé avec l'approche la plus simple : le rendu d'une image après l'autre, ce que font de nombreux outils.
Après quelques essais et erreurs, je suis passé à une approche de calques de rendu.
Cela signifie que notre document SceneData contient des calques qui contiennent des composants.
Chacune de ces couches est rendue séparément puis combinée avec ffmpeg pour créer la sortie finale.
La limitation était qu'un calque ne peut contenir que des composants du même type.
Par exemple, un calque avec vidéo ne peut pas contenir d'éléments de texte ; il ne peut contenir que d'autres vidéos.
Cela présente évidemment des avantages et des inconvénients.
Il était assez simple de restituer indépendamment des textes HTML avec des animations sur Lambda et de les transformer en vidéos transparentes, qui étaient ensuite combinées avec d'autres morceaux pour le résultat final.
D'un autre côté, les calques contenant des composants vidéo ont été simplement traités avec ffmpeg.
Cependant, cette approche présentait un énorme inconvénient.
Si je voulais implémenter un système d'images clés pour redimensionner, atténuer ou faire pivoter la vidéo, je devrais créer des ports de ces fonctionnalités dans fluent-ffmpeg.
C'est tout à fait possible, mais avec toutes les autres responsabilités que j'ai, je n'y suis tout simplement pas parvenu.
J'ai donc décidé de revenir à la première approche : le rendu d'une image après l'autre.
Les demandes de rendu sont envoyées au serveur backend avec Express.
Cette route vérifie si la vidéo n'est pas encore rendue, et sinon, elle est ajoutée à la file d'attente BullMQ.
Une fois que la file d'attente commence à traiter le rendu, elle génère plusieurs instances de Chrome sans tête.
Remarque : ce traitement s'effectue sur un serveur Hetzner dédié avec un processeur AMD EPYC 7502P 32 cœurs et 128 Go de RAM, c'est donc une machine assez performante.
Gardez à l'esprit que Chromium n'a pas de codecs, j'utilise donc Playwright, ce qui rend l'installation de Chrome triviale.
Mais quand même, les images vidéo sont sorties noires pour une raison quelconque.
Je suis sûr qu'il me manquait juste quelque chose ; cependant, j'ai décidé de diviser les composants vidéo en images individuelles et de les utiliser dans le navigateur sans serveur au lieu d'utiliser des vidéos.
Mais quand même, le plus important était d'éviter d'utiliser la méthode de capture d'écran.
Puisque nous avons tout dans un seul canevas, nous pouvons le mettre dans une image avec .getDataURL() sur le canevas, ce qui est beaucoup plus rapide.
Pour simplifier cela, j'ai créé une page statique qui regroupe l'éditeur vidéo et ajoute quelques fonctions dans la fenêtre.
Ceci est ensuite chargé avec Playwright/Puppeteer, et sur chaque image, j'appelle simplement :
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
Cela me donne les données d'image que je peux soit enregistrer en tant qu'image, soit ajouter dans un tampon pour restituer le morceau vidéo.
L'ensemble de ce processus est divisé en 5 à 10 travailleurs différents, en fonction de la durée de la vidéo, qui sont fusionnés dans le résultat final.
Au lieu de cela, il peut également être déchargé sur quelque chose comme Lambda, mais j'ai tendance à utiliser RunPod. Le seul inconvénient de leur architecture sans serveur est qu'ils utilisent Python, que je ne connais pas très bien.
De cette façon, le rendu peut être divisé en plusieurs morceaux traités sur le cloud, et même le rendu d'une vidéo de 60 minutes peut être effectué en une minute ou deux. C'est bien de l'avoir, mais ce n'est pas notre objectif principal ni notre cas d'utilisation.
La raison pour laquelle je suis passé de Pixi 8 à Pixi 7 est que Pixi 7 possède également la version « héritée » qui prend en charge le canevas 2D. C'est BEAUCOUP plus rapide pour le rendu. Le rendu d'une vidéo de 60 secondes prend environ 80 secondes sur le serveur, mais si le canevas a un contexte WebGL ou WebGPU, je n'ai pu restituer que 1 à 2 images par seconde.
Chose intéressante, selon mes tests, Chrome sans serveur était beaucoup plus lent que Firefox, lors du rendu des canevas WebGL.
Même l'utilisation d'un GPU dédié n'a pas permis d'accélérer le rendu de manière significative. Soit je faisais quelque chose de mal, soit tout simplement Chrome sans tête n'est pas très performant avec WebGL.
WebGL dans notre cas d'utilisation est idéal pour les transitions, qui sont généralement assez courtes.
L'une des façons que je prévois de tester à ce sujet est de restituer séparément les morceaux WebGL et non-WebGL.
De nombreuses parties sont impliquées dans le projet.
Les données de scène sont stockées sur MongoDB, car il est plus logique que la structure des documents soit stockée dans une base de données sans schéma.
Le frontend, écrit en SvelteKit, utilise urql comme client GraphQL.
Le serveur GraphQL utilise PHP Laravel avec MongoDB et l'incroyable Lighthouse GraphQL.
Mais c'est peut-être un thème pour la prochaine fois.
Donc c'est tout pour l'instant ! Il y a beaucoup de travail à faire avant de mettre cela en production et de remplacer l'éditeur vidéo actuel, qui est assez buggé et me rappelle un peu Frankenstein.
Dites-moi ce que vous pensez et continuez à rocker !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!