踩坑指南:如何在你的网站上顺利使用ffmpeg.wasm

广告位招租
扫码页面底部二维码联系

ffmpeg.wasm这个项目可以帮助我们在浏览器内使用ffmpeg进行视频处理,是一个非常棒的项目。然而,要真正在浏览器里面跑起来,却需要花费非常多的精力去走好第一步。本文详细聊一聊。

必要文件有哪些?

当你打算使用它时,并不代表你可以立即使用它。网上的大部分教程都无法直接启动。甚至,它的官方教程都没有一篇能让你快速跑起来的文档。这是因为它不像其他库一样,可以被直接加载,要在前端加载和使用ffmpeg.wasm,需要多个步骤多个文件。我们先来看看,必要的文件有哪些:

  • @ffmpeg/ffmpeg – 加载和调用库,用以被包含在你的实际业务代码中,作为调用ffmpeg.wasm的入口
  • @ffmpeg/util – 工具库,用以辅助上面的主库进行加载
  • @ffmpeg/core – ffmpeg.wasm的运行文件,不被包含在你的实际业务代码中,需要被远程加载

你需要使用yarn/npm先安装上面这些包,以下载代码。但是@ffmpeg/core这个包里面的代码可能需要被拷贝出来放在一个独立文件夹内,另外@ffmpeg/ffmpeg中的worker.js也是需要拷贝出来备用。是否需要拷贝,需要取决于你编译工具是否支持从node_modules目录下引用js作为url,同时,你还必须考虑另外一个问题,即你是不是通过CDN使用业务js文件。CND问题下面会讲。

每一个包下载之后,你进入每一个包的dist文件夹,可以看到esm和umd两个分包,虽然看上去只是模块差别,但是在使用上,天差地别。默认在webpack、vite等环境中,直接import时,走的是esm,记住这一点很重要。

当你选择其中一种模式之后,后续所准备的文件,也必须选择这种模式。例如选择esm模式后,后续其他包也要选择这种模式。但是,在ffmpeg.wasm的官方网站中,已经标注出弃用CDN,实际上也就弃用了umd。我在看它的dist时,也发现了umd下没有worker.js文件。因此,我们实际上只能选择esm。

接下来,我们看看实际上到底是哪些文件在起作用:

  • @ffmpeg/ffmpeg/dist/esm/classes.js 提供FFmpeg类,包含了加载和调用方法
  • @ffmpeg/ffmpeg/dist/esm/worker.js 提供了worker线程中的代码,在调用ffmpeg.load时,该文件作为classWorkerURL传入
  • @ffmpeg/core/dist/esm/ffmpeg-core.js 提供了用于加载wasm和暴露接口的代码
  • @ffmpeg/core/dist/esm/ffmpeg-core.wasm 提供了wasm文件

另外,你可能还需要util文件来使用辅助功能,特别是fetchFile函数,基本上一定会用到。因此,我们可以将文件也拷贝出来,重命名为util.js

  • @ffmpeg/util/dist/esm/index.js

以上这些文件入口将作为我们最后使用的全部文件入口。当然,这些文件内自身还引用了其他文件,这些我们都不用管,esm会自动加载它们。

如何快速部署?

我们将@ffmpeg文件夹直接从node_modules拷贝出来,重命名为一个名字叫做ffmpeg.wasm的文件夹,你可以只保留dist/esm目录,把其他目录全部剔除掉,甚至删除掉全部.d.ts和.map文件,这样可以节省空间。最后将ffmpeg.wasm这个文件夹放到你网站的根路径下,vite可以放在public目录下,或者通过vite的vite-plugin-static-copy插件,让vite自动拷贝ffmpeg.wasm这个目录,webpack下也有类似的拷贝插件。

当你把文件夹上传后,你可以通过类似 /ffmpeg.wasm/ffmpeg/dist/esm/classes.js 的路径访问到这些文件。

注意,这里的做法是标准做法,到目前为止,你不要想着其他方式,你的目的是先跑起来。

执行代码

接下来,你就可以在你自己的代码中开始使用ffmpeg.wasm了。

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';

async transcodeToMp4(file: File): Promise<ArrayBuffer> {
    const ffmpeg = new FFmpeg();

    // 加载上面部署好的必要文件,很重要,如果没有上面的处理,这里你都不知道要填什么
    await ffmpeg.load({
        coreURL: '/ffmpeg.wasm/core/dist/esm/ffmpeg-core.js',
        wasmURL: '/ffmpeg.wasm/core/dist/esm/ffmpeg-core.wasm',
        classWorkerURL: '/ffmpeg.wasm/ffmpeg/dist/esm/worker.js',
    });

    await ffmpeg.writeFile(file.name, await fetchFile(file));
    await ffmpeg.exec(['-i', file.name, '-c', 'copy', 'output.mp4']);
    const data = await ffmpeg.readFile('output.mp4');

    await ffmpeg.deleteFile('output.mp4');
    await ffmpeg.deleteFile(inputFileName);

    // @ts-ignore
    return data.buffer;
}

这段代码实现了最简单的调用ffmpeg.wasm来将视频转化为mp4文件的过程。不过实际上,我们在真实的环境中,不会如此简单的写代码,我们会考虑复用ffmpeg实例,考虑只需要加载一次这些必要文件等等,这里只是为了方便演示,使得代码非常清晰,才这么给了一个函数(虽然它也是正确的代码)。

部署在CDN上

很多项目部署方案不是前端团队自己出的,js代码必须部署在CDN上。这就遇到跨域的问题了。

这里会遇到两个问题,一个是脚本加载跨域,另一个是webworker实例化时不允许传入非当前被访问页面origin之外的.js文件路径。这两个问题都可以解决。

