背景
当我们在做视频相关的业务时,常见的需求就是希望可以拿到视频的首帧作为视频的封面图,本文主要介绍了纯前端(浏览器端)方案,如何通过 video
和 canvas
截取视频的首帧,并将其绘制为图片,供我们实际业务使用。
目录
- 技术方案
- 效果示例
- 兼容方案
技术方案
- 技术原理:在 canvas 中绘制 video 标签的内容,并将其输出为图片。
- 核心代码:
const getVideoInfo = async (videoUrl) => { return new Promise((resolve) => { let video = document.createElement("video"); video.src = videoUrl; video.currentTime = 0.1; video.preload = "metadata"; video.addEventListener("loadeddata", async () => { let canvas = document.createElement("canvas"), width = video.videoWidth, //canvas的尺寸和图片一样,保证清晰度 height = video.videoHeight; canvas.width = width; canvas.height = height; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // 使用 await 等待 delay 函数完成,最好就是100ms,尝试过0、50ms都不行,画出来的图是黑屏 await delay(100); canvas .getContext("2d") .drawImage(video, 0, 0, canvas.width, canvas.height); //绘制canvas const thumb = canvas.toDataURL("image/jpeg"); // 上传base64 // 释放资源,防止移动端网页内存泄漏,导致页面崩溃 canvas.width = 0; canvas.height = 0; video.src = ""; video.load(); video.remove(); video = null; canvas = null; resolve(thumb); }); }); };
- 使用方法:只需要传入对应的 video 的视频源地址,即可获取对应的截图,可以将上述方法直接当作项目中的工具函数使用,或者直接应用在业务逻辑中。
效果示例
理论上 Chrome 电脑端可以直接使用,所以本文的案例偏向于移动端 H5 界面。主要使用了桌面端的 Chrome 的手机端模拟器,以及真机 iPhone 12 的微信浏览器(iPhone 的 IOS 系统中所有浏览器基本都是基于 Safari 的 webkit 内核,因此可以当作 Safari 浏览器看待)。
- Chrome 的移动端模拟器
-
默认视频显示效果
-
点击截图按钮后效果(不设置 video 的 poster 属性)
-
点击截图按钮后效果(设置 video 的 poster 属性)
-
- iPhone 12 的微信浏览器
- 默认视频显示效果
- 点击截图按钮后效果(不设置 video 的 poster 属性)
- 点击截图按钮后效果(设置 video 的 poster 属性)
- 默认视频显示效果
兼容方案
从上面的效果可以看到,在 iPhone 中的浏览器,默认不会加载视频首帧作为视频封面,需要开发者手动设置 poster 属性,而在 Chrome 的手机端模拟器上是会默认选择视频的首帧来作为视频封面的。
因此可以理解为本文的核心代码作为一种兼容方案,解决了 iPhone 手机浏览器中视频没有封面的兼容性问题。
以下为效果示例中的完整代码,同时也可以作为如何使用 getVideoInfo 方法的一种示例效果。大家可以根据实际情况来尝试 poster 的设置,以及视频帧的截取,调整 getVideoInfo 中的 currentTime 时间可以截取到预期的 video 播放画面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Canvas + Video 实现首帧截取</title>
</head>
<body>
<h3>Canvas + Video 实现首帧截取</h3>
<video
width="100%"
id="video"
src="./assets/video/test.mp4"
controls
></video>
<div>
<button id="btn">点我开始截取</button>
</div>
<h3>截取后的首帧截图</h3>
<div>
<img width="100%" id="img" src="" />
</div>
</body>
<script>
const video = document.getElementById("video");
const btn = document.getElementById("btn");
const img = document.getElementById("img");
btn.addEventListener("click", async () => {
const thumb = await getVideoInfo(video.src);
img.src = thumb;
// 设置 video 的 poster 属性
video.poster = thumb;
});
const getVideoInfo = async (videoUrl) => {
return new Promise((resolve) => {
let video = document.createElement("video");
video.src = videoUrl;
// 根据实际项目可以调整期望截取的视频帧
video.currentTime = 0.1;
video.preload = "metadata";
video.addEventListener("loadeddata", async () => {
let canvas = document.createElement("canvas"),
width = video.videoWidth, //canvas的尺寸和图片一样,保证清晰度
height = video.videoHeight;
canvas.width = width;
canvas.height = height;
const delay = (ms) =>
new Promise((resolve) => setTimeout(resolve, ms)); // 使用 await 等待 delay 函数完成,最好就是100ms,尝试过0、50ms都不行,画出来的图是黑屏
await delay(100);
canvas
.getContext("2d")
.drawImage(video, 0, 0, canvas.width, canvas.height); //绘制canvas
const thumb = canvas.toDataURL("image/jpeg"); // 上传base64
// 释放资源,防止移动端网页内存泄漏,导致页面崩溃
canvas.width = 0;
canvas.height = 0;
video.src = "";
video.load();
video.remove();
video = null;
canvas = null;
resolve(thumb);
});
});
};
</script>
</html>
杂谈
- 本文利用了浏览器的 video 标签和 canvas 绘制了视频首帧的获取,实际项目中可以借助服务端对视频做裁剪也能实现相同效果。其中可以在服务端利用无头浏览器,或者使用 FFmpeg,或者使用第三方多媒体服务等方式实现。
- 在我们处理 video 标签相关的逻辑和效果时,不要完全依赖桌面端的浏览器模拟器,要时刻关注 Chrome 和 Safari 的差异,以及移动端不同浏览器之间的差异,避免等到上线时才发现兼容性问题,临时更改方案,造成风险。
参考文档
浏览知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。