首頁  >  文章  >  web前端  >  作為獨立開發者建立 TypeScript 影片編輯器

作為獨立開發者建立 TypeScript 影片編輯器

DDD
DDD原創
2024-11-06 08:09:02584瀏覽

踏上令人興奮的 SaaS 構建之旅 4 年後,現在是重建我們應用程式的關鍵組件之一的最佳時機。

用 JavaScript 編寫的社群媒體影片的簡單影片編輯器。

這是我決定用於此重寫的堆棧,目前正在進行中。

苗條5

由於我們的前端是用 SvelteKit 編寫的,因此這是我們用例的最佳選擇。

影片編輯器是一個單獨的私人 npm 庫,我可以簡單地將其添加到我們的前端。它是一個無頭庫,因此視訊編輯器 UI 是完全隔離的。

影片編輯器庫負責將影片和音訊元素與時間軸同步、渲染動畫和過渡、將 HTML 文字渲染到畫布中等等。

SceneBuilderFactory 接受場景 JSON 物件作為參數來建立場景。 StateManager.svelte.ts 然後即時保持影片編輯器的當前狀態。

這對於在時間軸中繪製和更新播放頭位置等等非常有用。

Pixi.js

Pixi.js 是一個出色的 JavaScript 畫布函式庫。

最初,我開始使用 Pixi v8 來建造這個項目,但由於本文後面提到的一些原因,我決定使用 Pixi v7。

但是,影片編輯器庫並未與任何依賴項緊密耦合,因此可以根據需要輕鬆替換它們或測試不同的工具。

總體規劃計劃

對於時間軸管理和複雜的動畫,我決定使用 GSAP。

據我所知,JavaScript 生態系統中沒有其他工具可以以如此簡單的方式建立嵌套時間軸、組合動畫或複雜的文字動畫。

我擁有 GSAP 營業執照,因此我還可以利用其他工具來使更多事情變得簡單。

主要挑戰

在深入研究我在後端使用的東西之前,讓我們看看在使用 javascript 建立影片編輯器時需要解決的一些挑戰。

將視訊/音訊與時間軸同步

這個問題常在 GSAP 論壇中被問到。

是否使用GSAP進行時間軸管理並不重要,您需要做的只是幾件事。

在每個渲染刻度上:

取得影片相對於時間軸的時間。假設您的影片從時間軸的 10 秒標記處開始播放。

嗯,10秒之前你其實並不關心視訊元素,但一旦進入時間線,就需要保持同步。

您可以透過計算影片的相對時間來做到這一點,該時間必須根據視訊元素的 currentTime 計算,與當前場景時間進行比較,並在可接受的「滯後」週期內。

如果延遲大於(比方說)0.3 秒,您需要自動尋找視訊元素以修復其與主時間軸的同步。這也適用於音訊元素。

您需要考慮的其他事項:

  • 處理播放/暫停/結束狀態
  • 處理尋找

播放和暫停實作起來很簡單。為了進行查找,我將影片查找元件 id 新增到我們的 svelte StateManager 中,這會自動將狀態變更為「正在載入」。

StateManager 具有 EventManager 依賴性,並且在每次狀態變更時,它會自動觸發「changestate」事件,因此我們可以在不使用 $effect 的情況下監聽這些事件。

搜尋完成且影片準備好播放後,也會發生同樣的事情。

這樣,當某些元件正在載入時,我們可以在 UI 中顯示載入指示器,而不是播放/暫停按鈕。

文字渲染並不像你想像的那麼簡單

CSS、GSAP 和 GSAP 的 TextSplitter 讓我可以用文字元素做一些非常令人驚奇的事情。

原生畫布文字元素有限,並且由於我們應用程式的主要用例是為社交媒體創建短片視頻,因此它們不太適合。

幸運的是,我找到了一種將幾乎所有 HTML 文字渲染到畫布中的方法,這對於渲染影片輸出至關重要。

Pixi HTMLText

這將是最簡單的解決方案;不幸的是,它對我不起作用。

當我使用 GSAP 對 HTML 文字進行動畫處理時,它明顯滯後,而且它也不支援我嘗試使用的許多 Google 字體。

Satori 太棒了,我可以想像它被用在一些更簡單的用例中。不幸的是,一些 GSAP 動畫更改了與 Satori 不相容的樣式,從而導致錯誤。

帶有異物的 SVG

最後,我制定了一個自訂解決方案來解決這個問題。

棘手的部分是支援表情符號和自訂字體,但我設法解決了這個問題。

我建立了一個 SVGGenerator 類,它有一個generateSVG 方法,它產生一個如下所示的 SVG:

<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>

styleTag 看起來像這樣:

<style>@font-face { font-family: ${fontFamilyName}; src: url('${fontData}') }</style>

為此,我們傳入的 HTML 需要在內聯樣式中設定正確的字型系列。字體資料需要是 Base64 編碼的資料字串,例如 data:font/ttf;base64,longboringstring

3. 組件生命週期

他們說,組合優於繼承。

作為一項實踐練習,我從基於繼承的方法重構為基於鉤子的系統。

在我的影片編輯器中,我將影片、音訊、文字、字幕、圖像、形狀等元素稱為組件。

在重寫之前,有一個抽象類別BaseComponent,每個元件類別都擴展它,因此VideoComponent具有視訊等邏輯。

問題是它很快就變得一團糟。

元件負責它們的渲染方式、管理 Pixi 紋理的方式、動畫的方式等等。

