可记录的帆布Web组件
#javascript #教程 #Web组件 #webcodecs

我很奇怪吗?我在工作中使用了<canvas>标签。无论是绘图2D图形还是使用Three.jsBabylon的成熟3D引擎。

无论您使用帆布,您都可以逃脱普通的文档对象模型并手动渲染像素(或者如果要去3D,则三角形)。

使用帆布可能很有趣。您可以绘制和画画。否则可能会令人发指,尤其是当您的着色器代码无法编译时。不相信我吗? Check out my latest attempt试图让Chatgpt为我制作着着色器。实际上,我当时有chatgpt(无论是成功还是失败),我会用几个用于各种视频介绍的着色器。

但是,当您有工作时,结果可能会令人信服!无论您是完美地计划每个像素,还是创造了一些快乐的事故,这些事故变得精美。

显然,如果您想分享您的工作,您将分享一个网页,对吗?

不再了!我想向您介绍recordable-canvas Web组件。要安装,简单:

npm i recordable-canvas

我在repo中有很多文档和一些代码示例,因此我不会在这里使用它。尽管组件的简单非常简单,但它可能非常有趣,尤其是考虑到它使用了全新的[Web Roctecs api(https://developer.mozilla.org/en us/docs/web/api/webcodecs_api)来创建视频。

包裹画布

在开始进行视频编码之前,让我们快速谈论该组件如何处理画布。确实,Web组件只是包装器。我们将普通的canvas元素放在Web组件中。真可惜!我记得在网络组件的早期,我们希望扩展您的浏览器中的任何元素。想在视频元素中添加额外功能吗?只需说class MyComponent extends HTMLVideoElement

可悲的是事实并非如此。今天,我们只能扩展像普通<div>HTMLElement。但这没关系,我们只包装<canvas>标签。

幸运的是,canvas没有那么大的API。大多数canvas用法都是创建2D或WebGL上下文,然后使用该上下文来绘制。另外,唯一的canvas特定属性是widthheight。因此,这一切都非常容易使用,并在周围创建一个薄层,我们可以将其烘烤到Web组件API中。

现在,录制部分呢?

使用Web编解码器API编码视频

即使我将使用新的且当下时髦的Web编解码器API录制视频,但在包括画布在内的许多事物和本地(意思是没有额外的库)之前,可以录制许多内容。

捕获:没有mp4!

我正在考虑[Mediarecorder(https://developer.mozilla.org/en US/doc/web/api/mediarecorder)API。这实际上确实令人困惑。在写这篇文章时,我不得不三重检查我要说实话。请参阅recording your canvas的这篇方便的文章。您可以指定MediaRecorder和传出数据斑点的MIME类型。但是问题在于,仅仅因为您指定了MIME类型,并不意味着浏览器会支持它!这可能是由于传统上已获得专利的许可和法律问题,因此可能是由于许可和法律问题。无论哪种方式,通常都支持开源WebM格式,因此通常是人们用来记录的。

虽然这很不方便! WebM在任何地方都不需要使用视频,因此,如果要将其放入自己喜欢的编辑器中,请准备将其转换为MP4或类似。

在您的浏览器中,有一些转码的解决方案,例如[ffmpeg.js(https://github.com/Kagami/ffmpeg.js/)。 MP4实际上只是包含文件!您记录的编解码器仍然需要由MP4支持。因此,如果您走这条路线,请准备ffmpeg.js做很多工作!

幸运的是,我们可以使用新的Web编解码器API提供的VideoEncoder对象记录我们的画布。

让我们从实例化和配置我们的VideoEncoder开始:

const myencoder = new VideoEncoder({
    output: onEncoderOutput.bind(this),
    error: onEncoderError.bind(this)
});

await myencoder.configure(cfg);

首先,我们创建我们的编码器。第一个参数使我们可以指定功能来处理编码的输出和错误。它这样运行是有原因的。您将在编码器中添加帧,但是当它具有足够的框架以适合EncodedVideoChunk时,您会看到这些帧在输出中通过。它可以快速运行,并在JavaScript的主线程中运行。确实,它正在呼唤您的操作系统级别系统编码器。这就是为什么您可能要使用VideoEncoder.isConfigSupported(config)方法来测试是否首先支持您的特定编码配置。否则,您将击中您指定的error处理程序,并且会给您一个超级模糊的错误。

使用VideoEncoder实例化,我们需要对其进行配置。配置对象指定了一些诸如宽度,高度和编解码器之类的东西。这是一个示例:

{
    codec : 'avc1.42001E',
    width: 640,
    height: 480,
    hardwareAcceleration:"prefer-hardware",
    avc:{format:"avc"}
}

这让编码器知道我想用AVC级别的3级编码视频。如果可能的话,我还想使用GPU。 AVC是一种可以卡在MP4容器中的编解码器,因此一旦到达包装文件点,我们就可以去那里。

我们如何知道它的3级?那是什么意思? AVC迭代良好,并为每次迭代提供新的功能。我之所以选择3级,是因为在我的项目中,我想捕获1920x1080尺寸,我相信3级是支持此尺寸的最低版本。 42001E的编解码字符串指定了这一点。不用担心,我也不知道这也很有意义。我最终使用了这个handy table

配置编码器(不同步,您必须等待它),您就可以开始编码!

编码画布

所以我们可以准备去编码器,我们可以开始向其扔画布吗?不完全的。首先,我们需要创建一个VideoFrame才能传递。

const frame = new VideoFrame(canvas, { duration: 0, timestamp: 0 } );

这是我们将画布扔进去的地方!我们当时采用画布的当前状态,并从中制作一个VideoFrame

您可能需要在使用时指定持续时间和时间戳。这是一个可选的参数,这个对象,但为了理智的缘故,我认为使用它是最好的做法。从我所看到的,您可以将框架持续时间和时间视为元数据。它将有助于编写文件,确定帧量,确定文件持续时间以及类似的内容。
老实说,duration似乎不会影响我所看到的任何东西,但是如果您再次加载框架并想要持续时间以获取自己的参考。

甚至更多的建议 - 我已经看到时间戳数字通常在微秒中表达。但是不,我没有玩过实际的视频文件中看到这有多少重要。这可能与您自己播放帧的方式,或图书馆对文件进行编码或外部播放器如何处理这些时间戳有关。
无论哪种方式,微秒似乎都非常标准。为了清楚起见,这意味着1秒是1,000,000微秒!

无论如何,一旦您准备将该框架传递到编码器,只需执行以下操作:myencoder.encode(frame, { keyFrame: true/false });

简单吧?当您看到keyframe属性时,也许不是!此对象参数也是可选的,但是问题在于,除非您使用它,否则您的视频不会很好。默认情况下,如果您不传递此参数,则KeyFrame属性默认为false。

您可以将关键帧视为视频的完整图像。但是,如果每个帧都是钥匙帧会发生什么?
这使您的文件非常大!相反,如果您通过false(或什么都没有),则您正在创建“增量”帧。增量框架仅包含有关更改的信息,可以节省大量文件大小。

想想一个人反对静态背景。如果背景没有改变,为什么一遍又一遍地保存所有这些像素?

我不具体知道密钥帧之间的最佳距离是什么,但是当我探索Web编解码器时,我注意到臭名昭著的开源电影"Big Buck Bunny"在每个密钥帧之间都有1秒的间隔,所以我一直在默认这一点。这可能实际上取决于您的视频包含的动作以及您对压缩的关注。

处理我们的块

一旦将框架传递给编码器,您基本上只是相信它会通过设置VideoEncoder时指定的处理程序返回。

这是我的recordable-canvas回调,配有参数的打字稿定义:

protected onEncoderOutput(chunk: EncodedVideoChunk, chunkMetadata: EncodedVideoChunkMetadata) {
    this.recording.push(chunk);
    if (chunkMetadata?.decoderConfig?.description) {
        this.description = chunkMetadata.decoderConfig.description;
    }
}

注意,我们得到了EncodedVideoChunkEncodedVideoChunkMetadata。出于我们的目的,直到我们要保存文件时,元数据直到最后才有用。为此,我们需要“描述”,老实说,这对我来说是一个不可理解的数据缓冲区,可以传递给我们的MP4库。

所有话说,元数据确实包含宽度,高度,色彩空间信息等信息,如果您有使用!我们没有,因为我们只是直接传递此信息,并且我们已经知道宽度和高度,因为我们从一开始就从该信息开始时就开始了。

现在,EncodedVideoChunk可以包含多个视频帧,所有视频帧都被压缩并捆绑为一件事。但是,实际上,我不确定我在任何给定的块中都看过多个框架。

无论哪种方式,这都是我们的“视频流”!我只是将所有这些块都推入数组。幸运的是,它们确实按照您将它们喂入编码器的顺序(phew!)。

最酷的是它们是编码和压缩的视频帧,但是可以使用方便的
来解码它们 Web编解码器API的VideoDecoder。实际上,它们可以按照您想要的任何顺序进行解码!您好视频编辑!

写我们的MP4文件

是的,使用Web编解码器API很复杂。关于视频有很多知识,可以了解您在这里做什么。但是这样的低水平API有很多事情可以做一些疯狂的事情!

这是您可能不知道的另一个视频术语:Muxing。 Muxing基本上意味着您将视频和音轨结合在一起
并将它们放入容器文件中(例如MP4)。同样,登录也相反。...从容器开始并获得单独的视频和音轨。

不幸的是,浏览器没有提供MUX/Demux视频的方法。也就是说,有一些讨论,因为Web编解码器API很棒,但是除非您可以使用Muxing/Demux,否则您将无法做很多事情。

幸运的是,有很多很棒的图书馆可以为我们做到这一点。一个这样的库是[mp4box.js(https://github.com/gpac/mp4box.js/)。 mp4box.js是MP4实用程序的难以置信的全面工具集的JS端口。

尽管这些事情通常会发生,但它是一个commonjs项目。叹。我不会上肥皂盒说ES模块不再是未来,而是现在,我们都应该放弃commonjs。取而代 在recordable-canvas组件中,因此我们可以将其用作ES模块。我们[此操作是为Tensorflow.js(http://webcomponents.space/posts/s01e03/)在太空中的Web组件上进行的。但这意味着我将其与汇总捆绑在一起,因此它成为我们项目中的另一个源文件。这意味着想要使用recordable-canvas的最终用户仍然可以将原始源文件作为ES模块使用,而不必担心前端工具设置本身。

保存!当我们开始录制时,我最初使用mp4box.js创建文件确实掩饰了。但这只是因为它是如此简单:file = MP4Box.createFile();

但是,当我们停止录制时,这是最后一步的recordable-canvas代码:

public async stopRecording(saveas?: string) {
        const oneSecondInMillisecond = 1000;
        const timescale = 1000;
        let durationInMillisecond = 1000;
        const fps = this.frameCount / (this._duration / 1000);
        let frameTimeInMillisecond = 1000 / fps;
        let totalFrames = Math.floor(durationInMillisecond / frameTimeInMillisecond) ;

        this._isRecording = false;
        this.encoder?.flush().then(() => {
            this.encoder?.close();

            this.recording.forEach((chunk: EncodedVideoChunk) => {
                let ab = new ArrayBuffer(chunk.byteLength);
                chunk.copyTo(ab);
                if (this.track === null) {
                    this.track = this.file.addTrack({
                        timescale: (oneSecondInMillisecond * timescale),
                        width: this.width,
                        height: this.height,
                        nb_samples: totalFrames,
                        avcDecoderConfigRecord: this.description });
                }
                this.file.addSample(this.track, ab, {
                    duration: (frameTimeInMillisecond * timescale),
                    dts: chunk.timestamp,
                    cts: chunk.timestamp,
                    is_sync: (chunk.type === 'key')
                });
            });
            if (saveas) {
                this.saveFile(saveas);
            }
        });
    }

    public saveFile(saveas: string) {
        if (this.file) {
            this.file.save(`${saveas}.mp4`);
            return true;
        } else {
            console.warn('Cannot save file because no file was created/recorded');
            return false;
        }
    }

第一步是冲洗我们的编码器。也许我们已经停止编码帆布框架,但这并不意味着我们编码器中没有任何挥之不去的框架等待更多框架,以便可以编码该集合并创建EncodedVideoChunk。因此,我们等待齐平,在等待的时候,当它们通过我们的编码器回调通过时,可能会将更多帧附加到我们的数组中。

下一步。。。mp4box具有一些复杂的API。我们在数组中为每个EncodedVideoChunk添加样本。我们还确保存在视频曲目以添加这些示例。基本上,我们的块被转换为与MP4Box(密钥帧,时间戳和所有)兼容的格式。如果您不知道MP4文件的基本规格,可能会有点陌生(我当然不)。

之后,MP4Box具有保存文件的不错方法。只需致电file.save(myfilename.mp4),就完成了!

使用recordable-canvas

我不会涉及使用此组件的太多。我提到了repo中的文档和演示,但是最基本的用法是这样设置:

<recordable-canvas width="300" height="300"></recordable-canvas>
<script>
    import 'recordable-canvas';
    const canvas = document.body.querySelector('recordable-canvas');
    canvas.addEventListener('ready', () => { ...
</script>

组件“准备就绪”后,开始执行您喜欢的帆布操作!但是要启动/停止录制,只需在元素上调用startRecordingstopRecording,只要您想拍摄快照即可编码视频帧。

我应该呼吁我这是一个小而不是那么全面的项目。再次,我这样做是因为
我想录制一些我要求Chatgpt在webcomponents.space上为我的视频系列制作和使用的WebGL着色器。
那是一个整体磨难,您可以看到here on YouTube:)

当我为类似项目浏览NPM时,我确实看到this one看起来更好!它不是Web组件,而是它的作用相似。 Canvas-record似乎提供了几种编码您的录音的方法,包括一个应该超快的WASM!

无论您录制的哪一个,我的主要目标是谈论Web编解码器和Web组件。
希望您发现这有用!