ffmpeg.wasm处理视频

5,478 阅读4分钟

ffmpeg.wasm

github.com/ffmpegwasm/…

ffmpeg & wasm 是什么

ffmpeg是功能非常强大的视频处理开源软件,很多视频播放器就是使用它来做为内核。

webassembly 是 Binary Code, 是编译目标。WebAssembly将很多编程语言带到了Web中。

wasm解决了性能问题,将各种耗性能的app从Desktop搬到Web上。

想用ffmpeg纯web端实现处理视频。就要用到wasm提高操作性能,就是ffmpeg.wasm做的事情。

前端实现

不使用node, 纯前端项目,实现在browser上处理视频。

image.png

上图是git的文档, 只需要在本地引入ffmpeg.min.js (文件很小22KB)就可以了。

1. 获取ffmpeg.min.js文件。

install 后,会看到文件ffmpeg/dist/ffmpeg.min.js。

image.png

然后,直接将ffmpeg.min.js文件放到public下,打包后,直接在根目录。

image.png

image.png

2. index页面引入

这里引入后,使用时就很方便,直接获取 const { createFFmpeg, fetchFile } = FFmpeg;

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
    <script src="/ffmpeg.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

注意: 纯前端不需要install @ffmpeg/ffmpeg @ffmpeg/core。上面install,就为了方便拿ffmpeg.min.js

按照上面2步,到这里都比较简单,按理可以直接使用了。但是会遇到比较麻烦的跨域隔离问题。当给网站设置好跨域隔离后,又会发现网站上其他资源获取不了。哎,所以在使用前,要先处理一下跨域隔离。

先会发现报错信息,SharedArrayBuffer is not defined。因为shareArrayBuffer 要求跨域隔离same-origin。

跨域隔离(本地&部署)

git上有一句提醒:SharedArrayBuffer is only available to pages that are cross-origin isolated.So you need to host your own server with  Cross-Origin-Embedder-Policy: require-corp and Cross-Origin-Opener-Policy: same-origin headers to use ffmpeg.wasm.

因为用了sharedArrayBuffer共享内存。然后shareArrayBuffer要求host设置跨域隔离same-origin。

本地处理

这里demo用了vite+react,直接在vite.config.js中设置。其他框架类似设置就可以。

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
  headers: {
      "Cross-Origin-Opener-Policy": "same-origin",
      "Cross-Origin-Embedder-Policy": "require-corp",
    },
  },
})

部署处理

响应用户的HTTP请求时,增加HTTP header, coop + coep 这两个响应头。

Response Headers

cross-origin-embedder-policy: require-corp

cross-origin-opener-policy: same-origin

到这里,可以正常使用ffmpeg.wasm了。但是会发现网站上其他的图片视频等资源获取不到了。 会看到新的报错信息。接下来处理一下网站上的其他资源获取问题。

image.png

Because your site has the Cross-Origin Embedder Policy (COEP) enabled, each resource must specify a suitable Cross-Origin Resource Policy (CORP). This behavior prevents a document from loading cross-origin resources which don’t explicitly grant permission to be loaded.

跨域隔离后,加载其他资源(图片视频等)

因为host加了两个响应头启用了跨域隔离。那这个host上的其他资源,所有跨域资源都需要明确被允许加载。比如图片和视频。

明确被允许加载,有两种实现方式,一种是CORP,另一种是 CORS。

CORS

<img crossOrigin="anonymous" alt="图片" src={value} /> 
<video crossOrigin="anonymous" controls width={200} src={URL.createObjectURL(video)} />

CORP

对应的图片或者视频域名配置加header。Cross-Origin-Resource- Policy:cross-origin

各种视频操作

完成上面引入和跨域隔离两步,就可以愉快地各种操作使用ffmpeg了。

视频剪切

image.png 如图可以看到 fetchFile 接受的入参格式,下面代码尝试了传file,和直接用图片地址都可以。

import { useState } from "react";
import { InputNumber, Button, Card } from "antd";
// import { createFFmpeg, fetchFile } from "@ffmpeg/ffmpeg";
// const ffmpeg = createFFmpeg({
//   corePath: CONFIG.IS_TEST
//     ? "http://localhost:3000/ffmpeg-core.js"
//     : "ffmpeg-core.js",
//   log: true,
// });

// eslint-disable-next-line no-undef
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({
  log: true,
});

const fakeUrl =
  "https://*****.mp4";

