查看原文
其他

【第1969期】NutUI 视频组件开发心得

叶文文 前端早读课 2020-09-30

前言

今日早读文章由京东用户体验设计部@叶文文投稿分享。

京东用户体验设计部-前端开发部现有前端开发人员 50 左右,主要为京东零售集团、京东健康提供 WEB 前端开发、APP RN开发,小程序开发、小游戏开发、H5开发等能力支持。

正文从这开始~~

引子

说到在项目中引入一个视频,我们肯定会想到 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> 标签时,很多人都是先掌握了使用方法,比如在页面中添加一个 Video 标签,再加一个视频地址。

  1. <video controls width="250">

  2. <source src="videoname.webm" type="video/webm">

  3. <source src="videoname.mp4" type="video/mp4">

  4. 您的浏览器不支持 video 标签。

  5. </video>

当视频能在页面中能顺利播放之后,才开始关注它的属性和参数的使用。

  1. <video src="videofile.ogg" autoplay muted poster="posterimage.jpg">

  2. 抱歉,您的浏览器不支持内嵌视频

  3. </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 属性管理,用集合形式管理视频源信息,可支持多种格式的视频源的配置,以便解决不同设备视频格式的兼容问题。

  1. <video ref="video" class="nut-videoplayer"

  2. :muted="options.muted"

  3. :autoplay="options.autoplay"

  4. :loop="options.loop"

  5. :poster="options.poster"

  6. :controls="options.controls"

  7. :preload="options.preload"

  8. >

  9. <source v-for="source in sources" :src="source.src" :type="source.type" :key="source.src" />

  10. </video>

到了这一步用户调用组件,配置好参数就能正常播放视频了。

  1. <nut-video :sources="sources" :options="options"></nut-video>

  2. data() {

  3. return {

  4. sources: [{ src: 'video.mp4', type: 'video/mp4'}],

  5. options: {

  6. controls: true,

  7. autoplay: true,

  8. volume: 0.6,

  9. poster: ''

  10. },

  11. }

  12. }

效果演示

2、自定义属性

除了 Video 的基本属性,在基础版组件中我们也为用户抛出了一些个性化的属性设置如:disabled 禁止操作,playsinline 行内展示等。

  1. options: {autoplay: true, muted: true,disabled: true, playsinline: true, loop: true,controls: false}

上述配置项规定了一个行内自动播放的背景图视频的例子,需要注意的是禁用操作目前只对自动播放时有效,在自动播放中用户不可操作播放器,点击播放器无效。而行内展示 playsinline 属性,目前只有IOS端和个别安卓设备能兼容,要想完全实现行内播放还是要具体问题具体分析。

效果演示:

3、事件的实现

在事件实现这方面,视频最重要的操作无非是播放、暂停、播放结束这三个事件,还有 error 事件,在报错时提示错误信息,效果如下。

当我们使用 video 的原生控制栏时,要想实现播放、暂停和播放结束,主要就靠监听 video 的播放事件了。

  1. //监听播放

  2. this.videoElm.addEventListener('play', () => {

  3. this.state.playing = true;

  4. this.$emit('play', this.videoElm);

  5. });

  6. //监听暂停

  7. this.videoElm.addEventListener('pause', () => {

  8. this.state.playing = false;

  9. this.$emit('pause', this.videoElm);

  10. });

  11. //监听播放结束

  12. this.videoElm.addEventListener('ended',this.playEnded);

用户调用方法如下

  1. <nut-video :sources="sources" :options="options" @play="play" @pause="pause" @playend="playend">

  2. </nut-video>

  3. methods: {

  4. play(elm) {console.log('play', elm);},

  5. pause(e) {console.log('pause');},

  6. playend(e) {alert('播放结束');},

  7. }

效果演示:

从视频中可以看到,当我点击播放、暂停和播放结束时会触发回调事件,当视频播放结束后会提示播放结束。

进阶版实现--自定义控件的实现

如果说基础版是依赖原生 Video 的控制栏,那么自定义控件的实现就是掌握播放自主权的进阶版。因为 Video 标签在不同设备上都会有不同的默认设定,我们很难控制它们,所以自定义一套自己的视频播放控件,可以一定程度上避免原生控件被默认修改的问题。下面,我们来看看它的实现。

1、控制条的重构

关于重构控制条我们可以先来看张图,分析下自定义控制条需要的元素。

上图标注了控制栏需要的元素:

  • 播放按钮

  • 当前播放时间

  • 总体时间

  • 播放控制条

  • 缓冲时间条

  • 可拖动播放按钮

  • 静音控制按钮

  • 全屏控制按钮

按照上述控制栏的元素进行重构即可,这里就不多做赘述,直接上代码。

  1. <div class="nut-video-controller" >

  2. <div class="control-play-btn" @click="play"></div> <!-- 播放暂停 -->

  3. <div class="current-time">01:30</div> <!-- 当前播放时间 -->

  4. <div class="progress-container"> <!-- 播放控制条 -->

  5. <div class="progress" ref="progressBar"> <!-- 总体播放时间条 -->

  6. <div class="buffered" ></div> <!-- 缓冲时间条 -->

  7. <!-- 可拖动播放按钮 -->

  8. <div class="video-ball"

  9. @touchmove.stop.prevent="touchSlidMove($event)"

  10. @touchstart.stop="touchSlidSrart($event)"

  11. @touchend.stop="touchSlidEnd($event)">

  12. <div class="move-handle"></div>

  13. </div>

  14. <div class="played" ref="playedBar"></div>

  15. </div>

  16. </div>

  17. <div class="duration-time">03:30</div> <!-- 总体时间 -->

  18. <div class="volume" @click="handleMuted"></div> <!-- 静音按钮 -->

  19. <div class="fullscreen-icon" @click="fullScreen"></div> <!-- 全屏按钮 -->

  20. </div>

2、初始化配置

在控制栏元素重构完成后,我们需要先获取到 Video 元素、自定义控制条元素以及用户配置的属性的初始状态。

  • 获取 Video 标签

  1. this.videoElm = this.$el.getElementsByTagName('video')[0];

这里我们拿到了 video 标签,这一步非常重要,因为之后所有的操作都是基于它而成行的。

  • 获取自定义控制条位置

  1. const $player = this.$el;

  2. const $progress = this.$el.getElementsByClassName('progress')[0];

  3. // 播放器位置

  4. this.player.$player = $player;

  5. this.progressBar.progressElm = $progress;

  6. this.progressBar.pos = $progress.getBoundingClientRect();

  7. this.videoSet.progress.width = Math.round($progress.getBoundingClientRect().width);

代码中我们获取到刚才重构的控制条 progressBar 并对它的位置和宽做了定义。

  • 初始化属性配置

初始化是将用户设置的属性参数绑定到 video 上,比如自动播放设置时要触发播放事件,行内播放设置时要在 video 上绑上兼容属性等等。

  1. //自动播放

  2. if (this.options.autoplay) {

  3. this.videoElm.play();

  4. }

  5. //行内播放

  6. if (this.options.playsinline) {

  7. this.videoElm.setAttribute('playsinline', this.options.playsinline);

  8. this.videoElm.setAttribute('webkit-playsinline', this.options.playsinline);

  9. this.videoElm.setAttribute('x5-playsinline', this.options.playsinline);

  10. this.videoElm.setAttribute('x5-video-player-type', 'h5');

  11. this.videoElm.setAttribute('x5-video-player-fullscreen', false);

  12. }

3、播放与暂停

视频的播放与暂停的在自定义控制栏中我们统一用 play() 事件控制,在界面渲染上用 data 中的 state.playing 控制。

  1. play() {

  2. this.state.playing = !this.state.playing;

  3. if (this.videoElm) {

  4. // 播放状态

  5. if (this.state.playing) {

  6. try {

  7. this.videoElm.play();

  8. // 监听缓存进度

  9. this.videoElm.addEventListener('progress', e => {this.getLoadTime();});

  10. // 监听播放进度

  11. this.videoElm.addEventListener('timeupdate', throttle(this.getPlayTime, 100, 1));

  12. // 监听结束

  13. this.videoElm.addEventListener('ended', this.playEnded);

  14. this.$emit('play', this.videoElm);

  15. } catch (e) {

  16. this.handleError()

  17. }

  18. }

  19. // 停止状态

  20. else {

  21. this.videoElm.pause();

  22. this.$emit('pause', this.videoElm);

  23. }

  24. }

  25. },

当视频处于播放状态时触发 video.play(),我们会对缓存进度、播放进度和播放结束的状态进行监听。当视频是暂停状态时,会触发 video.pause() 暂停事件。

4、音量控制

视频的音量控制就是在获取到页面中的 Video 元素后,设置它的 volume,方法如下。

  1. volumeHandle() {

  2. this.videoElm.volume = this.state.vol ;

  3. }

5、播放时间的获取

