前言
之前接触过一个获取视频的第一帧当作封面图的需求,当时好像是后端处理的。那纯前端能不能拿到视频的第一帧甚至第 N
帧呢?
今天我们就来看看这个问题,不依赖服务端看看是否可以实现
获取第一帧
获取第一帧这个需求可能更常见一些,一般就是用来作为封面图展示。前端获取第一帧主要用到 video
的 loadeddata
事件以及 canvas
相关的 API
。
loadeddata
事件会在视频的第一帧加载完毕后触发,表示视频至少有一帧数据可用。我们可以监听这个时间的触发,然后把 video
元素绘制到 canvas上
,就可以拿到第一帧的图像信息。
封装一个 getFirstFrame
函数如下:
js
const getFirstFrame = (url) => {
return new Promise((resolve) => {
const video = document.createElement("video");
const canvas = document.createElement("canvas");
video.src = url;
video.addEventListener("seeked", async () => {
const ctx = canvas.getContext("2d");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const frameDataURL = canvas.toDataURL("image/png");
resolve(frameDataURL);
});
video.addEventListener("loadeddata", () => {
video.currentTime = 0;
});
});
};
直接在 loadeddata
中获取第一帧可能会导致空白,这里我们采用 loadeddata+seeked
的方式来获取。尝试使用 seeked
事件来确保视频已经加载并且能够渲染,然后再获取第一帧的图像数据。
在组件中使用如下:
js
const Fps = () => {
const [url, setUrl] = useState("");
useEffect(() => {
const run = async () => {
const src = await getFirstFrame("/flower.webm");
setUrl(src);
};
run();
}, []);
return (
<div className="container">
<img src={url} />
</div>
);
};
export default Fps;
这样就能获取到视频的第一帧,并渲染出来。
再拓展一下,我们可以拿这个生成好的 URL
转换成一个 file
对象,发送给后端,让后端存一下这第一帧的图片,以便后续当作视频封面来使用。
实现一个 dataURLtoFile
函数,这样就可以获取到一个 file
对象:
js
const dataURLtoFile = (dataurl, filename) => {
const arr = dataurl.split(",");
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
};
js
const file = dataURLtoFile(frameDataURL, "first_frame.png");
ffmpeg
ffmpeg
是一个强大的音视频处理工具集合库,一般跑在服务器上。但是它也有 wasm
版本,利用它的 wasm
版本我们就可以在前端做一些音视频处理的功能,包括我们今天要介绍的获取视频的任意一帧。
首先安装一下依赖:npm i @ffmpeg/ffmpeg @ffmpeg/util
,然后通过以下方式加载 ffmpeg
:
js
const ffmpegRef = useRef(new FFmpeg());
useEffect(() => {
const init = async () => {
const baseURL = "";
const ffmpeg = ffmpegRef.current;
ffmpeg.on("log", ({ message }) => {
console.log(message);
});
await ffmpeg.load({
coreURL: await toBlobURL(
`${baseURL}/ffmpeg-core.js`,
"text/javascript"
),
wasmURL: await toBlobURL(
`${baseURL}/ffmpeg-core.wasm`,
"application/wasm"
),
});
};
init();
}, []);
我是提前把 ffmpeg
依赖的一些文件下载到了本地,最简版本的加载方式可以参见它的文档------ffmpeg.wasm。
然后可以使用 ffmpeg
来获取某一帧,我们以获取第 10
帧为例:
js
const getFrame = async () => {
const ffmpeg = ffmpegRef.current;
await ffmpeg.writeFile("input.webm", await fetchFile("/flower.webm"));
await ffmpeg.exec([
"-i",
"input.webm",
"-vf",
"select='eq(n,9)'",
"-vframes",
"1",
"output.png",
]);
const data = await ffmpeg.readFile("output.png");
const blob = new Blob([data.buffer], { type: "image/png" });
const url = URL.createObjectURL(blob);
setUrl(url);
};
解释一下上面的代码:
-i input.webm
:定义输入的视频文件名是input.webm
。-vf "select=eq(n,9)"
:视频过滤器(Video Filter)
的参数,它的功能是从视频中选择出满足某种条件的帧。在这里,select=eq(n,9)
表示选择出第10
帧(注意,帧的起始计数从0
开始,所以第10
帧的序号是9
)。-vframes 1
:指定输出的帧数。在这里,我们只需要输出一帧,所以这个值是1
。output.png
:输出文件的名字。writeFile
,往wasm
虚拟文件系统中写入文件,同理,readFile
读取文件- 最后构造一个
url
,供页面展示
最后
以上就是本文的全部内容,介绍了"常见的"获取视频第一帧的方法,以及介绍了一个"不常见的"获取视频任意一帧的解决方案。
如果你觉得有意思的话,点点关注点点赞吧~