首先,你需要在CDN后台,调整跨域策略,让你的网站可以通过CORS访问到这些脚本。其次,你需要使用util中的toBlobURL来解决webworker的问题。

具体修改后的代码如下:

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';

async transcodeToMp4(file: File): Promise<ArrayBuffer> {
    const ffmpeg = new FFmpeg();

    // 加载上面部署好的必要文件
    await ffmpeg.load({
        coreURL: await toBlobURL(new URL('/ffmpeg.wasm/core/dist/esm/ffmpeg-core.js', import.meta.url), 'application/javascript'),
        wasmURL: await toBlobURL(new URL('/ffmpeg.wasm/core/dist/esm/ffmpeg-core.wasm', import.meta.url), 'application/wasm'),
        classWorkerURL: await toBlobURL(new URL('/ffmpeg.wasm/ffmpeg/dist/esm/worker.js', import.meta.url), 'application/javascript'),
    });

    await ffmpeg.writeFile(file.name, await fetchFile(file));
    await ffmpeg.exec(['-i', file.name, '-c', 'copy', 'output.mp4']);
    const data = await ffmpeg.readFile('output.mp4');

    await ffmpeg.deleteFile('output.mp4');
    await ffmpeg.deleteFile(inputFileName);

    // @ts-ignore
    return data.buffer;
}

通过上面这个修改,它会把这些脚本下载下来后,用blob:地址来挂载脚本,而webworker支持在同域名下的blob:脚本作为worker脚本,这就解决了webworker实例化问题。

需要注意的是,上面这段业务代码的最终build,必须安装我前面说的方法,和必要文件们部署在一起,且都放在根目录下,否则你又不知道文件路径该怎么填写了。

启用多线程

⚠️ 注意,启用多线程不仅复杂,而且带来的限制(负面影响)还比较大,建议采用下文的iframe方案时考虑。

上面我们已经完全跑起来一个ffmpeg.wasm的实例了,但是,官方提供了多线程(启动多个worker)来加速处理的能力,下面就介绍如何使用多线程。

我们需要新安装一个包@ffmpeg/core-mt,mt就是multiple thread的缩写。按照上面必要文件一节的方法进行部署。接下来,直接进入到执行代码处:

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';

async transcodeToMp4(file: File): Promise<ArrayBuffer> {
    const ffmpeg = new FFmpeg();

    await ffmpeg.load({
        coreURL: '/ffmpeg.wasm/core-mt/dist/esm/ffmpeg-core.js',
        wasmURL: '/ffmpeg.wasm/core-mt/dist/esm/ffmpeg-core.wasm',
        workerURL: '/ffmpeg.wasm/core-mt/dist/esm/ffmpeg-core.worker.js',
        classWorkerURL: '/ffmpeg.wasm/ffmpeg/dist/esm/worker.js',
    });

    await ffmpeg.writeFile(file.name, await fetchFile(file));
    await ffmpeg.exec(['-i', file.name, '-c', 'copy', 'output.mp4']);
    const data = await ffmpeg.readFile('output.mp4');

    await ffmpeg.deleteFile('output.mp4');
    await ffmpeg.deleteFile(inputFileName);

    // @ts-ignore
    return data.buffer;
}

在代码层面,我们只需要按照上面红色处进行修改即可。

然而这还没有完,由于涉及到多线程的问题,所以自然也涉及到“内存共享”问题,core-mt会使用到SharedArrayBuffer来在多线程之间共享内存,然而,要开启对SharedArrayBuffer的支持,则成本比较大。首先,你需要在你的主站点(也就是提供视频转换的站点)域名,开启如下两个headers:

'Cross-Origin-Opener-Policy': 'same-origin'
'Cross-Origin-Embedder-Policy': 'require-corp'

这才能让SharedArrayBuffer在当前域名下工作。

然而,如果你的团队无法自己控制服务器的话,就比较麻烦,因为devops团队很难为你开独立的配置。而且开启之后,会让你的网站元气大伤。你的网站加载第三方站点的资源(例如CDN上的文件、第三方服务商、统计代码等等),都必须要求第三方的文件服务headers中(除了开启CORS之外)包含如下:

'Cross-Origin-Resource-Policy': 'cross-origin'

你自己的CND服务还可能有机会去设置这个头,如果是第三方服务商,例如统一登录代码、网站统计,则几乎必然受到影响。(如果你是把代码放在CDN上,不要忘记了使用toBlobURL来转换。)我的网站也因此导致我的google登录受到影响,不得不放弃多线程模式。

通过iframe来提供多线程服务

既然开启多线程能力对主站点的影响巨大,那么,可不可以通过开一个子站点,在这个子站点中实现上述多线程的配置呢?我们开一个子站点,在保证子站点和主站点可以自由通信的处理之后,我们按照上面的方法在子站点开启对SharedArrayBuffer的支持,然后在子站点下提供一个html,在该html内按照上述逻辑,提供多线程处理视频的能力。在主站点中通过iframe来加载子站点的这个html,再利用postMessage实现两者之间的通信。当主站点有视频处理请求的时候,将请求发送给iframe内的代码完成,完成后再把结果通过postMessage返回给主站点。这样就可以利用多线程能力了。不过,iframe本身其实也限制多多,你需要一个一个解决。

写在最后

在我们的网站需要用到视频处理的能力的时候,我们可以利用ffmpeg.wasm来直接在浏览器内进行处理。然而,这个项目虽好,但在一些很基础的使用上存在坑,没有去趟过,还是会比较痛苦。本文总结了我遇到了几个点,如果你也遇到了,欢迎在下面评论区留言,我会继续补充。

2025-08-31 1884 ,

为价值买单,打赏一杯咖啡

本文价值18.84RMB