[贝聊科技]首屏视频的优化过程(补充moov的研究)

做者:江敏熙 贝聊前端开发工程师
本文同时发布于我的博客html

前言

我司的官网首页——贝聊官网,首屏有一个自动播放的背景视频,一直被诟病视频加载慢、播放卡。刚开始觉得是文件太大,或者是网速太慢,但当我去优化它的时候,发现并无预想的简单。本文记录了优化过程和经验总结,但愿能对读者有所帮助。 前端

image

现状

官网的首页由6屏组成,首屏主要内容是一个自动循环播放的背景视频。页面无缓存时:git

  • 视频画面须要好几秒才能出现,期间只能看到页面的背景色,而且出现第一帧之后,画面会卡着不动,持续好久,有的时候甚至超过10秒,这种状况在下文统一简称第一帧卡
  • 画面第一帧卡完了之后播放不流畅,体验起来像幻灯片

初探

要优化视频播放卡顿的问题,我首先从视频的文件大小入手。 下载MediaInfo查看视频文件:github

  • 格式:mp4
  • 分辨率:1080P
  • 码率:4K
  • 大小:7.3M
  • 时长:15秒

4K的码率在对于在线视频是很是高的,我使用视频压缩工具格式工厂对其进行调整,把码率压到画质可接受的2400,此时文件大小4.4M。眼见文件大小已经瘦身为原来的60%,想必会有明显的优化效果。chrome

诡异的第一帧

打包发上测试环境,效果却大跌个人眼镜:视频卡顿感比之前减轻了一点,但仍是能明显的感觉到不流畅,而视频的第一帧卡的问题更是几乎没有改善。看来经过压缩码率下降文件大小的作法貌似是杯水车薪。segmentfault

深刻探索

意识到简单的减小资源文件大小的方法行不通以后,便上网搜查解决方案,但发现相关文章少之又少,并无找出第一帧卡的缘由。找不到解决方案,就只好本身摸索摸索了,慢慢的脑里有个猜测:若是把视频分红多份,浏览器只要加载了第一份就能够播放,这样会不会减轻视频的第一帧卡的问题呢?浏览器

视频分块加载

我把原有的视频切成了两段,并经过监听video标签的ended事件,在第一段播完后修改src切换到第二段,第二段播完后又切换回第一段,并循环这个过程。缓存

这样虽然给第一帧卡的问题带来了必定的改善,可是反作用是:切换画面并非无缝的,每次切换都会卡一秒左右。bash

反思

视频分块的作法我最终选择了放弃。缘由一方面视频时长原本才15秒,分块的意义并不大;另外一方面我认为这种方案即便作出来能无缝切换,也不会是最好的方案,由于并无解决根本问题(为何视频第一帧卡)。服务器

moov位置致使第一帧卡?打破传说

对于为何第一帧加载慢,我开始怀疑和mp4格式有关,我搜索了一下,很多文章说起到moov的问题:

image

mp4虽然支持流传输播放,但视频的“索引”储存在了moov对象,只有moov下载完视频才会开始播放。大多mp4文件会把这个moov放在文件头部,但若是放在了尾部则须要下载完整个文件才能开始播放。参考blog.csdn.net/jinshelj/ar…

我查看了压缩后的mp4文件,moov的确是在尾部。因而我使用qt-faststart(基于ffmpeg的moov前置工具),对moov对象作了前置处理。但通过个人测试,发现前置了moov并无优化第一帧卡的问题,播放表现和在尾部的时候同样。而且当文件moov在尾部的时候,视频在文件下载完以前就开始播了,并没有文件下载完才能播一说。

因而我再查阅资料,终于找到了缘由:若是服务器自己是支持seek的,那么mp4视频也是能正常边下边播的,参考segmentfault.com/a/119000001…

既然不是moov致使了第一帧卡,那到底是什么缘由呢?

更适合网络流传输的格式——flv

至此第一帧卡的问题尚未解决,因而我打算换一种视频格式试试,那么是否存在一个比mp4更适合在线播放的视频格式呢?

我搜索不少相关资料,flv是一种很是简洁,天生具有流式特征,很是适合网络流传输的格式。若是说mp4视频的“索引”是整个一块儿存储的,那么flv的“索引”则是分段存储的。打个简单的例子:看一个视频的开头,mp4须要下载整个视频的“索引”才能开始播放,flv只须要下载开头部分的“索引”。

浏览器并无原生支持flv解码,通常是经过flash来完成。可是,来自哔哩哔哩的开源插件——flv.js,能让video标签支持flv的播放。为了尝试flv可否改善第一帧卡的问题,我引入了flv.js,并把原来的视频转为flv格式。flv.js压缩后只占100KB+,使用起来也很是方便,代码实例以下:

const flvjs = require('../fiv.js');
if (flvjs.isSupported()) {
    var videoElement = $bgVideo[0];
    var flvPlayer = flvjs.createPlayer({
        type: 'flv',
		url: src // 视频的地址
    });
	flvPlayer.attachMediaElement(videoElement);
	flvPlayer.load();
	flvPlayer.play();
}
复制代码

测试了一下,结果让我惊喜。第一帧卡的问题解决了,可是播放依然是不流畅,体验像幻灯片。虽然问题没有彻底解决,并且flv.js不兼容IE10如下的浏览器,但至少得到了实质性的进展。接着只要集中精力解决视频播放不流畅的问题就能够了。

揪出抢占网速的凶手

播放不流畅的问题相对简单,缘由要么是下载太慢,要么就是文件太大。而文件已经被压缩,就只须要研究为何下载慢了。

这个时候我开始把眼光投在页面的静态资源上,看看可否对一些占用大的资源作优化。静态资源在发布生产时已被工具压缩过,已经没有什么再压缩的空间。个人思路是尽可能让首屏看见的资源马上加载,而第一屏外的资源延迟加载。打开浏览器的开发者工具,发现大部分的资源文件都很小,只有一个文件特别大,高达900+KB,打开发现是一个动图,而且不在第一屏内。这个时候就要考虑怎么样才能把图片放在视频下载完以后才加载。基本思路是视频下载完以后,再把图片标签动态添加到页面中。

我查阅了video标签的原生事件。 在众多事件中,suspend是比较适用于当前场景的,表现为当视频下载完成后触发。 但suspend的兼容性并很差,在IE 9等低版本浏览器下不能触发,而progress事件却没有这个问题。progress事件在视频下载时触发,假设一个视频下载耗时10秒,那10秒内每秒都会触发progress。

经过监听progress事件和设置定时器来判断视频是否加载完,达到动图延迟加载的目的。具体代码以下:

/**
监听事件progress,触发后设置定时器。每一次progress事件触发便会清空上一次的定时器。
假如在一秒内progress都没有触发,则视为下载完成,触发callback同时删除绑定。
**/

// $bgVideo[0]是视频的dom节点
afterDownload($bgVideo[0], function() {
	$bgImg.removeClass('hiden');
});

function afterDownload(video, callback) {
	// 计时器
	var callbackTimer = null;
	// progress事件回调函数。监听progress,直到一秒间不触发progress才执行callback
	var progressCallBack = function() {
		clearTimeout(callbackTimer);
		// 设定1秒的定时器,触发后删除绑定,删除定时器
		callbackTimer = setTimeout(function() {
			callback();
			video.removeEventListener('progress', progressCallBack);
		}, 1000);
	};
	// 绑定事件
	video.addEventListener('progress', progressCallBack);
}
复制代码

在调试的过程当中,上面的代码还有点小问题。若是视频已经被缓存,progress事件有时候不会触发,suspend事件也有一样的状况。我猜想是视频下载太快,addEventListener尚未执行就已经下载完了。我尝试把事件的绑定写在html上:

<video onprogress="progressCallBack" .../>
复制代码

采用这种作法之后,在本地调试时不断按F5刷新也不会出现问题,可是发布到服务器上却偶尔会出现问题,事件progress又没有被触发。最后我选择一种简单粗暴的方法,在页面初始化时在设置一个3秒的定时器:

function afterDownload(video, callback) {
	var callbackTimer = null;
	var progressCallBack = function() { ... };
	
	// 防止chrome在缓存的状况下,不触发progress
	callbackTimer = setTimeout(function() {
		callback();
		clearTimeout(callbackTimer);
		video.removeEventListener('progress', progressCallBack);
	}, 3000);
	
	video.addEventListener('progress', progressCallBack);
}
复制代码

在进入页面后,3秒内不触发progress事件,就认为视频已被缓存,直接执行callback并删除绑定和定时器。问题就此解决了。

虽然此作法能解决问题,可是我以为实现作法不太完美。若是您有更好的方法,请在下面留言☺

好用又免费的视频压缩工具——小丸工具箱

通过上述的优化,首页的视频播放效果已经好了不少,可是仍是有偶尔卡顿的状况。以前虽然已经使用格式工厂压缩了一遍视频,但考虑到市面上还有不少其余的视频压缩工具,因而再去百度里多找了一下,发现一款口碑不错的工具,叫“小丸工具箱”。