現在只有一個組件類,非常簡單。

現在有四個生命週期事件:

<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>

Building a TypeScript Video Editor as a Solo Dev

該元件類別有一個名為 addHook 的方法,可以更改其行為。

掛鉤可以掛鉤組件生命週期事件並執行操作。

例如,我有一個用於視訊和音訊組件的 MediaHook。

MediaHook 建立底層音訊或視訊元素並自動使其與主時間軸保持同步。

對於建置元件,我使用了建構器模式和主管模式(請參閱參考資料)。

這樣,在建立音訊組件時,我將 MediaHook 添加到其中,並將其添加到視訊組件中。然而,影片還需要額外的掛鉤:

  • 創建紋理
  • 設定精靈
  • 在場景中設定正確的位置
  • 處理渲染

這種方法使得更改、擴展或修改渲染邏輯或元件在場景中的行為方式變得非常容易。

後端和渲染

我嘗試了多種不同的方法來以最快且最具成本效益的方式渲染影片。

2020 年,我從最簡單的方法開始——一幀又一幀地渲染,這是很多工具都會做的事情。

經過一些嘗試和錯誤,我改用渲染層方法。

這表示我們的 SceneData 文件包含包含元件的圖層。

每個圖層都單獨渲染,然後與 ffmpeg 組合以建立最終輸出。

限制是一個層只能包含相同類型的元件。

例如,有影片的圖層不能包含文字元素;它只能包含其他影片。

這顯然有一些優點和缺點。

在 Lambda 上獨立渲染帶有動畫的 HTML 文字並將其轉換為透明視頻,然後與其他區塊組合以獲得最終輸出非常簡單。

另一方面,具有視訊組件的圖層只需使用 ffmpeg 進行處理。

但是,這種方法有一個巨大的缺點。

如果我想實現一個關鍵幀系統來縮放、淡入淡出或旋轉視頻,我需要在 Fluent-ffmpeg 中移植這些功能。

這絕對是可能的,但由於我還有其他所有責任,我根本沒能做到。

所以我決定回到第一個方法 - 渲染一幀又一幀。

Express 和 BullMQ

渲染請求透過 Express 傳送到後端伺服器。

此路由檢查影片是否尚未渲染,如果沒有,則將其新增至 BullMQ 佇列。

劇作家/木偶師

佇列開始處理渲染後,它會產生多個 Headless Chrome 實例。

注意:此處理發生在配備 AMD EPYC 7502P 32 核心處理器和 128 GB RAM 的專用 Hetzner 伺服器上,因此它是一台性能相當出色的機器。

請記住,Chromium 沒有編解碼器,因此我使用 Playwright,這使得安裝 Chrome 變得很簡單。

但是,由於某種原因,視訊畫面還是變黑了。

我確信我只是錯過了一些東西;但是,我決定將視訊組件拆分為單獨的圖像幀,並在無伺服器瀏覽器中使用它們,而不是使用視訊。

但是,最重要的是避免使用截圖方法。

由於我們將所有內容放在一個畫布上,因此我們可以在畫布上使用 .getDataURL() 將其獲取到圖像中,這要快得多。

為了讓這個更簡單,我製作了一個靜態頁面,其中捆綁了影片編輯器並向視窗添加了一些功能。

然後使用 Playwright/Puppeteer 加載,在每一幀上,我只需調用:

<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>

這為我提供了幀數據,我可以將其保存為圖像或添加到緩衝區中以渲染視頻塊。

整個過程根據視訊長度分為 5-10 個不同的工作人員,並合併到最終輸出中。

除此之外,它也可以卸載到 Lambda 之類的東西,但我傾向於使用 RunPod。他們的無伺服器架構的唯一缺點是他們使用Python,我不太熟悉。

這樣,渲染可能會被分割成多個區塊在雲端處理,甚至60分鐘影片的渲染也可以在一兩分鐘內完成。很高興擁有,但這不是我們的主要目標或用例。

我還沒解決的​​問題

我從 Pixi 8 降級到 Pixi 7 的原因是因為 Pixi 7 也有支援 2D 畫布的「舊版」版本。這對於渲染來說要快得多。 60 秒的影片在伺服器上渲染大約需要 80 秒,但如果畫布具有 WebGL 或 WebGPU 上下文,我每秒只能渲染 1-2 幀。

有趣的是,根據我的測試,在渲染 WebGL 畫布時,無伺服器 Chrome 比 headful Firefox 慢得多。

即使使用專用 GPU 也無法顯著加快渲染速度。要嘛是我做錯了什麼,要嘛就是無頭 Chrome 在 WebGL 上的效能不太好。

在我們的用例中,WebGL 非常適合過渡,通常很短。

我計劃對此進行測試的方法之一是分別渲染 WebGL 和非 WebGL 區塊。

其他組件

專案涉及很多部分。

場景資料儲存在 MongoDB 上,因為文件的結構儲存在無模式資料庫中最有意義。

前端使用 SvelteKit 編寫,使用 urql 作為 GraphQL 用戶端。

GraphQL 伺服器使用 PHP Laravel 和 MongoDB 以及令人驚嘆的 Lighthouse GraphQL。

但這也許是下一次的主題。

總結

現在就是這樣!在將其投入生產並替換當前的影片編輯器之前,還有很多工作需要完成,目前的影片編輯器有很多問題,讓我想起了弗蘭肯斯坦。

讓我知道你的想法並繼續搖滾!

以上是作為獨立開發者建立 TypeScript 影片編輯器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn