说到在项目中引入一个视频,咱们确定会想到 HTML5 为咱们提供的 Video 标签,它为咱们提供了许多属性和方法,使用起来很方便,固然直接使用也会遇到各类兼容问题,在最初学习 Video
标签时,W3C 官网就给出了这样的舒适提示:javascript
在 HTML 中播放视频并不容易!html
您须要谙熟大量技巧,以确保您的视频文件在全部浏览器中(Internet Explorer, Chrome, Firefox, Safari, Opera)和全部硬件上(PC, Mac , iPad, iPhone)都可以播放。java
这份提示在以后接触了一系列视频项目后,才明白这“不容易”指的是什么,在移动端,咱们须要深谙的“大量技巧”却还远远不够......web
NutUI 是一套京东风格的移动端组件库,开发和服务于移动 Web 界面的企业级前中后台产品。现拥有 50+ 个高质量组件,GitHub 上已得到 1.9k 的 star,NPM 下载量超过 14k。公司内部已赋能支持 50+ 个项目,外部接入使用项目达到 20+ 个。感兴趣的同窗,快来扫码体验吧!canvas
言归正传,距离 NutUI v2.2.2 版本 Video 视频组件发布已有一段时间了,在 NutUI 交流群和 GitHub 上咱们也收到了一些用户反馈,在这里想跟你们聊聊 NutUI Video 组件的开发、使用以及遇到的问题和解决方案。浏览器
首先,开发 Vue 视频组件这个想法是源于一个移动端的项目。项目需求相对简单,使用的是 Vue 技术栈,只有一个视频须要点击播放,因此在最初选择实现视频播放的时候没有考虑要引入第三方的插件。而在项目开发之初调研 Vue 的 Video 视频组件时,发现 NutUI 组件库尚未视频组件,这怎么能忍呢?因而乎 NutUI Video 组件就这么诞生了!缓存
在开发以前,咱们先来再次认识下 video
标签。相信初识 <video>
标签时,不少人都是先掌握了使用方法,好比在页面中添加一个 Video 标签,再加一个视频地址。微信
<video controls width="250">
<source src="videoname.webm" type="video/webm">
<source src="videoname.mp4" type="video/mp4">
您的浏览器不支持 video 标签。
</video>
复制代码
当视频能在页面中能顺利播放以后,才开始关注它的属性和参数的使用。异步
<video src="videofile.ogg" autoplay muted poster="posterimage.jpg">
抱歉,您的浏览器不支持内嵌视频
</video>
复制代码
好比上述代码中对视频的播放地址 src
、自动播放属性 autoplay
、静音属性 muted
和海报设置属性 poster
进行了设置。ide
除了基本的可选属性,Video 标签还支持 HTML 中的全局属性和事件属性。
当咱们在 HTML 中建立了一个视频后,就能够拿到 Video 标签的对象属性和对象方法,好比
currentTime
视频当前播放位置(即当前播放时间,以秒计)duration
视频的长度(整个视频的播放长度,以秒计)ended
视频是否播放结束volume
视频的播放音量等对象属性还有一些对象方法:
canPlayType()
检查浏览器是否可以播放指定的视频类型load()
从新加载视频元素play()
开始播放视频pause()
暂停当前播放的视频等这些对象方法。感兴趣的同窗能够查阅 W3C等相关文档中,这里就不一一赘述了。
经过对 Video 标签的重温,在 Vue 中要实现视频的播放(仅指播放)能够说很简单,但要想“通关”移动端全部的“隐藏关卡”,那能够说是不可能完成的任务。由于即便流传度很广的 Video.js、Vue-video-player 也存在不少待解决的问题,咱们只能具体问题具体分析。因此在实现 NutUI Video 组件这件事上,咱们分为两个阶段:
第一阶段是基础的实现,完成视频播放的基本功能。第二阶段是进阶版的自定义控制栏的实现,完成播放、暂停、控制条等操做项的自定义开发。
一、属性的实现
对于属性的实现,最开始想用一一对应将属性绑定后抛给用户使用,用户操做的就是 video
标签的原生属性。但考虑到后期自定义控制栏的迭代,这种方法可能不利于管理,因而咱们仍是将 Video 的操做属性用 options
对象统一管理,而视频源则用 source
属性管理,用集合形式管理视频源信息,可支持多种格式的视频源的配置,以便解决不一样设备视频格式的兼容问题。
<video ref="video" class="nut-videoplayer" :muted="options.muted" :autoplay="options.autoplay" :loop="options.loop" :poster="options.poster" :controls="options.controls" :preload="options.preload" >
<source v-for="source in sources" :src="source.src" :type="source.type" :key="source.src" />
</video>
复制代码
到了这一步用户调用组件,配置好参数就能正常播放视频了。
<nut-video :sources="sources" :options="options"></nut-video>
data() {
return {
sources: [{ src: 'video.mp4', type: 'video/mp4'}],
options: {
controls: true,
autoplay: true,
volume: 0.6,
poster: ''
},
}
}
复制代码
效果演示
二、自定义属性
除了 Video 的基本属性,在基础版组件中咱们也为用户抛出了一些个性化的属性设置如:disabled 禁止操做,playsinline 行内展现等。
options: {autoplay: true, muted: true,disabled: true, playsinline: true, loop: true,controls: false}
复制代码
上述配置项规定了一个行内自动播放的背景图视频的例子,须要注意的是禁用操做目前只对自动播放时有效,在自动播放中用户不可操做播放器,点击播放器无效。而行内展现 playsinline 属性,目前只有IOS端和个别安卓设备能兼容,要想彻底实现行内播放仍是要具体问题具体分析。
效果演示:
三、事件的实现
在事件实现这方面,视频最重要的操做无非是播放、暂停、播放结束这三个事件,还有 error 事件,在报错时提示错误信息,效果以下。
当咱们使用 video
的原生控制栏时,要想实现播放、暂停和播放结束,主要就靠监听 video 的播放事件了。
//监听播放
this.videoElm.addEventListener('play', () => {
this.state.playing = true;
this.$emit('play', this.videoElm);
});
//监听暂停
this.videoElm.addEventListener('pause', () => {
this.state.playing = false;
this.$emit('pause', this.videoElm);
});
//监听播放结束
this.videoElm.addEventListener('ended',this.playEnded);
复制代码
用户调用方法以下
<nut-video :sources="sources" :options="options" @play="play" @pause="pause" @playend="playend">
</nut-video> methods: { play(elm) {console.log('play', elm);}, pause(e) {console.log('pause');}, playend(e) {alert('播放结束');}, } 复制代码
效果演示:
从视频中能够看到,当我点击播放、暂停和播放结束时会触发回调事件,当视频播放结束后会提示播放结束。
若是说基础版是依赖原生 Video 的控制栏,那么自定义控件的实现就是掌握播放自主权的进阶版。由于 Video
标签在不一样设备上都会有不一样的默认设定,咱们很难控制它们,因此自定义一套本身的视频播放控件,能够必定程度上避免原生控件被默认修改的问题。下面,咱们来看看它的实现。
一、控制条的重构
关于重构控制条咱们能够先来看张图,分析下自定义控制条须要的元素。
上图标注了控制栏须要的元素:
按照上述控制栏的元素进行重构便可,这里就很少作赘述,直接上代码。
<div class="nut-video-controller" >
<div class="control-play-btn" @click="play"></div> <!-- 播放暂停 -->
<div class="current-time">01:30</div> <!-- 当前播放时间 -->
<div class="progress-container"> <!-- 播放控制条 -->
<div class="progress" ref="progressBar"> <!-- 整体播放时间条 -->
<div class="buffered" ></div> <!-- 缓冲时间条 -->
<!-- 可拖动播放按钮 -->
<div class="video-ball" @touchmove.stop.prevent="touchSlidMove($event)" @touchstart.stop="touchSlidSrart($event)" @touchend.stop="touchSlidEnd($event)">
<div class="move-handle"></div>
</div>
<div class="played" ref="playedBar"></div>
</div>
</div>
<div class="duration-time">03:30</div> <!-- 整体时间 -->
<div class="volume" @click="handleMuted"></div> <!-- 静音按钮 -->
<div class="fullscreen-icon" @click="fullScreen"></div> <!-- 全屏按钮 -->
</div>
复制代码
二、初始化配置
在控制栏元素重构完成后,咱们须要先获取到 Video 元素、自定义控制条元素以及用户配置的属性的初始状态。
this.videoElm = this.$el.getElementsByTagName('video')[0];
复制代码
这里咱们拿到了 video
标签,这一步很是重要,由于以后全部的操做都是基于它而成行的。
const $player = this.$el;
const $progress = this.$el.getElementsByClassName('progress')[0];
// 播放器位置
this.player.$player = $player;
this.progressBar.progressElm = $progress;
this.progressBar.pos = $progress.getBoundingClientRect();
this.videoSet.progress.width = Math.round($progress.getBoundingClientRect().width);
复制代码
代码中咱们获取到刚才重构的控制条 progressBar
并对它的位置和宽作了定义。
初始化是将用户设置的属性参数绑定到 video
上,好比自动播放设置时要触发播放事件,行内播放设置时要在 video
上绑上兼容属性等等。
//自动播放
if (this.options.autoplay) {
this.videoElm.play();
}
//行内播放
if (this.options.playsinline) {
this.videoElm.setAttribute('playsinline', this.options.playsinline);
this.videoElm.setAttribute('webkit-playsinline', this.options.playsinline);
this.videoElm.setAttribute('x5-playsinline', this.options.playsinline);
this.videoElm.setAttribute('x5-video-player-type', 'h5');
this.videoElm.setAttribute('x5-video-player-fullscreen', false);
}
复制代码
三、播放与暂停
视频的播放与暂停的在自定义控制栏中咱们统一用 play()
事件控制,在界面渲染上用 data
中的 state.playing
控制。
play() {
this.state.playing = !this.state.playing;
if (this.videoElm) {
// 播放状态
if (this.state.playing) {
try {
this.videoElm.play();
// 监听缓存进度
this.videoElm.addEventListener('progress', e => {this.getLoadTime();});
// 监听播放进度
this.videoElm.addEventListener('timeupdate', throttle(this.getPlayTime, 100, 1));
// 监听结束
this.videoElm.addEventListener('ended', this.playEnded);
this.$emit('play', this.videoElm);
} catch (e) {
this.handleError()
}
}
// 中止状态
else {
this.videoElm.pause();
this.$emit('pause', this.videoElm);
}
}
},
复制代码
当视频处于播放状态时触发 video.play()
,咱们会对缓存进度、播放进度和播放结束的状态进行监听。当视频是暂停状态时,会触发 video.pause()
暂停事件。
四、音量控制
视频的音量控制就是在获取到页面中的 Video 元素后,设置它的 volume,方法以下。
volumeHandle() {
this.videoElm.volume = this.state.vol ;
}
复制代码
五、播放时间的获取
播放时间的获取是根据 video
的 duration
和 currentTime
来的。
// 获取播放时间
getPlayTime() {
const percent = this.videoElm.currentTime / this.videoElm.duration;
this.videoSet.progress.current = Math.round(this.videoSet.progress.width * percent);
// 赋值时长
this.videoSet.totalTime = this.timeFormat(this.videoElm.duration);
this.videoSet.displayTime = this.timeFormat(this.videoElm.currentTime);
},
复制代码
经过对获取当前播放时长占总体播放时长的比值,对应到当前播放时间按钮在整个播放条的位置,实现了播放时间的显示。
六、进度条拖动控制
说到进度条,经过上边分析的控制栏布局,咱们知道它有一个可拖动的按钮,这里咱们对它的 touchmove
和 touchend
事件作处理。
// 拖动播放进度
touchSlidMove(e) {
let currentX = e.targetTouches[0].pageX;
let offsetX = currentX - this.progressBar.pos.left;
// 边界检测
if (offsetX <= 0) {
offsetX = 0;
}
if (offsetX >= this.videoSet.progress.width) {
offsetX = this.videoSet.progress.width;
}
this.videoSet.progress.current = offsetX;
let percent = this.videoSet.progress.current / this.videoSet.progress.width;
this.videoElm.duration && this.setPlayTime(percent, this.videoElm.duration);
},
touchSlidEnd(e) {
let currentX = e.changedTouches[0].pageX;
let offsetX = currentX - this.progressBar.pos.left;
this.videoSet.progress.current = offsetX;
let percent = offsetX / this.videoSet.progress.width;
this.videoElm.duration && this.setPlayTime(percent, this.videoElm.duration);
},
// 设置手动播放时间
setPlayTime(percent, totalTime) {
this.videoElm.currentTime = Math.floor(percent * totalTime);
},
复制代码
在拖动开始时获取控制条的左侧位置,并实时监听偏移量,将偏移量的值赋给 this.videoSet.progress.width
播放控制条的长度,并用百分比转化成时间,重置当前视频播放的时间。
七、全屏控制
全屏和退出全屏咱们用 data
中 state.fullScreen
来控制它的按钮状态,默认是 false
表示不全屏,当用户点击全屏按钮时,将其置成 true
并调用进入全屏事件 element.webkitRequestFullScreen()
,再次点击时调用 document.webkitCancelFullScreen()
退出全屏,并把 state.fullScreen
置成 false
来改变按钮图标的样式。
fullScreen() {
if (!this.state.fullScreen) {
this.state.fullScreen = true;
this.videoElm.webkitRequestFullScreen();
} else {
this.state.fullScreen = false;
document.webkitCancelFullScreen();
}
}
复制代码
自定义控制条演示效果:
以上是自定义控制栏的实现,固然还有其余功能待开发,以后会根据业务和用户的反馈不断的进行完善。
组件开发完了以后,终于能够在项目中跑起来了,可是随之而来的问题也接连出现了。这里为你们总结了下咱们在项目中遇到问题和解决方案。
在移动端 Video 的自动播放问题相信必定有许多人都遇到过,在 Video 标签上加上 autoplay 后 PC 浏览器测试的很好,在手机端测试就失效,这是由于 autoplay 的兼容问题。形成这些问题的缘由可能有:
- 浏览器不支持该视频格式,建议可使用 MP四、WebM、Ogg 这三种视频格式
- 出于用户体验,节省流量的考虑,移动端禁止自动播放
- 视频文件太大,加载时间过长或错误
若是必定要作自动播放的功能的话,能够参照以下方案:
一、检查视频格式是否正确,尽可能转成 MP4,压缩大小到2M如下
二、IOS 设备中 autoplay 失效,能够加上静音属性 muted:
<video autoplay muted></video>
复制代码
三、在用户有触屏操做后进行模拟播放。
let video = document.getElementById("video");
document.removeEventListener('touchstart', function(){
video.play();
});
复制代码
- 这里须要注意的是必需要等用户有操做后才能执行模拟播放,不然会有报错。
- 安卓机加载完成后进行模拟播放是无效的,用户必需要有触屏操做后才能生效,好比点击、触摸、滑动屏幕等
四、若是是微信中自动播放失效,能够考虑安装微信的 JSSDK,经过监听 WeixinJSBridgeReady
,来控制自动播放,具体操做以下。
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
document.addEventListener("WeixinJSBridgeReady", function () {
document.getElementById('video').play();
}, false);
复制代码
五、若是安卓机仍是没法作到自动播放,能够考虑降级处理,展现控制条引导用户点击播放按钮播放。
全屏播放视频时,咱们可能会遇到屏幕没有被占满,上下会有黑白边的状况,此时能够添加 style= "object-fit: fill;width:100%;height:100%;"
,控制视频撑满屏幕。
视频行内播放,也就是视频在页面局部的内嵌播放,像文档流同样在它在的位置播放。但在移动端下,Video 视频播放是默认全屏的,那要禁用全屏要如何实现呢?
一、IOS 设备能够在 Video 标签上设置 playsinline
属性,兼容写法以下:
<video muted src="video.mp4" autoplay webkit-playsinline playsinline x5-playsinline>
</video>
复制代码
以上写法在 IOS 中基本能够解决行内播放的问题,x5-playsinline
可让部分安卓机也兼容,但添加该属性后不能再有 x5-video-player-type='h5'
和 x5-video-player-fullscreen='true'
,不然还会默认全屏。
二、Canvas 模拟视频播放
安卓设备上行内播放若是上述解决方案不能知足,能够试试用 canvas 模拟视频播放,将 video
标签在页面中隐藏,经过监听播放、暂停以及播放结束事件,将视频在 canvas 中绘画出来。
initCanvas() {
//获取video
let TestVideo = document.getElementById('videoPlay');
let videoW = TestVideo.offsetWidth;
let videoH = TestVideo.offsetHeight;
//获取canvas画布
let TestCanvas = document.getElementById('videoCanvas');
//设置画布
let TestCanvas2D = TestCanvas.getContext('2d');
//设置setinterval定时器
let TestVideoTimer = null;
//监听播放
TestVideo.addEventListener('play', function() {
TestVideoTimer = setInterval(function() {
TestCanvas2D.drawImage(TestVideo,0,0,320,180);
}, 20);
}, false);
//监听暂停
TestVideo.addEventListener('pause',function() {
clearInterval(TestVideoTimer);
}, false);
//监听结束
TestVideo.addEventListener('ended', function() {
clearInterval(TestVideoTimer);
}, false);
}
复制代码
用 canvas 模拟视频虽然能够实现行内展现,但效果不是很理想,视频播放的清晰度不高,会有卡顿问题,也可能我在试验这个方法的时候用的视频源是被压缩过的缘由,画质不好,移动端控制画布大小会有一点问题,感兴趣的同窗能够尝试下用 canvas 模拟视频播放。
发版以后咱们也陆陆续续收到一些 issue 反馈,针对这些问题咱们也进行了逐一排查和修复。
issue问题以下:
一、video 组件运行控制台会报错
这个问题是由于开发自定义控件时,代码遗留未注释掉引发的,新版发布已修复该问题。
二、视频源异步切换
在基础版发布以后,NutUI 交流群里有反馈当异步切换视频源时,视频播放不了。那是由于视频地址切换时没有被监听到,在组件中加上监听事件重新加载一下就能够解决。
方法优化以下:
watch: {
sources: {
handler(newValue, oldValue) {
if (newValue && oldValue && newValue != oldValue) {
this.$nextTick(() => {
this.videoElm.load()
})
}
},
immediate: true
},
},
复制代码
该方法已经跟随新版本上线了,你们能够更新版本后体验一下。
感谢你们的反馈,也但愿你们能多提宝贵意见,帮助咱们一块儿捉虫,让这个视频组件可以走得远一点。
Video 视频组件第一版虽然已经发布,但功能也仅是基于原生 Video 标签的封装,面对移动端复杂的兼容问题,它还须要不断地打磨。对自定义控制栏的开发目前还处于试验阶段,但愿在不远的未来会有一套兼容原生和自定义的Video 组件与你们见面。若是你们对 NutUI Video 开发有什么好的建议,也欢迎留言参与 NutUI Video 的开发与设计!移动端 Video 组件的开发之路,道阻且长,咱们一步一步来~