Heim > Artikel > Web-Frontend > Erstellen eines TypeScript-Video-Editors als Solo-Entwickler
Vier Jahre nach Beginn einer aufregenden Reise zum SaaS-Aufbau ist es der richtige Zeitpunkt, eine der Schlüsselkomponenten unserer App neu zu erstellen.
Einfacher Videoeditor für Social-Media-Videos, geschrieben in JavaScript.
Hier ist der Stapel, den ich für diese Umschreibung verwendet habe, die derzeit in Arbeit ist.
Da unser Frontend in SvelteKit geschrieben ist, ist dies die beste Option für unseren Anwendungsfall.
Der Video-Editor ist eine separate private NPM-Bibliothek, die ich einfach zu unserem Frontend hinzufügen kann. Da es sich um eine Headless-Bibliothek handelt, ist die Benutzeroberfläche des Videoeditors vollständig isoliert.
Die Video-Editor-Bibliothek ist für die Synchronisierung der Video- und Audioelemente mit der Zeitleiste, das Rendern von Animationen und Übergängen, das Rendern von HTML-Texten in die Leinwand und vieles mehr verantwortlich.
SceneBuilderFactory verwendet ein Szenen-JSON-Objekt als Argument zum Erstellen einer Szene. StateManager.svelte.ts behält dann den aktuellen Status des Videoeditors in Echtzeit bei.
Dies ist sehr nützlich zum Zeichnen und Aktualisieren der Abspielkopfposition in der Timeline und vielem mehr.
Pixi.js ist eine herausragende JavaScript-Canvas-Bibliothek.
Zuerst habe ich begonnen, dieses Projekt mit Pixi v8 zu erstellen, aber aus einigen Gründen, die ich später in diesem Artikel erwähnen werde, habe ich mich für Pixi v7 entschieden.
Die Video-Editor-Bibliothek ist jedoch nicht eng an irgendwelche Abhängigkeiten gekoppelt, sodass sie bei Bedarf einfach ersetzt oder verschiedene Tools getestet werden können.
Für die Zeitleistenverwaltung und komplexe Animationen habe ich mich für GSAP entschieden.
Mir ist kein anderes Tool im JavaScript-Ökosystem bekannt, mit dem sich verschachtelte Zeitleisten, kombinierte Animationen oder komplexe Textanimationen auf so einfache Weise erstellen lassen.
Ich habe eine GSAP-Geschäftslizenz, sodass ich auch zusätzliche Tools nutzen kann, um die Dinge noch einfacher zu machen.
Bevor wir uns mit den Dingen befassen, die ich im Backend verwende, sehen wir uns einige Herausforderungen an, die Sie beim Erstellen eines Video-Editors in Javascript lösen müssen.
Diese Frage wird oft im GSAP-Forum gestellt.
Es spielt keine Rolle, ob Sie GSAP für die Zeitleistenverwaltung verwenden oder nicht, Sie müssen ein paar Dinge tun.
Bei jedem Render-Tick:
Erhalten Sie das Video relativ zur Timeline. Nehmen wir an, Ihr Video beginnt bei der 10-Sekunden-Marke der Timeline von vorne abzuspielen.
Nun, vor 10 Sekunden ist Ihnen das Videoelement eigentlich egal, aber sobald es in die Timeline gelangt, müssen Sie es synchron halten.
Sie können dies tun, indem Sie die relative Zeit des Videos berechnen, die aus der aktuellen Zeit des Videoelements berechnet werden muss, verglichen mit der aktuellen Szenenzeit und innerhalb eines akzeptablen „Verzögerungszeitraums“.
Wenn die Verzögerung größer als beispielsweise 0,3 Sekunden ist, müssen Sie das Videoelement automatisch suchen, um seine Synchronisierung mit der Hauptzeitleiste zu korrigieren. Dies gilt auch für Audioelemente.
Weitere Dinge, die Sie beachten müssen:
Play und Pause sind einfach zu implementieren. Für die Suche füge ich die Videosuchkomponenten-ID in unseren schlanken StateManager ein, der den Status automatisch in „Laden“ ändert.
StateManager hat eine EventManager-Abhängigkeit und löst bei jeder Statusänderung automatisch ein „Changestate“-Ereignis aus, sodass wir diese Ereignisse abhören können, ohne $effect zu verwenden.
Das Gleiche passiert, nachdem die Suche abgeschlossen ist und das Video abspielbereit ist.
Auf diese Weise können wir in unserer Benutzeroberfläche eine Ladeanzeige anstelle der Wiedergabe-/Pause-Schaltfläche anzeigen, wenn einige der Komponenten geladen werden.
CSS, GSAP und GSAPs TextSplitter ermöglichen es mir, wirklich erstaunliche Dinge mit Textelementen zu machen.
Native Canvas-Textelemente sind begrenzt, und da der Hauptanwendungsfall unserer App darin besteht, Kurzvideos für soziale Medien zu erstellen, sind sie nicht gut geeignet.
Glücklicherweise habe ich eine Möglichkeit gefunden, fast jeden HTML-Text in Canvas zu rendern, was für die Wiedergabe der Videoausgabe von entscheidender Bedeutung ist.
Pixi HTMLText
Das wäre die einfachste Lösung gewesen; Leider hat es bei mir nicht funktioniert.
Als ich HTML-Text mit GSAP animierte, kam es zu erheblichen Verzögerungen und viele Google-Schriftarten, die ich damit ausprobiert habe, wurden auch nicht unterstützt.
Satori
Satori ist großartig und ich kann mir vorstellen, dass es in einigen einfacheren Anwendungsfällen verwendet wird. Leider ändern einige GSAP-Animationen Stile, die nicht mit Satori kompatibel sind, was zu einem Fehler führt.
SVG mit Fremdkörper
Schließlich habe ich eine maßgeschneiderte Lösung entwickelt, um dieses Problem zu lösen.
Der schwierige Teil war die Unterstützung von Emojis und benutzerdefinierten Schriftarten, aber ich habe es geschafft, das zu lösen.
Ich habe eine SVGGenerator-Klasse erstellt, die über eine Methode „generateSVG“ verfügt, die ein SVG wie dieses erzeugt:
<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>
Das styleTag sieht dann so aus:
<style>@font-face { font-family: ${fontFamilyName}; src: url('${fontData}') }</style>
Damit dies funktioniert, muss für den von uns übergebenen HTML-Code die richtige Schriftfamilie im Inline-Stil festgelegt sein. Schriftartdaten müssen eine Base64-codierte Datenzeichenfolge sein, etwa data:font/ttf;base64,longboringstring
Zusammensetzung geht vor Vererbung, heißt es.
Um mir die Hände schmutzig zu machen, habe ich von einem vererbungsbasierten Ansatz zu einem Hook-basierten System umgestaltet.
In meinem Videoeditor nenne ich Elemente wie VIDEO, AUDIO, TEXT, UNTERTITEL, BILD, FORM usw. Komponenten.
Vor dem Umschreiben gab es eine abstrakte Klasse BaseComponent, die von jeder Komponentenklasse erweitert wurde, sodass VideoComponent über Logik für Videos usw. verfügte.
Das Problem war, dass es ziemlich schnell zu einem Chaos wurde.
Die Komponenten waren dafür verantwortlich, wie sie gerendert werden, wie sie ihre Pixi-Textur verwalten, wie sie animiert werden und mehr.
Jetzt gibt es nur noch eine Komponentenklasse, was sehr einfach ist.
Dies hat jetzt vier Lebenszyklusereignisse:
<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>
Diese Komponentenklasse verfügt über eine Methode namens addHook, die ihr Verhalten ändert.
Hooks können sich in Komponentenlebenszyklusereignisse einklinken und Aktionen ausführen.
Zum Beispiel gibt es einen MediaHook, den ich für Video- und Audiokomponenten verwende.
MediaHook erstellt das zugrunde liegende Audio- oder Videoelement und hält es automatisch synchron mit der Hauptzeitleiste.
Zum Erstellen von Komponenten habe ich das Builder-Muster zusammen mit dem Director-Muster verwendet (siehe Referenz).
Auf diese Weise füge ich beim Erstellen einer Audiokomponente MediaHook hinzu, den ich auch zu Videokomponenten hinzufüge. Allerdings benötigen Videos auch zusätzliche Hooks für:
Dieser Ansatz macht es sehr einfach, die Rendering-Logik oder das Verhalten der Komponenten in der Szene zu ändern, zu erweitern oder zu modifizieren.
Ich habe mehrere verschiedene Ansätze ausprobiert, um Videos am schnellsten und kosteneffizientesten zu rendern.
Im Jahr 2020 begann ich mit dem einfachsten Ansatz – dem Rendern eines Frames nach dem anderen, was viele Tools tun.
Nach einigem Ausprobieren wechselte ich zu einem Rendering-Layer-Ansatz.
Das bedeutet, dass unser SceneData-Dokument Ebenen enthält, die Komponenten enthalten.
Jede dieser Ebenen wird separat gerendert und dann mit ffmpeg kombiniert, um die endgültige Ausgabe zu erstellen.
Die Einschränkung bestand darin, dass eine Ebene nur Komponenten desselben Typs enthalten kann.
Zum Beispiel kann eine Ebene mit Video keine Textelemente enthalten; Es kann nur andere Videos enthalten.
Das hat natürlich einige Vor- und Nachteile.
Es war ganz einfach, HTML-Texte mit Animationen auf Lambda unabhängig zu rendern und in transparente Videos umzuwandeln, die dann mit anderen Chunks für die endgültige Ausgabe kombiniert wurden.
Ebenen mit Videokomponenten wurden dagegen einfach mit ffmpeg verarbeitet.
Dieser Ansatz hatte jedoch einen großen Nachteil.
Wenn ich ein Keyframe-System zum Skalieren, Ausblenden oder Drehen des Videos implementieren wollte, müsste ich Ports dieser Funktionen in fluent-ffmpeg erstellen.
Das ist durchaus möglich, aber bei all den anderen Aufgaben, die ich habe, habe ich es einfach nicht geschafft.
Also habe ich beschlossen, zum ersten Ansatz zurückzukehren – einen Frame nach dem anderen zu rendern.
Rendering-Anfragen werden mit Express an den Backend-Server gesendet.
Diese Route prüft, ob das Video noch nicht gerendert wird, und wenn nicht, wird es zur BullMQ-Warteschlange hinzugefügt.
Nachdem die Warteschlange mit der Verarbeitung des Renderings begonnen hat, werden mehrere Instanzen von Headless Chrome erzeugt.
Hinweis: Diese Verarbeitung erfolgt auf einem dedizierten Hetzner-Server mit AMD EPYC 7502P 32-Core-Prozessor und 128 GB RAM, es handelt sich also um eine ziemlich leistungsstarke Maschine.
Denken Sie daran, dass Chromium keine Codecs hat, daher verwende ich Playwright, was die Installation von Chrome einfach macht.
Aber trotzdem waren die Videobilder aus irgendeinem Grund schwarz.
Ich bin mir sicher, dass mir einfach etwas entgangen ist; Ich habe mich jedoch entschieden, die Videokomponenten in einzelne Bildrahmen aufzuteilen und diese im serverlosen Browser zu verwenden, anstatt Videos zu verwenden.
Aber das Wichtigste war dennoch, die Screenshot-Methode zu vermeiden.
Da wir alles auf einer Leinwand haben, können wir es mit .getDataURL() auf der Leinwand in ein Bild umwandeln, was viel schneller geht.
Um dies einfacher zu machen, habe ich eine statische Seite erstellt, die den Video-Editor bündelt und einige Funktionen in das Fenster einfügt.
Das wird dann mit Playwright/Puppeteer geladen und bei jedem Frame rufe ich einfach auf:
<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>
Dadurch erhalte ich die Frame-Daten, die ich entweder als Bild speichern oder in einen Puffer hinzufügen kann, um den Videoblock zu rendern.
Dieser gesamte Prozess ist je nach Videolänge in 5–10 verschiedene Worker aufgeteilt, die in der endgültigen Ausgabe zusammengeführt werden.
Stattdessen kann es auch auf etwas wie Lambda verlagert werden, aber ich tendiere eher zur Verwendung von RunPod. Der einzige Nachteil ihrer serverlosen Architektur ist die Verwendung von Python, mit dem ich nicht so vertraut bin.
Auf diese Weise kann das Rendering in mehrere Teile aufgeteilt werden, die in der Cloud verarbeitet werden, und sogar das Rendern eines 60-minütigen Videos kann in ein oder zwei Minuten erfolgen. Schön zu haben, aber das ist nicht unser primäres Ziel oder Anwendungsfall.
Der Grund, warum ich ein Downgrade von Pixi 8 auf Pixi 7 durchgeführt habe, ist, dass Pixi 7 auch über die „Legacy“-Version verfügt, die 2D-Canvas unterstützt. Dies ist VIEL schneller beim Rendern. Das Rendern eines 60-Sekunden-Videos auf dem Server dauert etwa 80 Sekunden, aber wenn die Leinwand WebGL- oder WebGPU-Kontext hat, konnte ich nur 1-2 Bilder pro Sekunde rendern.
Interessanterweise war serverloses Chrome laut meinen Tests beim Rendern von WebGL-Canvases viel langsamer als kopflastiges Firefox.
Selbst die Verwendung einer dedizierten GPU hat nicht dazu beigetragen, das Rendering wesentlich zu beschleunigen. Entweder habe ich etwas falsch gemacht, oder Headless Chrome ist mit WebGL einfach nicht sehr leistungsfähig.
WebGL eignet sich in unserem Anwendungsfall hervorragend für Übergänge, die normalerweise recht kurz sind.
Eine Möglichkeit, dies zu testen, besteht darin, WebGL- und Nicht-WebGL-Blöcke getrennt zu rendern.
An dem Projekt sind viele Teile beteiligt.
Szenendaten werden auf MongoDB gespeichert, da die Struktur der Dokumente am sinnvollsten in einer schemalosen Datenbank gespeichert werden sollte.
Das in SvelteKit geschriebene Frontend verwendet urql als GraphQL-Client.
Der GraphQL-Server verwendet PHP Laravel mit MongoDB und das erstaunliche Lighthouse GraphQL.
Aber das ist vielleicht ein Thema für das nächste Mal.
Das war's also vorerst! Es muss noch viel Arbeit geleistet werden, bevor dies in Produktion geht und der aktuelle Videoeditor ersetzt wird, der ziemlich fehlerhaft ist und mich ein wenig an Frankenstein erinnert.
Lassen Sie mich wissen, was Sie denken und rocken Sie weiter!
Das obige ist der detaillierte Inhalt vonErstellen eines TypeScript-Video-Editors als Solo-Entwickler. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!