基于MSE实现web前端视频预加载

2019-01-08 19:55:45 浏览数 (1)

在MSE标准提出前,js无法处理buffer级别的视频资源,video标签本身的一些限制导致业务方很难对视频流进行过多干涉处理,今天我们主要来聊一下如果通过MSE,容器软编解码等技术来实现mp4文件“真正”意义上的预加载,预处理。

一.背景

刷过抖音的同学应该都熟悉抖音的UI交互方式,上下滑动可以随时切换视频。这种设计对于普通用户而言,可以很快切走自己不感兴趣的,更快刷到自己感兴趣的视频。于开发和产品同学而言,通过统计用户在特定视频的停留时长,点赞评论等行为,可以进一步优化推荐算法和运营策略。

BUT,上下滑动的交互方式就意味着要进行资源预加载(在浏览当前视频的时候,已经在加载邻下临近的几个其它视频资源),类似于我们的图片瀑布流加载模式,图片预加载我们常用的方式为:

var img =new Image();

img.onload =function(){

        //加载完成回调

}

img.src='xxx';

通常我们会用上面的方式封装一个img loader模块用来实现图片预加载

但是对于视频资源这种预加载方式也可行吗???

很显然不行。

video标签本身也有关于预下载的属性preload,但是大多数浏览器对它其实支持非常差,这个属性并没有起到我们想要的效果。

二.现行方案及其缺陷

方案1:

将多段视频拼接成一个视频,借助video对象的currentTime调整播放点位置来达到多个视频播放时候无缓存的假象,单其实只有一个视频。

方案2:

创建多个video标签,对于暂时不播放的video先调用play()再调用pause(),然后等真正需要播放它的时候再次调用play()达到类似先激活的状态。

------------------------------------------------------------------------------------------------------------

上述方案在解决一个页面只有2个或者3-5个视频的场景时候勉强可用,但也都存在缺陷,并不适合抖音,微视这样的业务场景。

三.基于MSE及软编解码的新方案

首先,我们改变对 mp4 视频的播放流程,不再直接使用 video 的 src 来播放,因为我们没有任何可以操作的空间。video不仅支持 src 属性还支持 Blob 对象,我们就是利用后者。播放的流程如下:

流程图流程图

对于MSE,想了解更多的同学可以去https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API,查阅,当然网上也有很多资料

简单来说MSE它允许JS脚本动态构建媒体流,允许JS传送媒体块到H5媒体元素。这种接口的应用可以让h5播放器实现持续添加数据进行播放,从而摆脱传统只依赖video的src属性的方式。

video.src = window.URL.createObjectURL(this.mediaSource);

结合URL.createObjectURL及MSE一系列API,我们可以将加载好的视频流buffer注入video进行播放,

MSE在其中扮演了buffer流的管理及桥接工作,因为MSE支持的是fmp4格式,所以对于mp4文件我们需要在加载队列之后进行一个容器层级的软编解码。关于容器格式与视频编解码的区别可参考https://yanhaijing.com/html/2016/03/12/html5-video/

综上所述,实现流程如下:

1. 编写加载器loader,请求 mp4 视频数据。 2. 编写解析器将 mp4 视频数据进行解复用,流处理等 。 3. 将解复用的视频数据转成 fmp4 格式并传递给 MediaSource。 4. 通过createObjectURL将MediaSource与 video 进行关联,完成播放。

流级别处理包含:多段mp4流合并,剔除/替换mp4流音轨,字幕,裁剪视频长度,清除无用视频流buffer等。

模块设计图:

体验demo:

http://sqimg.qq.com/qq_product_operations/test/mse.html

(安卓手Q webview(x5),安卓微信 webview(x5),chrome都支持)

ios手Q,ios微信的webview, safari暂时不支持

demo的实现代码如下

