一、背景
最近在开发一个热点功能,整体是类似微博内容的短图文排版,但在开发如下图所示的Gif图片的展示轮播时遇到了一些坑,记录下来希望能和大家分享讨论~
二、需求与问题
首先分析下要实现的具体播放形式:
- 从第一张Gif开始轮流播放,同一时间内有且仅有一张图片在播放
- 图片播放结束后返回第一帧
- 所有图片播放结束后从第一张重新开始播放
看起来实现思路也比较简单:监听Gif图片的播放结束事件,一张Gif播放完再播放下一张,循环往复。
但问题在于GIF图片虽然有“动”的性质,但在Web中被一视同仁地做为图片处理,没有提供任何特殊待遇API,所以无法控制Gif图片的播放、暂停、结束监听等事件。思路中的必要条件——播放结束监听——完全无法达成。
三、思路与方案
既然原生无法控制,那么只好考虑从“替换”入手了。可以将gif替换成其它可控的动态元素,比如video或者canvas,经过一番调研后的两种方案简要如下:
四、实现与坑
下面介绍详细方案和具体实现过程,附上相关代码以参考,欢迎一起讨论
A. Gif转Video方案
一、前置条件
1.需要服务端支持提供转换后的video 2. 需要客户端支持video的自动播放及play()调用
二、最大缺陷
video标签在移动端的兼容很麻烦,不仅IOS和Android不同,甚至各平台都有自己的套路(微信重灾区),如果是做分享类页面不推荐该方案
三、实现环境
X5内核
四、核心问题
(一)禁止Ios播放全屏
- 配置参数
playsinline和webkit-playsinline为 true
(二)首次可自动播放或调用play()函数播放
- 配置
muted和autoplay为 true;Android需要客户端配置IX5WebViewExtension的invokeMiscMethod(String method, Bundle bundle)以允许自动播放; - 各平台兼容性不同,最好添加兜底方案:将
play()绑定到touchstart事件(模拟点击事件无效)
PS:
- 早期的IOS和Android都是禁止自动播放的,
play()函数也必须由真实用户交互触发; IOS从版本10以后放宽了规则,无音源或静音的video元素在屏幕可见后允许自动播放; 安卓的chrome 53 后设置autoplay和muted才允许自动播放; - X5内核作为webview,具备自动播放能力,但这能力的控制权交由TBS宿主控制;
- 目前安卓QQ浏览器允许wifi下自动播放、手机QQ允许自动播放、微信不允许自动播放。
(三)禁止用户与视频交互&去除视频控件样式
- 不配置
controls或者配置为false
PS:如果平台或者webview不支持自动播放,即使配置为false后也可能会出现控件,可尝试下面的CSS方案
video::-webkit-media-controls,
video::-webkit-media-controls-enclosure {
display: none !important;
}
video::-webkit-media-controls-play-button,
video::-webkit-media-controls-panel,
video::-webkit-media-controls-panel-container,
video::-webkit-media-controls-start-playback-button {
display: none !important;
-webkit-appearance: none;
}
五、实现结果
- 没有采用此方案所以只有测试数据
- 在IOS10以上和绝大多数Android手机均可完美实现轮播需求,测试的10+部不同品牌手机中仅有一台Android5版本的低端vivo无法自动播放
- 页面的首次观察到动画时间基本在1s内:Chrome环境下,以原大小为7.5M、平均传输时间在2.5s的Gif图片为例,转成video之后体积仅为2.2M、平均传输时间为1.2s(体积缩小70%以上,传输速度提升50%以上);且video支持边传输边播放,感官上等待时间较短
- 下图是平均体积2M的6图GIF转换成video之后的播放示例,开头一闪而过的是页面加载状态标志,可以看到无论是页面加载速度还是动起来的速度都较快
六、具体过程
-
参数配置:引入
video,配置合理参数:去掉视频的控件样式&禁止播放全屏<li :class="['hotImgWrap', `imgCount${videos.length}`]" v-for="(v, i) in videos" :key="i"> <video :id="`hotGif${i}`" class="hotImg" :src="v" :poster="imgUrls[i]" //设置首帧图片覆盖,因为部分Android手机视频缓冲时会闪过灰色底和白色按钮 :muted="true" //设置静音,否则无法自动播放+无法用代码触发首次播放 :controls="false" //关闭用户控制和隐藏控件 preload="auto" //预加载 :webkit-playsinline="true" :playsinline="true" //和上一行👆一起禁止IOS播放全屏 ></video> </li> -
循环播放:获取video元素,首次播放则添加播放结束的监听函数;调用play() 播放当前视频
//this.currentVideoId: 当前要播放的视频id,在数据初始化时设为 视频数量-1 //this.videoEles: 已播放过的视频数组,初始化为[], 用于存储 视频元素 playVideoGif() { //获取当前要播放的视频id if (this.currentVideoId + 1 === this.videos.length) { this.currentVideoId = 0; } else { this.currentVideoId++; } if (!this.videoEles[this.currentVideoId]) { //如果数组中没有当前视频元素,则获取后加入 const v = document.getElementById(`hotGif${this.currentVideoId}`); this.videoEles.push(v || {}); //每个视频只需添加一次结束事件监听 v.addEventListener("ended", e => { e.target.currentTime = 0; //播放结束后返回第一帧 this.playVideoGif(); //调用此播放函数,进行下次播放 }, false); } //调用视频播放 const playPromise = this.videoEles[this.currentVideoId].play && this.videoEles[this.currentVideoId].play(); //输出调试确认播放结果 if (playPromise) { playPromise.then(() => { console.log(`play ${this.currentVideoId} success`); }).catch(err => { console.log(`play ${this.currentVideoId} fail: `, err); }); } }
B. Gif+Canvas方案
一、前置条件
图片通过HTTP请求需要跨域,所以需要图片服务器添加CORS配置
二、最大缺陷
很慢,如所用图片较大建议压缩或者换其它方案
- 该方案中Gif从请求到显示所需时间为传输时间+解析时间。
- 其中传输时间略低于通过
请求图片,解析则耗时很长。以大小为2M,平均传输时间为1.5s(
标签请求)的Gif为例,通过HTPP请求平均耗时0.9s,平均解析时间为3s
- 虽然可以边解析边绘制,但在低端Android机上观察到闪动现象,不建议使用
三、核心问题
(一)如何在两天的开发时间中实现canvas对gif的读取、绘制、跳帧以及结束回调呢?
- libgif-js :上述功能均支持,有简要文档和示范,是实现的不二之选(主要也没有第二个选择 = =)
(二)canvas的开销很高,IOS测试中4个canvas同时加载已有明显卡顿,但gif图片数量可能到9甚至更多,页面初始化时严重卡顿+崩溃
- 首先获取所有GIF第一帧静态图(+缩略图)覆盖其上,目的是快速显示出所有图片;
- 读取前2张Gif图片,完成后播放第一张;第一张播放结束后,第二张(基本已完成读取)即可播放,同时开始读取第三张,以此类推直到所有图片完成读取
- 首轮读取的过程中需要配合静态图的显示与消失
(三)基于问题二,如何获取Gif第一帧静态图or缩略图
- 如果图片服务器用的是云的话,大部分都支持通过url参数配置来缩放和转换类型,但是会延长图片请求响应中的等待时间:在Chrome上测试请求阿里云的图片,不加参数时TTFB占总时长为2%左右,偶尔会到最高10%;添加缩放参数后TTFB占比为12%左右,偶尔能到30%+;
- 所以图片较大时最好还是找服务端直接提供静态缩略图的url
四、实现结果
- 可实现轮播需求,兼容性良好,性能要求不高,在测试和线上环境中暂未收到无法显示或页面崩溃的报告(希望这不是个Flag)
- 首张图有明显等待时间,例如2M大小的Gif在页面初始化完成后要等待大概3s(解析时间)后才能动起来
- 由于前一张播放时下一张已经在加载,所以后续动图一般无需等待,可流畅播放
- 下图为平均体积2M的6图GIF,相比较video方案页面加载时间和首张GIF动起来的时间都更长
五、具体过程
-
获取所有GIF的第一帧静态图(+缩略图)覆盖其上
<li :class="['hotImgWrap', `imgCount${feedInfo.imageInfos.length}`]" v-for="(img, i) in feedInfo.imageInfos" :key="i" > <img v-if="img.gif" :id="`hotGif${img.gifId}`" class="hotImg" :rel:animated_src="getFormatUrl(img)" /> <!--用于渲染GIF--> <img class="hotImg" v-show="!img.gif || img.show" :data-index="i" v-lazy="getFormatUrl(img, img.gif)" /> <!--用于快速显示的GIF静态缩略图--> </li> -
初始化前2张Gif为SuperGif类型并
load(),并组成supGifs数组const arr = []; for (let i = 0; i < 2; i++) { const sup = new SuperGif({ gif: document.getElementById(`hotGif${i}`), loop_mode: false, on_end: this.onGifEnd, show_progress_bar: false, auto_play: i === 0 ? 1 : 0, //第一张图load()后自动开始播放 }); sup.load(() => { //GIF load完成后去掉覆盖的缩略图 this.setImgShow(this.imgGifs[i], false); }); arr.push(sup); } this.supGifs = arr; this.nextGifId++; -
初始化时添加播放结束的回调函数on_end
onGifEnd() { //播放结束重置回第一帧 this.supGifs[this.nextGifId > 0 ? this.nextGifId-1 : this.imgGifs.length-1].move_to(0); //下一张开始播放 this.supGifs[this.nextGifId++].play(); if (this.nextGifId === this.imgGifs.length) { this.nextGifId = 0; } //判断是否是首轮播放(首轮播放需要初始化和load) if (this.supGifs.length < this.imgGifs.length) { const len = this.supGifs.length; const sup = new SuperGif({ gif: document.getElementById(`hotGif${len}`), loop_mode: false, on_end: this.onGifEnd, show_progress_bar: false, auto_play: 0 }); sup.load(() => { this.setImgShow(this.imgGifs[len], false); }); this.supGifs.push(sup); } },
五、总结
这两种方案各有优缺点,这里我去掉了页面加载时间(canvas方案的页面加载时间也更长)可以更直观的对比出两种方案首张图动起来的快慢(上video方案,下canvas方案):
总体来看,video方案体验好但是有兼容性问题且需要服务端支持;canvas方案自由度高、兼容性好,但是在图片体积大的时候,首张图动起来的速度慢,如果没有动图和加载标识的话用户很可能会以为这是张静态图……好在图片多的时候以小图形式展示,缩放后的体积控制在500k以内,最终效果也勉强能接受。 总体来看两种方案都只能算可行,中间也在思考查找其它方案但无果(甚至想找服务端直接加上每张Gif的时长然后用定时器控制)。希望这篇踩坑经历有所帮助,也欢迎随时留言交流想法~感谢阅读