ffmpeg.wasm
ffmpeg & wasm 是什么
ffmpeg是功能非常强大的视频处理开源软件,很多视频播放器就是使用它来做为内核。
webassembly 是 Binary Code, 是编译目标。WebAssembly将很多编程语言带到了Web中。
wasm解决了性能问题,将各种耗性能的app从Desktop搬到Web上。
想用ffmpeg纯web端实现处理视频。就要用到wasm提高操作性能,就是ffmpeg.wasm做的事情。
前端实现
不使用node, 纯前端项目,实现在browser上处理视频。
上图是git的文档, 只需要在本地引入ffmpeg.min.js (文件很小22KB)就可以了。
1. 获取ffmpeg.min.js文件。
install 后,会看到文件ffmpeg/dist/ffmpeg.min.js。
然后,直接将ffmpeg.min.js文件放到public下,打包后,直接在根目录。
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了。但是会发现网站上其他的图片视频等资源获取不到了。 会看到新的报错信息。接下来处理一下网站上的其他资源获取问题。
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了。
视频剪切
如图可以看到 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,就还好。
但是运行时,用Core i7, 内存8G电脑测试剪切一个3秒的视频。浏览器的任务管理器可以看到cpu占比就挺高的。
参考文档:
developer.mozilla.org/zh-CN/docs/…
zhuanlan.zhihu.com/p/165776646