播放时间的获取是根据 videodurationcurrentTime 来的。

  1. // 获取播放时间

  2. getPlayTime() {

  3. const percent = this.videoElm.currentTime / this.videoElm.duration;

  4. this.videoSet.progress.current = Math.round(this.videoSet.progress.width * percent);


  5. // 赋值时长

  6. this.videoSet.totalTime = this.timeFormat(this.videoElm.duration);

  7. this.videoSet.displayTime = this.timeFormat(this.videoElm.currentTime);

  8. },

通过对获取当前播放时长占整体播放时长的比值,对应到当前播放时间按钮在整个播放条的位置,实现了播放时间的显示。

6、进度条拖动控制

说到进度条,通过上边分析的控制栏布局,我们知道它有一个可拖动的按钮,这里我们对它的 touchmovetouchend 事件做处理。

  1. // 拖动播放进度

  2. touchSlidMove(e) {

  3. let currentX = e.targetTouches[0].pageX;

  4. let offsetX = currentX - this.progressBar.pos.left;

  5. // 边界检测

  6. if (offsetX <= 0) {

  7. offsetX = 0;

  8. }

  9. if (offsetX >= this.videoSet.progress.width) {

  10. offsetX = this.videoSet.progress.width;

  11. }

  12. this.videoSet.progress.current = offsetX;

  13. let percent = this.videoSet.progress.current / this.videoSet.progress.width;

  14. this.videoElm.duration && this.setPlayTime(percent, this.videoElm.duration);


  15. },

  16. touchSlidEnd(e) {

  17. let currentX = e.changedTouches[0].pageX;

  18. let offsetX = currentX - this.progressBar.pos.left;

  19. this.videoSet.progress.current = offsetX;

  20. let percent = offsetX / this.videoSet.progress.width;

  21. this.videoElm.duration && this.setPlayTime(percent, this.videoElm.duration);

  22. },

  23. // 设置手动播放时间

  24. setPlayTime(percent, totalTime) {

  25. this.videoElm.currentTime = Math.floor(percent * totalTime);

  26. },

在拖动开始时获取控制条的左侧位置,并实时监听偏移量,将偏移量的值赋给 this.videoSet.progress.width 播放控制条的长度,并用百分比转化成时间,重置当前视频播放的时间。

7、全屏控制

全屏和退出全屏我们用 datastate.fullScreen 来控制它的按钮状态,默认是 false 表示不全屏,当用户点击全屏按钮时,将其置成 true 并调用进入全屏事件 element.webkitRequestFullScreen() ,再次点击时调用 document.webkitCancelFullScreen() 退出全屏,并把 state.fullScreen 置成 false 来改变按钮图标的样式。

  1. fullScreen() {

  2. if (!this.state.fullScreen) {

  3. this.state.fullScreen = true;

  4. this.videoElm.webkitRequestFullScreen();

  5. } else {

  6. this.state.fullScreen = false;

  7. document.webkitCancelFullScreen();

  8. }

  9. }

自定义控制条演示效果:

以上是自定义控制栏的实现,当然还有其他功能待开发,之后会根据业务和用户的反馈不断的进行完善。

问题&解决方案

组件开发完了之后,终于可以在项目中跑起来了,但是随之而来的问题也接连出现了。这里为大家总结了下我们在项目中遇到问题和解决方案。

自动播放问题

在移动端 Video 的自动播放问题相信一定有许多人都遇到过,在 Video 标签上加上 autoplay 后 PC 浏览器测试的很好,在手机端测试就失效,这是因为 autoplay 的兼容问题。造成这些问题的原因可能有:

浏览器不支持该视频格式,建议可以使用 MP4、WebM、Ogg 这三种视频格式 出于用户体验,节省流量的考虑,移动端禁止自动播放 视频文件太大,加载时间过长或错误

如果一定要做自动播放的功能的话,可以参照如下方案:

1、检查视频格式是否正确,尽量转成 MP4,压缩大小到2M以下

2、IOS 设备中 autoplay 失效,可以加上静音属性 muted:

  1. <video autoplay muted></video>

3、在用户有触屏操作后进行模拟播放。

  1. let video = document.getElementById("video");

  2. document.addEventListener('touchstart', function(){

  3. video.play();

  4. });

这里需要注意的是必须要等用户有操作后才能执行模拟播放,否则会有报错。 安卓机加载完成后进行模拟播放是无效的,用户必须要有触屏操作后才能生效,比如点击、触摸、滑动屏幕等

4、如果是微信中自动播放失效,可以考虑安装微信的 JSSDK,通过监听 WeixinJSBridgeReady,来控制自动播放,具体操作如下。

  1. <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>


  2. document.addEventListener("WeixinJSBridgeReady", function () {

  3. document.getElementById('video').play();

  4. }, false);

