之前的运营活动项目一直使用 Canvas 还原动画,费时费力不说,想要达到完美复刻设计稿动画简直难于上青天。一番折腾下来,设计师跟开发都感慨:“我太南了”!懒是人类进步之光,对于很多纯展示类型的运营活动页面用最少的开发资源达到最佳的动画效果,有没有银弹呢?有,那就是视频!
对了之前的文章在这里:
为什么使用视频?
传统开发模式劣势
- 开发效率低:所有动画效果都需要通过代码编写实现,还需要与设计师一起调配动画参数,开发效率,且 UI 一通改,码农两行泪。
- 动画实现还原度低:设计师有一百种淡入淡出的方法,更多时候前端工程师需要具体的到动画参数,但是设计师却只能提供一种感觉。
- 性能要求高:机器崩溃,开发也奔溃。
- 技术实现局限了设计师的发挥: 本可狂拽酷炫吊炸天却只能因为性能问题,实现成本问题到处取舍。
使用视频的优势
- 开发与设计提效:节约开发使用代码还原动画以及设计与开发反复就动画实现的沟通时间与成本。
- 高保真还原 UI 与动效:开发依赖动画稿进行还原,因设计工具的不支持,难以获取精准的动画数值如贝塞尔曲线等,只能估算具体的数值,因此会产生误差,无法高保真还原动画。
- 无动画性能问题:使用
CSS
、Canvas
、WebGl
动画,在动画元素较多或者大的场景切换的情况下,容易引发性能问题,而视频播放帧率可以维持流畅的播放体验。 - 资源大小更优:视频文件相较于 Gif 图以及序列帧实现方式资源文件更小,Google Chrome 以及 Webkit 官方也推荐使用视频替代 Gif 图。1
最佳实践
初看起来视频是解决问题的一枚银弹了,不过 <video>
在成为一枚银弹之前还有很多雷要去排。
去除控件
移动端的视频默认存在控件,即便是设置属性 controls="false"
也无济于事,且视频开始播放后还会进入全屏模式,这无法给用户带来沉浸式体验,分分钟就出戏。幸好使用 playsinline="true"
与 webkit-playsinline="true"
属性可以使视频内联播放,也可避免视频开始播放后进入全屏模式。
<video src="video.mp4"
playsinline="true"
webkit-playsinline="true"
></video>
Android 端 WebView 接入了 UC 内核,使用上述属性后可完美去除播放控件。
iOS 10 以及之后的版本支持 playsinline="true"
属性,iOS 10 之前的版本可使用 webkit-playsinline="true"
替代即可。不过部分 iOS 还是会出现播放控件,我们可以用 iphone-inline-video 来进行补救。2
<video src="video.mp4"
id="video"
playsinline="true"
webkit-playsinline="true">
</video>
<script>
import enableInlineVideo from 'iphone-inline-video';
let video = document.querySelector('#video');
video.play();
enableInlineVideo(video);
</script>
<style lang="scss">
.IIV::-webkit-media-controls-play-button,
.IIV::-webkit-media-controls-start-playback-button {
opacity: 0;
pointer-events: none;
width: 5px;
object-position: center center;
}
</style>
自此双端均可获得完美的行内播放体验。
预加载
移动端使用属性 preload="auto"
是无法达成视频预加载的,因为浏览器会忽略这个属性将其处理成 preload="metadata"
,而这仅能加载视频的元数据。视频只能在用户点击之后才开始加载,这固然是浏览器考虑到帮用户节约流量的目的,但对运营活动来说用户体验就大打折扣了。
试想一下这样的场景:上班途中用户打开我们的运营活动页面,触发了视频播放,此时地铁恰巧进入了信号不好的路段,那么用户能看到的只有等待加载的提示了,而用户在进入页面的时候已经出现过一次资源加载的提示了,就会很困惑,用户体验会大打折扣。
使用 <link rel="preload">
可以提升视频加载优先级(我们还得关注下其兼容性3),将视频加载的行为左移,但是这无法获取视频的加载进度,显然这也不是最优解。
<link rel="preload" as="video" href="xxx.mp4" onload="handleVideoLoaded">
<video id="video"></video>
<script>
function handleVideoLoaded() {
video.src = 'xxx.mp4';
}
</script>
最终我们采取的方案为:直接构造一个 XMLHttpRequest
直接获取视频文件的二进制文件,然后直接将处理后的 URL 赋值给 <video>
的 src
属性,同理适用 fetch
API,为了更好的向下兼容,还是选择了 XMLHttpRequest
。具体实现代码如下:
const req = new XMLHttpRequest();
// 作为测试用加时间戳
req.open('GET', `video.mp4?t=${new Date() * 1}`, true);
req.responseType = 'blob';
req.onload = () => {
if (req.readyState === 4) {
if (req.status === 200) {
const videoBlob = req.response;
const src = URL.createObjectURL(videoBlob);
video.src = src;
}
}
};
req.onerror = () => {
// Error
};
req.onprogress = () => {
// 处理加载进度
};
req.send();
经过测试,此方案可以可控的对视频文件进行预加载。Blob
与 createObjectURL
浏览器兼容性还不错 4 5。
自动播放
同移动端对音频的策略:给 <video>
设置 autoplay
属性并不能触发视频的自动播放,需用户触发交互才可。难道只能借助于引导用户操作,去触发 play()
事件吗?
playBtn.addEventListener('click', function(event) {
event.stopPropagation();
if (video.paused) {
video.play();
}
});
自从 iOS 10,iOS 有了新的 <video>
播放策略,设置了 autoplay
属性后可以允许视频自动播放,但是得满足以下任意一种条件:
- 无音轨视频
- 或者无声音视频(设置了
muted
属性)
另外此下两种条件还得必须满足:
- 视频元素需在是视口内且是可见的
- 视频元素需插入到 DOM 中
经过测试,符合上述条件的视频,AMAP iOS 端内 WebView 可以通过 play()
方法直接调用视频播放。iOS WebView 可以通过更改 WKWebViewConfiguration
配置 mediaTypesRequiringUserActionForPlayback
属性来开启。6
Chrome 53 之后也支持了 iOS 同样的策略,无音轨或者静音的视频可以自动播放。另外Firefox 和 UC 浏览器已经在 Android 上支持自动播放。更好的消息是,AMAP Android 接入了 UC 内核 Webview,同样我们可以通过 play()
方法来调用视频的播放。
最终我们想要控制视频播放的话,可以手动执行 video.play()
方法即可。
fallback
执行视频的 play()
方法后,还需确认视频已成功播放。可以通过监听 timeupdate
事件去检测 currenttime
的值是否变化来间接获知,但这并不是最优解。最佳方案是,对视频执行 play()
后,浏览器会返回一个 Promise
:如果视频播放错误,我们可以 catch
到进而进行一些补救措施。
var playPromise = document.querySelector('video').play();
if (playPromise !== undefined) {
playPromise
.then(function() {
})
.catch(function(error) {
// 处理异常
});
}
屏幕适配
使用设计稿的宽高比为基准,如页面可视区域与设计稿宽高比一致则采取等比例缩放、无画面裁切。如页面可视区域比设计稿高,则将视频高度等比例缩放至页面可视区域高度,裁剪超出的宽度区域,反之裁剪超出的高度范围。
为了应对适配,根据前端的适配方案,要求在 UED 输出设计时需留有安全区域。
优化视频文件
视频格式
FFmpeg
说到视屏处理工具,不得不提 FFmpeg,市面上大多数音视频转码工具、视频播放器都是它的壳。
TODO
qt-faststart
TODO
Bug 处理
视频播放结束后视频展示黑色背景
监听视频播放进度,在视频播放结束提前处理。
const duration = video.duration;
// ...
video.addEventListener('timeupdate', () => {
const time = video.currentTime;
});
TODO