说在前面
🎈不知道大家平时在逛B站的时候有没有发现这么一个功能?在视频封面移入鼠标时我们可以对视频进行预览,预览过后再决定时候要点进去观看视频,那么这个实现具体是怎么实现的呢?让我们一起动手来试一下吧。
效果预览
B站效果
组件效果
体验地址
组件实现
组件设计
我们首先应该要对组件进行一个简单的设计。
主要的逻辑如上图☝️☝️☝️,可以拆分成这么几个步骤:
1、视频截取关键帧
我们可以先将视频各个时间的关键帧截图保存,具体截取帧数可以使用传入参数控制。
2、鼠标移入封面时显示对应关键帧
在鼠标移入的时候我们应该要计算当前鼠标位置和视频宽度的比例关系,然后从视频帧列表中获取到对应的图片作为当前的视频封面图片。
3、视频和封面的状态切换
这里我们是将用两个元素分别作为视频和封面,所以我们状态切换的时候需要控制两个元素的显示和隐藏。
- 点击封面
显示并播放视频,隐藏封面。
- 暂停播放
显示封面,隐藏视频。
功能实现
分析完组件的关键步骤之后我们便可以开始动手来实现相应的功能了。
1、视频截取关键帧图片列表
1.1 截取指定帧
视频关键帧的截取我们可以使用canvas
来实现,具体实现方法如下:
/** * @param {element} video * @param {number} currentTime * @return {void} */ cutCover(video, currentTime) { video.currentTime = currentTime; const canvas = document.createElement("canvas"); let ctx = canvas.getContext("2d"); canvas.width = parseInt(this.width); canvas.height = parseInt(this.height); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const img = canvas.toDataURL("image/png"); return img; },
通过该函数我们可以获取指定时间的视频图片帧。
- 传入参数
/** * @param {element} video * @param {number} currentTime */
video为需要截取视频的dom元素,currentTime为要截取图片帧的时间点。
- 返回参数
/** * @return {Base64} img */
返回参数为截取的指定帧的Base64格式的图片。
1.2 截取stepNums张关键帧图片
stepNums为我们传入的组件参数,及需要截取的封面关键帧图片数量,数量越多,预览的效果越连贯,可以根据视频长度来调整截取的张数。
init(){ const videoContentShow = document.getElementById( this.uid + "-video" ); videoContentShow.style.height = this.height; videoContentShow.style.width = this.width; const videoContent = videoContentShow.cloneNode(); videoContent.addEventListener("canplay", () => { if (this.currentTime < this.duration) this.cut(videoContent); else this.progressValue = 0; }); } cut(video) { const duration = video.duration; this.duration = duration; this.currentTime += duration / this.stepNums; const img = this.cutCover(video, this.currentTime); this.imgList.push(img); if (this.imgList.length == 2) { this.coverSrc = img; const coverImg = document.getElementById( this.uid + "-coverImg" ); coverImg.setAttribute("src", img); } }
具体代码如上,首先我们应该先要获取到视频的dom元素,但是要注意:我们不在原始视频元素
上进行截取操作,我们这里使用了cloneNode()
来克隆一个dom元素进行操作。因为在进行截取的时候我们需要对视频的currentTime属性进行一个修改,也就是改变视频的播放进度,如果在原视频上截取的话,在未截取完成前播放视频会导致视频播放进度混乱,所以这里我们在克隆元素对象上进行操作。
我们总共需要截取stepNums张图片,所以每次截取的时间间隔应该为:duration / this.stepNums
,即视频总时间长度/截取图片张数
,循环截取即可。
2、鼠标移入封面时显示对应关键帧
鼠标移入封面的时候我们需要对封面图片进行切换。
2.1 鼠标移动事件监听
<img :id="uid + '-coverImg'" :src="coverSrc" class="j-coverImg" @mousemove="imgHover" @mouseleave="hoverOut" @click="coverClick" />
这里我们使用vue中的mousemove
和mouseleave
对鼠标事件进行监听。
imgHover(e) { const coverImg = document.getElementById(this.uid + "-coverImg"); const w = coverImg.offsetWidth / this.stepNums; const x = e.offsetX - coverImg.offsetLeft; const index = Math.min( Math.max(Math.ceil(x / w), 1), this.stepNums ); if (this.imgList.length < index) return; this.progressValue = index; coverImg.setAttribute( "src", this.imgList[Math.min(this.imgList.length - 1, index)] ); },
鼠标移入的时候我们需要根据鼠标的坐标位置来计算展示的帧数下标,具体计算如下:
- 每张图片展示的区间大小
const w = coverImg.offsetWidth / this.stepNums;
每个区间的大小我们只需要将封面的宽度除于图片帧列表的数量即可得到每张图片展示的区间大小。
- 当前鼠标所在区间
const x = e.offsetX - coverImg.offsetLeft; const index = Math.min( Math.max(Math.ceil(x / w), 1), this.stepNums );
首先我们应该要计算当前鼠标在封面里的相对位置,这里我们只需要其横坐标x即可,然后将坐标除于区间大小,我们即可得到当前坐标所对应的区间下标。这里的最大值应该进行限制为1和stepNums。
2.2 鼠标移出事件监听
鼠标移出的时候我们需要将封面恢复成当前视频的封面。
hoverOut(e) { const coverImg = document.getElementById(this.uid + "-coverImg"); const step = this.duration / this.stepNums; const index = Math.ceil(this.pauseTime / step); this.progressValue = index; coverImg.setAttribute("src", this.pauseCover || this.coverSrc); },
3、视频和封面的状态切换
封面和视频的显示隐藏需要根据播放状态来进行对应的切换。
3.1 播放视频
点击封面的时候播放视频,需要隐藏封面及相关的进度条并显示视频
doHide(hide = false) { const videoContent = document.getElementById(this.uid + "-video"); videoContent.style.display = hide ? "block" : "none"; videoContent.currentTime = this.pauseTime; hide ? videoContent.play() : videoContent.pause(); const img = document.getElementById(this.uid + "-coverImg"); img.style.display = hide ? "none" : "block"; const progress = document.getElementById(this.uid + "-progress"); progress.style.display = hide ? "none" : "block"; const progress1 = document.getElementById(this.uid + "-progress1"); progress1.style.display = hide ? "none" : "block"; }, coverClick() { this.doHide(true); },
3.2 视频暂停
视频暂停时我们需要隐藏视频,截取当前帧作为封面并显示封面及相关的进度条。
videoContentShow.addEventListener("pause", e => { this.pauseTime = videoContentShow.currentTime; this.pauseCover = this.cutCover( videoContentShow, videoContentShow.currentTime ); coverImg.setAttribute("src", this.pauseCover); const step = this.duration / this.stepNums; const index = Math.ceil(this.pauseTime / step); this.progressValue = index; setTimeout(() => { if (videoContentShow.paused) this.doHide(); }, 200); });
这里我使用了一个setTimeout来进行一个延时控制,大家知道为什么吗?因为视频有两种操作会触发视频的pause事件:
- 点击暂停按钮
- 拉动进度条
这里拉动进度条的时候会触发视频的pause事件并且马上继续播放,所以我们应该要过滤掉这一情况。
组件使用
<template> <div class="content"> <div class="video-list"> <j-video-cover class="video" :videoUrl="videoUrl" stepNums="40" ></j-video-cover> </div> </div> </template> <script> export default { data() { return { videoUrl: require("../../assets/video/202112250058.mp4"), } } } </script>
组件库引用
这里我将这个组件打包进了自己的一个组件库,并将其发布到了npm上,有需要的同学也可以直接引入该组件进行使用。
引入教程可以看这里:http://jyeontu.xyz/jvuewheel/#/installView
引入后即可直接使用。
源码地址
组件库已开源,想要查看完整源码的可以到 gitee 查看,自己也整理了相关的文档对其进行了简单介绍,具体如下:
组件文档
jvuewheel: jyeontu.xyz/jvuewheel/#…
Gitee源码
Gitee源码:gitee.com/zheng_yongt…
觉得有帮助的同学可以帮忙给我点个star,感激不尽~~~
有什么想法或者改良可以给我提个pr,十分欢迎~~~
有什么问题都可以在评论告诉我~~~
往期精彩
说在后面
🎉这里是JYeontu,喜欢算法,GDCPC打过卡;热爱羽毛球,大运会打过酱油。毕业一年,两年前端开发经验,目前担任H5前端开发,算法业余爱好者,有空会刷刷算法题,平时喜欢打打羽毛球🏸 ,也喜欢写些东西,既为自己记录📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解🙇,写错的地方望指出,定会认真改进😊,在此谢谢大家的支持,我们下文再见🙌。