5、如果安卓机还是无法做到自动播放,可以考虑降级处理,展示控制条引导用户点击播放按钮播放。

全屏播放问题

全屏播放视频时,我们可能会遇到屏幕没有被占满,上下会有黑白边的情况,此时可以添加 style="object-fit: fill;width:100%;height:100%;",控制视频撑满屏幕。

行内播放

视频行内播放,也就是视频在页面局部的内嵌播放,像文档流一样在它在的位置播放。但在移动端下,Video 视频播放是默认全屏的,那要禁用全屏要如何实现呢?

1、IOS 设备可以在 Video 标签上设置 playsinline 属性,兼容写法如下:

  1. <video muted src="video.mp4" autoplay

  2. webkit-playsinline

  3. playsinline

  4. x5-playsinline>

  5. </video>

以上写法在 IOS 中基本可以解决行内播放的问题, x5-playsinline 可以让部分安卓机也兼容,但添加该属性后不能再有 x5-video-player-type='h5'x5-video-player-fullscreen='true',否则还会默认全屏。

2、Canvas 模拟视频播放

安卓设备上行内播放如果上述解决方案不能满足,可以试试用 canvas 模拟视频播放,将 video 标签在页面中隐藏,通过监听播放、暂停以及播放结束事件,将视频在 canvas 中绘画出来。

  1. initCanvas() {

  2. //获取video

  3. let TestVideo = document.getElementById('videoPlay');

  4. let videoW = TestVideo.offsetWidth;

  5. let videoH = TestVideo.offsetHeight;

  6. //获取canvas画布

  7. let TestCanvas = document.getElementById('videoCanvas');

  8. //设置画布

  9. let TestCanvas2D = TestCanvas.getContext('2d');

  10. //设置setinterval定时器

  11. let TestVideoTimer = null;

  12. //监听播放

  13. TestVideo.addEventListener('play', function() {

  14. TestVideoTimer = setInterval(function() {

  15. TestCanvas2D.drawImage(TestVideo,0,0,320,180);

  16. }, 20);

  17. }, false);

  18. //监听暂停

  19. TestVideo.addEventListener('pause',function() {

  20. clearInterval(TestVideoTimer);

  21. }, false);

  22. //监听结束

  23. TestVideo.addEventListener('ended', function() {

  24. clearInterval(TestVideoTimer);

  25. }, false);

  26. }

用 canvas 模拟视频虽然可以实现行内展示,但效果不是很理想,视频播放的清晰度不高,会有卡顿问题,也可能我在试验这个方法的时候用的视频源是被压缩过的原因,画质很差,移动端控制画布大小会有一点问题,感兴趣的同学可以尝试下用 canvas 模拟视频播放。

issue问题

发版之后我们也陆陆续续收到一些 issue 反馈,针对这些问题我们也进行了逐一排查和修复。

issue问题如下:

1、video 组件运行控制台会报错

这个问题是因为开发自定义控件时,代码遗留未注释掉引起的,新版发布已修复该问题。

2、视频源异步切换

在基础版发布之后,NutUI 交流群里有反馈当异步切换视频源时,视频播放不了。那是因为视频地址切换时没有被监听到,在组件中加上监听事件从新加载一下就可以解决。

方法优化如下:

  1. watch: {

  2. sources: {

  3. handler(newValue, oldValue) {

  4. if (newValue && oldValue && newValue != oldValue) {

  5. this.$nextTick(() => {

  6. this.videoElm.load()

  7. })

  8. }

  9. },

  10. immediate: true

  11. },

  12. },

该方法已经跟随新版本上线了,大家可以更新版本后体验一下。

感谢大家的反馈,也希望大家能多提宝贵意见,帮助我们一起捉虫,让这个视频组件能够走得远一点。

总结

Video 视频组件初版虽然已经发布,但功能也仅是基于原生 Video 标签的封装,面对移动端复杂的兼容问题,它还需要不断地打磨。对自定义控制栏的开发目前还处于试验阶段,希望在不远的将来会有一套兼容原生和自定义的Video 组件与大家见面。如果大家对 NutUI Video 开发有什么好的建议,也欢迎留言参与 NutUI Video 的开发与设计!移动端 Video 组件的开发之路,道阻且长,我们一步一步来~

NutUI相关系列


【第1953期】十分钟快速上手NutUI


【第1946期】NutUI官网开发关键技术揭秘


【第1930期】运用 NutUI - 快捷开发京东企业业务之酷兜


Modified on

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存