代码语言:javascript复制
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no,viewport-fit=cover"/>
	<meta name="apple-mobile-web-app-capable" content="yes">
	<meta name="apple-mobile-web-app-status-bar-style" content="black">
	<meta name="apple-itunes-app" content="app-id=444934666">
	<meta name="format-detection" content="telephone=no">
    <title></title>
    <style type="text/css">
    	/*手QH5页面通用css基础样式*/
    	html,body, div, p, ol, ul, li, table, tbody, tr, td, textarea,
        form, input, h1, h2, h3, h4, h5, dl, dt, dd, img, iframe, header, nav,
        section, article, footer, figure, figcaption, menu, a, p,button {padding: 0;margin: 0; -webkit-user-select: none; -moz-user-select: none; -webkit-text-size-adjust: none;-webkit-touch-callout: none;}
        html ,body{ width: 100%; height: 100%;}
        body { font-size: 62.5%; font: 16px "Helvetica Neue", Helvetica, STHeiTi, "5FAE8F6F96C59ED1", sans-serif; min-width: 320px; margin: 0 auto;}
        em{ font-style: normal;}
        a, span { text-decoration: none;display: inline-block;}
        a:link, a:visited{ color: #fff; text-decoration:none;}
        a,button{outline: none; -webkit-tap-highlight-color:rgba(0,0,0,0);}
        button{border:none; background: transparent;}
        li {list-style: none;}
        /*业务侧css样式*/
    </style>	  
</head>
<body>
    <video id="j-video" x-webkit-airplay="true" webkit-playsinline="true" preload="auto" style="width:100%;margin-top:50px;"></video>
    <div id="pros0" style="width:100%;margin-top:30px;">视频加载进度:</div>
    <button id="btn" onclick="play()" style="margin-top:20px;font-size:24px;color:blueviolet;">点我播放</button>
    <div id="noTips"></div>
    <!--业务js脚步区域-->
    <script src="http://sqimg.qq.com/qq_product_operations/test/mp4box.all.js"></script>
    <script type="text/javascript">

            

            var video = document.getElementById('j-video');
            var sourceBufferArr =[];
            var assetURL = 'http://sqimg.qq.com/qq_product_operations/test/mov_bbb.mp4';



            //http加载模块
            function fetchAB(url, cb) {
                var xhr = new XMLHttpRequest;
                xhr.open('get', url);
                xhr.responseType = 'arraybuffer';
                xhr.onprogress = function (event) {
                    if (event.lengthComputable) {
                        var loaded = parseInt(event.loaded / event.total * 100)   "%";
                        document.getElementById('pros0').innerText = 'demo视频加载进度:' loaded;
                    }
                }
                xhr.onload = function () {
                    cb(xhr.response);
                };
                xhr.send();
            };


            //buffer解析模块
            function dealBuffer(url){
                var mp4box = new MP4Box();
                mp4box.onReady = function (info) {
                    console.log(info)
                    //mediaSource.duration = Math.floor(info.duration / 1000);

                    for (var i = 0; i < info.tracks.length; i  ) {
                        var track = info.tracks[i];
                        var mime = 'video/mp4; codecs="'   track.codec   '"';
                        console.log(mime)

                        if (MediaSource.isTypeSupported(mime) && track.type=='video') {
                            try{
                                var sb = mediaSource.addSourceBuffer(mime);
                                sourceBufferArr.push(sb)
                                mp4box.setSegmentOptions(track.id, sb, { nbSamples: 4000 });
                            }catch(e){
                                document.getElementById('pros0').style.display = 'none';
                                document.getElementById('btn').style.display = 'none';
                                document.getElementById('noTips').innerText = '不支持MSE';
                            }
                        }
                    }

                    var initSegs = mp4box.initializeSegmentation();
                    var pendingInits = 0;
                    for (var i = 0; i < initSegs.length; i  ) {
                        var sb = initSegs[i].user;
                        sb.addEventListener("updateend", function (e) {
                            pendingInits--;
                            if (pendingInits === 0) {
                                mp4box.start();
                            }
                        });
                        sb.appendBuffer(initSegs[i].buffer);
                        pendingInits  ;
                    }

                }
                var b=null;
                mp4box.onSegment = function (id, sb, buffer, sampleNum) {   //生成一个片都会触发一次
                    console.log(sb '>>>' id '>>>' sampleNum);
                    sb.appendBuffer(buffer);
                }
                fetchAB(url, function (buffer) {
                    buffer.fileStart = 0
                    mp4box.appendBuffer(buffer)
                    mp4box.flush();
                });
            }

            //关联MSE到video
            if(window.MediaSource){
                window.mediaSource = new MediaSource();
                video.src = window.URL.createObjectURL(mediaSource); 
                dealBuffer(assetURL);
            }else{
                document.getElementById('pros0').style.display = 'none';
                document.getElementById('btn').style.display ='none';
                document.getElementById('noTips').innerText='不支持MSE';
            }

            function play(){
                video.currentTime =0;
                video.play();
            }
	</script>
</body>
</html>

0 人点赞