首页  >  文章  >  web前端  >  作为独立开发者构建 TypeScript 视频编辑器

作为独立开发者构建 TypeScript 视频编辑器

DDD
DDD原创
2024-11-06 08:09:02588浏览

踏上令人兴奋的 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