引子
说到在项目中引入一个视频,我们肯定会想到 HTML5 为我们提供的 Video 标签,它为我们提供了许多属性和方法,使用起来很方便,当然直接使用也会遇到各种兼容问题,在最初学习 Video
标签时,W3C 官网就给出了这样的温馨提示:
在 HTML 中播放视频并不容易!您需要谙熟大量技巧,以确保您的视频文件在所有浏览器中(Internet Explorer, Chrome, Firefox, Safari, Opera)和所有硬件上(PC, Mac , iPad, iPhone)都能够播放。
这份提示在之后接触了一系列视频项目后,才明白这“不容易”指的是什么,在移动端,我们需要深谙的“大量技巧”却还远远不够......
背景
NutUI 是一套京东风格的移动端组件库,开发和服务于移动 Web 界面的企业级前中后台产品。现拥有 50+ 个高质量组件,GitHub 上已获得 1.9k 的 star,NPM 下载量超过 14k。公司内部已赋能支持 50+ 个项目,外部接入使用项目达到 20+ 个。感兴趣的同学,快来扫码体验吧!
言归正传,距离 NutUI v2.2.2 版本 Video 视频组件发布已有一段时间了,在 NutUI 交流群和 GitHub 上我们也收到了一些用户反馈,在这里想跟大家聊聊 NutUI Video 组件的开发、使用以及遇到的问题和解决方案。
首先,开发 Vue 视频组件这个想法是源于一个移动端的项目。项目需求相对简单,使用的是 Vue 技术栈,只有一个视频需要点击播放,所以在最初选择实现视频播放的时候没有考虑要引入第三方的插件。而在项目开发之初调研 Vue 的 Video 视频组件时,发现 NutUI 组件库还没有视频组件,这怎么能忍呢?于是乎 NutUI Video 组件就这么诞生了!
前期准备
在开发之前,我们先来再次认识下 video
标签。相信初识 标签时,很多人都是先掌握了使用方法,比如在页面中添加一个 Video 标签,再加一个视频地址。
当视频能在页面中能顺利播放之后,才开始关注它的属性和参数的使用。
比如上述代码中对视频的播放地址 src
、自动播放属性 autoplay
、静音属性 muted
和海报设置属性 poster
进行了设置。
除了基本的可选属性,Video 标签还支持 HTML 中的全局属性和事件属性。
当我们在 HTML 中创建了一个视频后,就可以拿到 Video 标签的对象属性和对象方法,比如
-
currentTime
视频当前播放位置(即当前播放时间,以秒计) -
duration
视频的长度(整个视频的播放长度,以秒计) -
ended
视频是否播放结束 -
volume
视频的播放音量等对象属性 - ......
还有一些对象方法:
-
canPlayType()
检查浏览器是否能够播放指定的视频类型 -
load()
重新加载视频元素 -
play()
开始播放视频 -
pause()
暂停当前播放的视频等这些对象方法。
感兴趣的同学可以查阅 W3C等相关文档中,这里就不一一赘述了。
功能实现
通过对 Video 标签的重温,在 Vue 中要实现视频的播放(仅指播放)可以说很简单,但要想“通关”移动端所有的“隐藏关卡”,那可以说是不可能完成的任务。因为即使流传度很广的 Video.js、Vue-video-player 也存在很多待解决的问题,我们只能具体问题具体分析。所以在实现 NutUI Video 组件这件事上,我们分为两个阶段:
第一阶段是基础的实现,完成视频播放的基本功能。第二阶段是进阶版的自定义控制栏的实现,完成播放、暂停、控制条等操作项的自定义开发。
基础实现
1、属性的实现
对于属性的实现,最开始想用一一对应将属性绑定后抛给用户使用,用户操作的就是 video
标签的原生属性。但考虑到后期自定义控制栏的迭代,这种方法可能不利于管理,于是我们还是将 Video 的操作属性用 options
对象统一管理,而视频源则用 source
属性管理,用集合形式管理视频源信息,可支持多种格式的视频源的配置,以便解决不同设备视频格式的兼容问题。
到了这一步用户调用组件,配置好参数就能正常播放视频了。
data() {
return {
sources: [{ src: 'video.mp4', type: 'video/mp4'}],
options: {
controls: true,
autoplay: true,
volume: 0.6,
poster: ''
},
}
}
效果演示
2、自定义属性
除了 Video 的基本属性,在基础版组件中我们也为用户抛出了一些个性化的属性设置如:disabled 禁止操作,playsinline 行内展示等。
options: {autoplay: true, muted: true,disabled: true, playsinline: true, loop: true,controls: false}
上述配置项规定了一个行内自动播放的背景图视频的例子,需要注意的是禁用操作目前只对自动播放时有效,在自动播放中用户不可操作播放器,点击播放器无效。而行内展示 playsinline 属性,目前只有IOS端和个别安卓设备能兼容,要想完全实现行内播放还是要具体问题具体分析。
效果演示:
3、事件的实现
在事件实现这方面,视频最重要的操作无非是播放、暂停、播放结束这三个事件,还有 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);
用户调用方法如下
methods: {
play(elm) {console.log('play', elm);},
pause(e) {console.log('pause');},
playend(e) {alert('播放结束');},
}
效果演示:
从视频中可以看到,当我点击播放、暂停和播放结束时会触发回调事件,当视频播放结束后会提示播放结束。
进阶版实现--自定义控件的实现
如果说基础版是依赖原生 Video 的控制栏,那么自定义控件的实现就是掌握播放自主权的进阶版。因为 Video
标签在不同设备上都会有不同的默认设定,我们很难控制它们,所以自定义一套自己的视频播放控件,可以一定程度上避免原生控件被默认修改的问题。下面,我们来看看它的实现。
1、控制条的重构
关于重构控制条我们可以先来看张图,分析下自定义控制条需要的元素。
上图标注了控制栏需要的元素:
- 播放按钮
- 当前播放时间
- 总体时间
- 播放控制条
- 缓冲时间条
- 可拖动播放按钮
- 静音控制按钮
- 全屏控制按钮
按照上述控制栏的元素进行重构即可,这里就不多做赘述,直接上代码。
01:30
03:30
2、初始化配置
在控制栏元素重构完成后,我们需要先获取到 Video 元素、自定义控制条元素以及用户配置的属性的初始状态。
- 获取 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);
}
3、播放与暂停
视频的播放与暂停的在自定义控制栏中我们统一用 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()
暂停事件。
4、音量控制
视频的音量控制就是在获取到页面中的 Video 元素后,设置它的 volume,方法如下。
volumeHandle() {
this.videoElm.volume = this.state.vol ;
}
5、播放时间的获取
播放时间的获取是根据 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);
},
通过对获取当前播放时长占整体播放时长的比值,对应到当前播放时间按钮在整个播放条的位置,实现了播放时间的显示。
6、进度条拖动控制
说到进度条,通过上边分析的控制栏布局,我们知道它有一个可拖动的按钮,这里我们对它的 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
播放控制条的长度,并用百分比转化成时间,重置当前视频播放的时间。
7、全屏控制
全屏和退出全屏我们用 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 的兼容问题。造成这些问题的原因可能有:
- 浏览器不支持该视频格式,建议可以使用 MP4、WebM、Ogg 这三种视频格式
- 出于用户体验,节省流量的考虑,移动端禁止自动播放
- 视频文件太大,加载时间过长或错误
如果一定要做自动播放的功能的话,可以参照如下方案:
1、检查视频格式是否正确,尽量转成 MP4,压缩大小到2M以下
2、IOS 设备中 autoplay 失效,可以加上静音属性 muted:
3、在用户有触屏操作后进行模拟播放。
let video = document.getElementById("video");
document.removeEventListener('touchstart', function(){
video.play();
});
- 这里需要注意的是必须要等用户有操作后才能执行模拟播放,否则会有报错。
- 安卓机加载完成后进行模拟播放是无效的,用户必须要有触屏操作后才能生效,比如点击、触摸、滑动屏幕等
4、如果是微信中自动播放失效,可以考虑安装微信的 JSSDK,通过监听 WeixinJSBridgeReady
,来控制自动播放,具体操作如下。
document.addEventListener("WeixinJSBridgeReady", function () {
document.getElementById('video').play();
}, false);
5、如果安卓机还是无法做到自动播放,可以考虑降级处理,展示控制条引导用户点击播放按钮播放。
全屏播放问题
全屏播放视频时,我们可能会遇到屏幕没有被占满,上下会有黑白边的情况,此时可以添加 style= "object-fit: fill;width:100%;height:100%;"
,控制视频撑满屏幕。
行内播放
视频行内播放,也就是视频在页面局部的内嵌播放,像文档流一样在它在的位置播放。但在移动端下,Video 视频播放是默认全屏的,那要禁用全屏要如何实现呢?
1、IOS 设备可以在 Video 标签上设置 playsinline
属性,兼容写法如下:
以上写法在 IOS 中基本可以解决行内播放的问题,x5-playsinline
可以让部分安卓机也兼容,但添加该属性后不能再有 x5-video-player-type='h5'
和 x5-video-player-fullscreen='true'
,否则还会默认全屏。
2、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 反馈,针对这些问题我们也进行了逐一排查和修复。
issue问题如下:
1、video 组件运行控制台会报错
这个问题是因为开发自定义控件时,代码遗留未注释掉引起的,新版发布已修复该问题。
2、视频源异步切换
在基础版发布之后,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 组件的开发之路,道阻且长,我们一步一步来~