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

作者:江敏熙 贝聊前端开发工程师
本文同时发布于个人博客

前言

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

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

现状

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

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

初探

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

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

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

诡异的第一帧

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

深入探索

意识到简单的减少资源文件大小的方法行不通之后,便上网搜查解决方案,但发现相关文章少之又少,并没有找出第一帧卡的原因。找不到解决方案,就只好自己摸索摸索了,慢慢的脑里有个猜想:如果把视频分成多份,浏览器只要加载了第一份就可以播放,这样会不会减轻视频的第一帧卡的问题呢?

视频分块加载

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

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

反思

视频分块的做法我最终选择了放弃。原因一方面视频时长本来才15秒,分块的意义并不大;另一方面我认为这种方案即使做出来能无缝切换,也不会是最好的方案,因为并没有解决根本问题(为什么视频第一帧卡)。

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

对于为什么第一帧加载慢,我开始怀疑和mp4格式有关,我搜索了一下,不少文章提及到moov的问题:

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

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并删除绑定和定时器。问题就此解决了。

虽然此做法能解决问题,但是我觉得实现做法不太完美。如果您有更好的方法,请在下面留言☺

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

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

《[贝聊科技]首屏视频的优化过程(补充moov的研究)》 小丸工具箱相对之前的格式工厂,可以直接去除音频流(需求里视频不需要声音),这样视频体积更小了;操作更傻瓜化了,使用者只需要修改选项里的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文件会发起三次请求 《[贝聊科技]首屏视频的优化过程(补充moov的研究)》
  • 有的时候动图会在视频加载完之前开始加载(监听progress事件失败)

而在IE9和火狐浏览器,上述两个问题并不能重现,由于我在开发时使用主要使用了火狐,所以疏漏上述两个问题。我搜索了相关资料,并且用了三个结构顺序不同的mp4文件做了对比测试,现在给大家分享一下结果:

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

《[贝聊科技]首屏视频的优化过程(补充moov的研究)》 首先,文件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文件头信息,使其支持流式加载及播放

阿里云最近开始发放代金券了,新老用户均可免费获取, 新注册用户可以获得1000元代金券,老用户可以获得270元代金券,建议大家都领取一份,反正是免费领的,说不定以后需要呢? 阿里云代金券 领取 promotion.aliyun.com/ntms/yunpar…

热门活动 高性能云服务器特惠 助力企业上云 性能级主机2-5折 promotion.aliyun.com/ntms/act/en…

    原文作者:HTTP
    原文地址: https://juejin.im/post/5b68288df265da0fa21aa6bf
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