image
小丸工具箱相对以前的格式工厂,能够直接去除音频流(需求里视频不须要声音),这样视频体积更小了;操做更傻瓜化了,使用者只须要修改选项里的CRF和分辨率,基本上已经能完成多数状况的压缩需求。关于CRF,引用小丸做者的话介绍一下:

CRF(Const Quality, 固定质量),这种码率控制方式是很是优秀的,以致于能够无需2pass压制,即即便1pass也能实现很是好的码率分配利用。像质量模式的压制方式在其余编码器也有(如xvid或者压制rmvb的ERP),但据我所知都只是“固定量化(Const Quantization)”x264的CRF在量化的基础上,根据人的视觉心理学更为合理地分配码率,其目标是让人在看视频的时候,视频的质量尽量地统一,但码率达到尽量的有效利用。 CRF模式还有个优点,不少人在压片的时候不清楚应该给视频压到多少码率才比较好。CRF就是按须要来分配码率的,故其实就省下了到底要多少码率的苦恼。

这里附上小丸工具箱的入门操做教程

最后使用小丸工具箱尝试不一样了的CRF值,压制以后再肉眼对比,在画质和文件体积间找出一个平衡,把视频在1080P分辨率下压到了3.4M。 而我再尝试下降分辨率,发现当分辨率降为720P时,画质相差得并不明显。最终选择了分辨率720p,CRF23的压缩参数,此时视频压制到了2M,相较一开始的7.3M简直是暴瘦。

总结

至此,视频可以快速呈现,流畅播放。同时也发现,不管是使用mp4格式,仍是flv格式,第一帧卡的问题都已经不存在了。对于此状况,我用Chrome的限速功能测试过,只有在网速不够用的状况下(要么网速太慢,要么视频文件太大),mp4格式视频才会出现第一帧卡的问题。因为咱们已经把视频的大小压到了足够小,而且对大图作了延迟加载的处理,此时flv和mp4的差距已经微不足道了。最终我把flv.js撤了下来,统一使用mp4文件播放。

尾声

本文记录了我对于首屏的整个优化过程和当中获得的一些经验,过程磕磕碰碰,但愿能帮助读者少走些弯路。同时我认为优化这个事是永无止境的,特别是我对于视频压缩方面的知识较为薄弱,若是文中有什么不对的地方,或者好的建议,请读者们不吝赐教。

补充

通过热心网友的反馈,我发现我对moov的理解存在一些误差,而且首屏视频在Chrome播放时,会有两个问题:

  • 一个mp4文件会发起三次请求
    image
  • 有的时候动图会在视频加载完以前开始加载(监听progress事件失败)

而在IE9和火狐浏览器,上述两个问题并不能重现,因为我在开发时使用主要使用了火狐,因此疏漏上述两个问题。我搜索了相关资料,而且用了三个结构顺序不一样的mp4文件作了对比测试,如今给你们分享一下结果:

附上moov后置,moov前置无meta,moov前置有meta的三张mp4文件结构图:

image
首先,文件moov前置和后置会影响视频的Fast Streaming(快速播放),若是服务器不支持seek,moov后置的文件须要下载完才能播放,而若是服务器支持seek,那么也是能够在边下边播的,但浏览器会发送三个请求。 这三个请求的过程,简单来讲:第一个请求是从文件的头部开始下载并查找moov里播放所需的元数据,当获取不到就会发送第二个请求从文件的尾部开始下载并查找,查找到了之后再发起第三个请求去请求文件的内容,这个时候视频开始播放。对于在线播放,发送三个请求会比发送一个请求至少多耗几百毫秒。

第二,moov前置了也并不必定就能Fast Streaming,moov.udta.meta里存放的是视频的元数据,若是没有moov.udta.meta,那么也须要三次请求。而我在测试的过程当中发现,没有moov.udta.meta但moov前置的文件在chrome须要请求三次,而在Firefox只须要一次,这种状况我没有找到相关资料,暂时认为是不一样浏览器的采起的策略不一样。

因为此前使用小丸工具箱压制出来的是moov后置无meta的文件,使用qt-faststart也仅仅是前置了moov但没有生成moov.udta.meta,因此在Chrome上出现了三次请求并影响了progress事件的触发,因此致使了上面两个问题。 此后改成了使用ffmpeg优化(ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4),在moov下生成了moov.udta.meta,上述两个问题消失。

参考:

Optimizing MP4 Video for Fast Streaming

Understanding the MPEG-4 movie atom | Adobe Developer Connection

MP4文件格式详解——结构概述

媒体文件格式分析之MP4

利用ffmpeg修改MP4文件头信息,使其支持流式加载及播放

相关文章
相关标签/搜索