function VideoSlice() {
  const [video, setVideo] = useState();
  const [start, setStart] = useState(0);
  const [length, setLength] = useState(0);
  const [partVideo, setPartVideo] = useState();

  const sliceVideo = async (source) => {
    if (!ffmpeg.isLoaded()) {
      await ffmpeg.load();
    }

    ffmpeg.FS("writeFile", "vid.mp4", await fetchFile(source));
    await ffmpeg.run(
      "-i",
      "vid.mp4",
      "-s",
      "480x320",
      "-r",
      "3",
      "-t",
      String(length),
      "-ss",
      String(start),
      "-f",
      "mp4",
      "out.mp4"
    );

    const data = ffmpeg.FS("readFile", "out.mp4");
    const url = URL.createObjectURL(
      new Blob([data.buffer], { type: "video/mp4" })
    );
    setPartVideo(url);
  };

  return (
    <Card title="视频切片">
      {video && <video controls width={250} src={URL.createObjectURL(video)} />}
      <input
        type="file"
        onChange={(e) => {
          setVideo(e.target.files?.item(0));
        }}
      />
      <div>Url: {fakeUrl}</div>
      <div>
        <span>开始时间: </span>
        <InputNumber
          onChange={(value) => {
            setStart(value);
          }}
        />
        <span>结束时间: </span>
        <InputNumber
          onChange={(value) => {
            setLength(value - start);
          }}
        />
        <Button
          type="primary"
          style={{ marginLeft: 30 }}
          onClick={() => {
            sliceVideo(video);
          }}
        >
          截取
        </Button>
        <Button
          type="primary"
          style={{ marginLeft: 30 }}
          onClick={() => {
            sliceVideo(fakeUrl);
          }}
        >
          Url截取
        </Button>
      </div>
      {partVideo && <video controls src={partVideo} width={250} />}
    </Card>
  );
}

export default VideoSlice;

视频合并

import { useState } from "react";
import { Button, Card } from "antd";

const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({
  log: true,
});

function VideoSlice() {
  const [videoList, setVideoList] = useState([]);
  const [mergerVideo, setMergerVideo] = useState();

  const handleMergerVideo = async (source) => {
    if (!ffmpeg.isLoaded()) {
      await ffmpeg.load();
    }

    ffmpeg.FS("writeFile", "vid1.mp4", await fetchFile(videoList[0]));
    ffmpeg.FS("writeFile", "vid2.mp4", await fetchFile(videoList[1]));


    // concat协议 适合视频MPEG格式,其他格式文件,先转码再合并
    await ffmpeg.run("-i", "vid1.mp4", "-c", "copy", "-bsf:v", "h264_mp4toannexb", "-f", "mpegts", "vid1.ts");
    await ffmpeg.run("-i", "vid2.mp4", "-c", "copy", "-bsf:v", "h264_mp4toannexb", "-f", "mpegts", "vid2.ts");
    await ffmpeg.run("-i", "concat:vid1.ts|vid2.ts", "-c", "copy", "-bsf:a", "aac_adtstoasc", "-movflags", "+faststart", "out.mp4" );

    const data = ffmpeg.FS("readFile", "out.mp4");
    const url = URL.createObjectURL(
      new Blob([data.buffer], { type: "video/mp4" })
    );
    setMergerVideo(url);
  };

  return (
    <Card title="视频合并">
      {videoList[0] && <video controls width={250} src={URL.createObjectURL(videoList[0])} />}
      {videoList[1] && <video controls width={250} src={URL.createObjectURL(videoList[1])} />}
      <input
        type="file"
        onChange={(e) => {
          const newList = [...videoList, e.target.files?.item(0)];
          setVideoList(newList);
        }}
      />
      <div>
        <Button
          type="primary"
          style={{ marginLeft: 30 }}
          onClick={() => {
            handleMergerVideo(videoList);
          }}
        >
          合并
        </Button>
      </div>
      {mergerVideo && <video controls src={mergerVideo} width={250} />}
    </Card>
  );
}

export default VideoSlice;

图片转视频

import { useState } from "react";
import { Card, Button } from "antd";

const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({
  log: true,
});

export default function ImgToVideo () {
    const [imgs, setImgs] = useState([]);
    const [videoUrl, setVideoUrl] = useState();

    const imgToVideo = async () => {
        if (!ffmpeg.isLoaded()) {
            await ffmpeg.load();
          }
          console.log(imgs[0]);
        for (let i in imgs) {
            ffmpeg.FS('writeFile', `${i}.png`, await fetchFile(imgs[i]))
        }
        await ffmpeg.run('-r', "1", '-f', 'image2', '-i', '%d.png', 'video.mp4')
        const data = ffmpeg.FS('readFile', 'video.mp4')
        const url = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }))
        setVideoUrl(url);
    }

    return (
        <Card title="图片转视频">
          {
            imgs.length > 0 && imgs.map((item, index) => <p key={index}>{item.name}</p>)
          }
          <input
            type="file"
            onChange={(e) => {
            const list = [...imgs, e.target.files?.item(0)];
            setImgs(list);
            }}
          />
          <Button onClick={imgToVideo}>转视频</Button>
          {videoUrl && <video controls src={videoUrl} width={250} />}
        </Card>
    )
};

注意点

实际项目实现该功能,考虑到用户机器性能情况比较差,不采用这种纯web端的方式。

下载的话,ffmpeg-core.wasm 是8.5M,就还好。 image.png 但是运行时,用Core i7, 内存8G电脑测试剪切一个3秒的视频。浏览器的任务管理器可以看到cpu占比就挺高的。 image.png

参考文档:

ffmpeg.org/documentati…

github.com/ffmpegwasm/…

developer.mozilla.org/zh-CN/docs/…

zhuanlan.zhihu.com/p/165776646

www.ruanyifeng.com/blog/2018/0…

juejin.cn/post/684490…

juejin.cn/post/707889…