案例
这是在实际开发中遇到的问题,产品给出的效果图如下:
其实就是一个h5页面中有很多视频, 但是我为什么都画着预览图呢? 因为在ios中video标签中尽管有src属性, 但是在没有预览图的情况下, 是一个灰蒙蒙的图层的, 很丑, 效果如下:
看到这里是不是很崩溃, 在PC端, 在安卓端都是正常显示的, 但是在ios系统不行, 也就是说在ipad和iphone中不行.
究竟是如何产生这个问题的呢?
在ios系统中有个保护机制, 如果video标签未开始播放, 是不会去加载视频的.
所以也就是说, video标签还未去加载视频, 就显示不出来视频首帧画面.
把video标签加上autoplay属性, 就正常看到视频的画面了.
但是又出现了新的问题, 这样的话, 页面上很多视频, 导致视频声音重叠了
把video标签加上muted属性, 表示静音播放.
要想在video标签中显示首帧画面, 需要添加poster属性, 属性值应该是图片的url
接下来就是怎么获取到视频首帧的图片的url
前端获取视频首帧画面
loadedmetadata事件监听+canvas渲染首帧
在网上找了少相关事例, 同时也问了一下chatGPT如何处理, 得到的结果都差不多, 监听video标签的两个事件
一个是加载元数据loadedmetadata
一个是浏览器估计它可以在不停止内容缓冲的情况下播放媒体直到结束, canplaythrough事件
还有一个canplay, 浏览器可以播放媒体文件了,但估计没有足够的数据来支撑播放到结束,不必停下来进一步缓冲内容。
具体属性和方法可以查看MDN
video.addEventListener(
'loadedmetadata',
() => {
console.log('loadedmetadata')
const canvas = document.createElement('canvas');
video.addEventListener('canplaythrough', () => {
console.log('canplaythrough')
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
const firstFrame = canvas.toDataURL();
// console.log(firstFrame); // 输出第一帧画面的Base64编码字符串
});
},
{ once: true }
);
问题:
但是只是这样处理, 在谷歌浏览器端, 图片的不能正常展示出来
排查: 把canvas渲染出来, 新增img标签, 看一下base64的图片是什么情况
结果竟然连canvas画出来都是一张透明图层
img标签展示
1.第一帧显示不出来, 获取第二帧
指定video.currentTime = 2, 来获取第二帧
const canvas = document.getElementById('canvas');
const video = document.createElement('video');
video.src = "视频链接";
let firstFrame = true;
video.addEventListener('seeked', function() {
firstFrame = true;
});
video.addEventListener(
'loadedmetadata',
() => {
video.currentTime = 2;
video.addEventListener('canplay', () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
// const firstFrame = canvas.toDataURL('image/png');
document.querySelector("#testImg").src = canvas.toDataURL();
// console.log(firstFrame); // 输出第一帧画面的Base64编码字符串
});
},
{ once: true }
);
这个时候canvas显示是正常的了, 但是img还是不正常, 而且控制台报错了
原因:
videoFirstFrame.html:37 Uncaught DOMException: Failed to execute ‘toDataURL’ on ‘HTMLCanvasElement’: Tainted canvases may not be exported.
at HTMLVideoElement.
This error occurs when you try to use the toDataURL() method on an HTML canvas element that has been tainted. A tainted canvas is one that has been affected by a security feature that prevents scripts from accessing pixel data from certain sources, such as images loaded from a different domain.
当您尝试在已被污染的 HTML 画布元素上使用 toDataURL() 方法时,会发生此错误。受污染的画布受到安全功能的影响,该功能阻止脚本访问来自特定来源的像素数据,例如从不同域加载的图像。
To resolve this error, you need to make sure that all the images used in your canvas are hosted on the same domain as your webpage, or that they have the appropriate cross-origin resource sharing (CORS) headers set. If you cannot control the source of the images, you may need to proxy them through your own server to avoid the tainting issue.
要解决此错误,您需要确保画布中使用的所有图像都托管在与您的网页相同的域中,或者它们具有适当的跨域资源共享 (CORS) 标头集。如果您无法控制图像的来源,您可能需要通过自己的服务器代理它们以避免污染问题。
2.视频链接跨域
导致渲染出来的base64图片, 浏览器认为不安全
给video新增属性crossOrigin为anonymous
const video = document.createElement('video');
video.crossOrigin = 'anonymous';
video.src = url;
return new Promise((resolve, reject) => {
try {
video.addEventListener(
'loadedmetadata',
() => {
video.currentTime = 2;
const canvas = document.querySelector('#canvas');
video.addEventListener('canplay', () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
const firstFrame = canvas.toDataURL();
// document.querySelector("#testImg").src = firstFrame;
this.testImg = firstFrame;
// console.log(firstFrame); // 输出第一帧画面的Base64编码字符串
resolve(firstFrame);
});
},
{ once: true }
);
} catch (err) {
console.error(err);
reject('');
}
});
可以设置video允许视频url跨域,这样调用canvas.toDataURL()就能够看到图片正常展示在谷歌浏览器上
3.ios显示还是白屏
本以为将firstFrame赋值给video的poster属性上,就能解决ios白屏的问题了,但是效果却仍然不行,原因是video.canplay事件未执行
换成video.canplaythrough事件也还是未执行
原因还是ios中,视频只有在播放的情况下会加载
所以就只能给video标签加上autoplay属性和muted属性,这样就能在ios中获取到视频首帧了。
所以最后在ios上,前端负责渲染第一帧的处理,最后逻辑是
getFirstImg(url) {
const video = document.createElement('video');
video.crossOrigin = 'anonymous'; // 允许url跨域
video.autoplay = true; // 自动播放
video.muted = false; // 静音
video.src = url;
return new Promise((resolve, reject) => {
try {
video.addEventListener(
'loadedmetadata',
() => {
console.log('loadedmetadata')
video.currentTime = 2;
const canvas = document.createElement('canvas');
video.addEventListener('canplaythrough', () => {
console.log('canplaythrough')
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
const firstFrame = canvas.toDataURL();
// console.log(firstFrame); // 输出第一帧画面的Base64编码字符串
resolve(firstFrame);
});
},
{ once: true }
);
} catch (err) {
console.error(err);
reject('');
}
});
},
async handleImg() {
for (let i = 0; i < this.videoList.length; i++) {
this.videoList[i].img = await this.getFirstImg(this.videoList[i].url);
console.log(i, 11111);
}
},
mounted() {
this.handleImg();
},
4.当视频数量很多时,加载时间过长
前端来获取视频首帧终究不是上上策,因为前端处理视频,就需要视频加载这个过程,如果页面上有很多视频,那访问这样页面,岂不是要偷偷浪费用户流量来加载,最理想的情况是:用户点击播放,需要看哪个视频,就加载哪个视频,这样既能保证页面的响应速度,也能够保证不浪费用户流量,最重要的是用户体验感会增强很多。
所以还是后端直接返回视频首帧的图片,最理想
后端返回视频首帧图片
但是技术评审时,后端就需要排查,如果上传视频的时候,没有保存视频首帧图片,那他怎么去拿这个数据返回呢?
其实上传文件,现在最流行的不是oss就是cos
OSS:阿里云存储
COS:腾讯云存储
oss获取视频首帧
https://help.aliyun.com/document_detail/64555.html
阿里云获取视频首帧很简单,直接在url后面拼接参数就行了
这样都不需要后端怎么处理,前端这样就能够拼接
参数 描述 取值范围
t 指定截图时间。如果设置的截图时间t超过了视频时长,则返回视频的最后一帧关键帧。 [0,视频时长]
单位:ms
w 指定截图宽度,如果指定为0,则自动计算。 [0,视频宽度]
单位:像素(px)
h 指定截图高度,如果指定为0,则自动计算;如果w和h都为0,则输出为原视频宽高。 [0,视频高度]
单位:像素(px)
m 指定截图模式,不指定则为默认模式,根据时间精确截图。如果指定为fast,则截取该时间点之前的最近的一个关键帧。 枚举值:fast
f 指定输出图片的格式。 枚举值:jpg和png
// oss获取视频的第一帧
getVideoCover(url, time, format, width, height) {
const image = `${url}?x-oss-process=video/snapshot,t_${time},f_${format},w_${width},h_${height}`;
return image;
},
cos获取视频首帧
https://cloud.tencent.com/document/product/460/47505
发现需要收费,还有用量限制
cos是提供了接口,还没有oss这么方便
这里又有两种情况,一种是后端去请求cos接口,这样能够一起返回给前端页面
一种是前端去请求接口,但是如果在视频很多的情况下,会导致这个页面要请求很多接口
个人建议:还是后端去请求接口更可靠
好了,今天的分享到这里就结束了,如果大家有什么好的方法能够解决这个问题,也欢迎大家在评论区参与讨论~