FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。最主要的战场是在操作系统中,随着浏览器能力的不断发展,我们可以通过 WebAssembly 在浏览器运行各种语言的程序,这其中就包括了使用 c++ 写的 FFmpeg。

虽然 FFmpeg.wasm 在浏览器中运行的性能并不是很乐观,但是,它展示了一种在浏览器中运行 ffmpeg 的可能性,也许在不远的将来,我们可以在浏览器中解码任何封装格式的视频,又或许以后有大牛会做出一款Web OBS或者 Web PR。

image-20221220222028346

畅想了这么久未来,我们开始进入正题……

SharedArrayBuffer

额,还得再等会儿,先来了解一种 JavaScript 中的数据类型——SharedArrayBuffer。

SharedArrayBuffer 对象用来表示一个通用的、固定长度的原始二进制数据缓冲区,类似于 ArrayBuffer 对象,它们都可以用来在共享内存(shared memory)上创建视图。

SharedArrayBuffer 的出现是为了解决大体量的数据在主线程或者工作线程中通信的性能瓶颈,但是这也引入了一个漏洞,为了安全,所有的主流浏览器于 2018 年禁用了 SharedArrayBuffer,在 2020 年通过一些安全措施SharedArrayBuffer已经可用。

我们需要在上下文中设置以下HTTP 响应头

HTTP/1.1 304 Not Modified
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

否则会出现以下报错

image-20221220224023994

浏览器提供了检查环境的能力,我们可以通过crossOriginIsolated属性判断当前的环境是否可用 SharedArrayBuffer。

if (!crossOriginIsolated) {
  alert('当前环境不可用')
  return
}

在开发过程中我们可以在 devServer 中设置响应头,例如在 Vite 中我们可以进行如下设置

export default defineConfig({
  plugins: [vue(), {
    name: 'configure-response-headers',
    configureServer: server => {
      server.middlewares.use('/', (_, res, next) => {
        res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
        res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
        next()
      })
    }
  }],
})

这里通过 vite 的插件机制来完成,也可以理解为 express 的中间件。

如果是 webpack 等打包工具,同样的原理参照文档添加响应头即可。

如果实在开发环境,需要在静态资源服务器的响应中添加响应头,例如 Nginx

location / {
  root /usr/share/nginx/html/edgestack;
  index index.html;
  try_files $uri $uri/ /index.html?$query_string;
  add_header Cross-Origin-Embedder-Policy 'require-corp';
  add_header Cross-Origin-Opener-Policy 'same-origin';
}

FFmpeg.wasm

经过一些“题外话”,终于进入了今天的正文,可以先看一下他的官网

我们需要先安装@ffmpeg/core@ffmpeg/ffmpeg两个依赖,core 是核心包,我们可以通过 CDN 的形式引入,这里安装是为了本地引入,安装完成之后我们可以从 node_modules 目录中将 core 文件复制出来。

image-20221221200011621

将这三个文件复制出来就可以通过开发工具的静态资源代理来获取了(放到你设置的静态资源目录,默认一般是 public)。

然后可以通过以下方式初始化

import { createFFmpeg } from '@ffmpeg/ffmpeg'

const ffmpeg = ref(createFFmpeg({
  corePath: '/ffmpeg/ffmpeg-core.js', // 刚才移动到的目录
  log: true
}))

在初始化之后就可以使用fetchFile读物文件,这里读取文件并不是直接从文件系统中读取文件,而是从内存中将数据读取为 Uint8Array 格式数据。

import { fetchFile } from '@ffmpeg/ffmpeg'

const videoInput = ref<HTMLInputElement>() // file 类型的 input

async function getVideoUrl() {
  if (!crossOriginIsolated) {
    alert('当前环境不可用')
    return
  }
  if (!ffmpeg.value.isLoaded()) {
    await ffmpeg.value.load()
  }
  
  const fileName = videoInput.value?.files?.[0].name as string
  const video = await fetchFile(videoInput.value?.files?.[0] as File)
}

ffmpeg 需要手动 load,可以通过 ffmpeg.isLoaded 来判断当前环境时候加载完成(这里使用了Vue3 的 ref,所以我使用.value来访问 ffmpeg)

在读取完数据之后就可以使用ffmpeg 来进行处理了,使用 run 方法来执行 ffmpeg 的命令,这里的命令和 linux 中运行时是一样的,只是需要把原本的命令按照空格拆成一个个的字符串。

const videoRef = ref<HTMLVideoElement>() // video 元素

async function getVideoUrl() {
  // 省略上一部分代码
  const outputName = fileName.split('.')[0] + '.mp4'
  
  ffmpeg.value.FS(
    'writeFile',
    fileName,
    video)

  await ffmpeg.value.run(
    '-i',
    fileName,
    '-acodec',
    'aac',
    '-vcodec',
    'libx264',
    '-y',
    outputName
  )

  const data = ffmpeg.value.FS('readFile', outputName)
  videoRef.value!.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4'}))
}

ffmpeg.FS 用于文件读写, 这里的读写也不是相对于文件系统, 可以理解为应用程序相对于 ffmpeg, 使用 writeFile 将文件从内存写入 ffmpeg, readFile 将文件冲 ffmpeg 读取到内存

这样就可以进行操作视频了,我们可以加点逻辑计算一下视频处理的时间

async function getVideoUrl() {
  // 函数开始
  const startTime = new Date().getTime()
  
  // 函数结束
  const endTime = new Date().getTime()
  console.log('视频处理时间:', endTime - startTime, 'ms')
}

经过测试一个 47秒钟的视频,处理时间为 68 秒

image-20221221202014322

同样的视频在 mac 命令行里处理的时间是 5s 左右

image-20221221202739481

目前看来,ffmpeg 在浏览器中的性能还是有待提升,在生产环境应用的能力还是不太够。

具体的 ffmpeg 使用方法就不在这里介绍了,毕竟本文的主题是在浏览器中引入 ffmpeg。如果有时间我会总结一下 ffmpeg 的一些常用命令。


前端小白