移动端实践 - video

说起video,相信大家并不陌生。对于做过视频方面的小伙伴,特指前端方面的小伙伴,对它更是爱恨交加。因为,video使我们很方便在移动端播放视频,不必像PC端那样需要安装一个flash。

video很复杂吗?不,它很简单。要想使用它进行播放视频,只要在html下写出如下代码:

<video src="videoUrl"></video>

但是,由于浏览器对于video有各种奇葩的设定,所以通常导致开发者无法按照自己的想法实现,所以只能够尽量去适应浏览器,让video的表现不会过于偏离自己的想法。

通用的video属性

autoplay:布尔属性,指定后,视频会马上自动开始播放。
preload:表示视频预加载
controls:表示是否出现控制条
loop:表示是否循环播放
src:指定播放的视频源
width:指定视频宽度(通常在css中指定)
height:指定视频高度(通常在css中指定)

通用的video事件

play:视频开始播放触发的事件(触发此事件,但是视频不一定可以播放)
playing:视频可以播放触发的事件
timeupdate:音频/视频(audio/video)的播放位置发生改变时触发
pause:视频停止播放触发的事件
ended:视频播放结束或中断触发的事件

以上的这些属性和事件,只是属于video标签的常用的属性,要想知道更多的,可以去MDN那里查看。

测试机器和浏览器说明

本文所有的测试都在iphone6s和华为上测试,测试的浏览器有safari、微信、QQ浏览器和UC浏览器。

视频播放效果

在移动端实现的video效果,一是全屏播放,一是内联播放。所谓内联播放,就是指视频能够在你指定的位置播放。

视频全屏播放的效果实现

全屏播放,可以分成两种,一种是模拟全屏播放,也就是自己写个遮罩层模拟全屏播放的效果。另外一种是使用系统(浏览器或平台)内置的播放器。

如果是需要全屏播放的话,目前采取的策略有以下几种:

  • 简单粗暴的方法:
/* html */
<video src="http://godsong.bs2dl.yy.com/dmZlNTY3Y2VjZWRlNDM3NGM4MzNkZGE3OGJmYTJhYTVkMTIwMjY0NDI1N21j"></video>
/* css */
video {
    display: none;
}
var $video = document.getElementsByTagName("video")[0];
$video.play();

如上,这种方法只适用于IOS,使用这种方式,在微信和safari端,会调起IOS自带的播放器,在UC和QQ浏览器上会调用内置的播放器。简单粗暴,几行代码搞定。

  • 模拟全屏播放的方法
<div class="liveplayer">
    <video src="http://godsong.bs2dl.yy.com/dmZlNTY3Y2VjZWRlNDM3NGM4MzNkZGE3OGJmYTJhYTVkMTIwMjY0NDI1N21j"
            controls>
    </video>
</div>
<div class="btn">点击播放</div>
.liveplayer {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #000;
    display: none;
}
video {
    display: none;
    height: 100%;
    width: 100%;
}
.btn {
    border-radius: 10px;
    background-color: #ce3249;
    width: 100px;
    text-align: center;
    height: 50px;
    line-height: 50px;
    color: #fff;
}
var videoPlayer = (function() {
    var $livePlayer = document.getElementsByClassName('liveplayer')[0];
    var $video = document.getElementsByTagName('video')[0];
    var $playBtn = document.getElementsByClassName('btn')[0];

    $playBtn.addEventListener('click', function() {
        $livePlayer.style.display = 'block';
        $video.style.display = 'block';
        $video.play();
    })
})()

上面所说的全屏播放,实际是我们在页面上写了个遮罩层,模拟全屏。这种方式虽然麻烦点,但是适用性较广。

兼容性

在UC浏览器里面,IOS使用浏览器自带的播放器,始终处于全屏播放状态。
在QQ浏览器上,IOS上的表现是弹出小窗播放,所以没法做到全屏播放。
要使用播放器内联播放来模拟全屏播放,如果在IOS下,需要加入playsinline属性,才能做到一开始就在我们指定的位置播放视频,并且在IOS10以上才支持,以下的则是一开始默认全屏,点击全屏退出后,暂停播放视频。

视频内联播放

  • 视频内联播放的效果实现,实则与上述的模拟全屏播放的原理是一样的。
  • 不一样的是,全屏播放时,video的高宽可以是100%,视频内容会根据视频比例去适应。
  • 但是内联播放,你需要去定义视频的大小,这就需要获取到视频的大小了(原始方法不靠谱,最好是后台接口返回视频比例或者根据其他方法获取视频比例)。

视频事件的监听和处理

视频播放时,我们可以通过监听事件,来进行一些其他效果的实现。

上述说到的视频事件有:play,playing,timeupdate,pause,ended

首先我们来看看这些事件是在哪个阶段触发的。

  • 点击视频播放
    触发play事件,此时视频不一定可以播放。
  • 视频可以开始播放
    触发 playing 事件
  • 视频播放过程
    触发timeupdate事件,video的播放位置改变
  • 视频播放暂停
    触发pause事件
  • 视频播放结束
    触发ended事件。
    说明:
    • android中,UC浏览器,视频播放结束时,会触发pauseended事件。微信、QQ浏览器触发ended事件。
    • IOS中,UC浏览器下,视频播放结束,触发ended事件,其余,微信、QQ浏览器、safari下触发pauseended事件。

续播

续播的情况,可以分为两种情况,一种是视频播放过程中断了,续播。
另一种是视频播完了,续播。

视频结束后续播

通过上述的video事件,我们可以通过监听ended事件来实现这种续播效果。做法就是在ended事件,替换视频的src,然后再进行播放。如下:

var $video = document.getElementsByTagName('video')[0];
var videoUrl = 'url'

function playVideo(src) {
    $video.src = src;
    $video.play();
}

$video.addEventListener('ended', function() {
   playVideo(videoUrl);
})

兼容性
经过测试,在IOS和Android的UC浏览器、QQ浏览器、微信和safari均可以实现。

视频中断续播

这种情况多用于直播,直播时,视频播放时依靠视频流传输过来进行播放,一旦出现网络问题或者其他问题导致视频流断流,就会出现视频中断的情况。这种续播情况,也是比较难实现的。接下来,我们就来以直播断流为例,看怎么实现这种续播。

在此之前,我们先来看看video的几个属性。

  • readyState
    • 0 = HAVE_NOTHING - 没有关于音频/视频是否就绪的信息
    • 1 = HAVE_METADATA - 关于音频/视频就绪的元数据
    • 2 = HAVE_CURRENT_DATA - 关于当前播放位置的数据是可用的,但没有足够的数据来播放下一帧/毫秒
  • 3 = HAVE_FUTURE_DATA - 当前及至少下一帧的数据是可用的
  • 4 = HAVE_ENOUGH_DATA - 可用数据足以开始播放

测试结果
IOS:QQ浏览器的readyState始终是1,UC浏览器下始终是0。微信和safari在视频可播放的情况下,readyState为3或者4。
Android:微信、UC浏览器和QQ浏览器在视频可播放的情况下,readyState为4。

  • networkState
  • 0 = NETWORK_EMPTY - 音频/视频尚未初始化
  • 1 = NETWORK_IDLE - 音频/视频是活动的且已选取资源,但并未使用网络
  • 2 = NETWORK_LOADING - 浏览器正在下载数据
  • 3 = NETWORK_NO_SOURCE - 未找到音频/视频来源

测试结果
IOS:QQ浏览器在视频处于可播放状态时,networkState = 1。safari浏览器中,networkState = 2。UC浏览器中,networkState=0。
Android: 各浏览器表现基本一致,视频正常播放时,networkState = 2。

由上,我们可以得知,如果video的readyState = 3或4,networkState = 1 或 2时,视频是正常播放的。反之,则可能发生视频中断或结束.

直播断流表现

  • Android:没有触发事件,networkState和readyState也是正常。无法判断出是不是断流
  • IOS:触发ended事件

直播视频播放不顺畅

  • Android: 没有触发事件,networkState和readyState也是正常。无法判断出是不是断流
  • IOS:此时,readyState = 2

如果我们要处理直播断流这种情况的话,可以如下这样做。可以先想想,再看看代码。

<div class="liveplayer">
        <video
            src="http://hls.yy.com/newlive/54880976_54880976.m3u8?org=yyweb&appid=0&uuid=b08c0aac2a694cc19c781d6c268ef1ea&t=1487732029&tk=b84984ce4059851b5d456c7ffe205511&uid=0&ex_audio=0&ex_coderate=700&ex_spkuid=0"
            playsinline
            controls>
        </video>
    </div>
    <div class="debug">
        </div>
    <div class="loading">视频正在加载中...</div>

    <div class="btn">点击播放</div>
.liveplayer {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #000;
    display: none;
}

video {
    display: none;
    height: 50%;
    width: 100%;
}

.debug {
    z-index: 4;
    overflow: scroll;
    padding: 12px;
    font-size: 12px;
    line-height: 16px;
    color: #000;
    background-color: rgba(255,255,255,0.8);
    position: fixed;
    bottom: 0;
    width: 100%;
    top: 50%;
}

.btn {
    border-radius: 10px;
    background-color: #ce3249;
    width: 100px;
    text-align: center;
    height: 50px;
    line-height: 50px;
    color: #fff;
}

.loading {
    padding-top: 100px;
    text-align: center;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    color: #fff;
    background-color: #000;
    z-index: 2;
    display: none;
}
var videoPlayer = (function() {
    var $livePlayer = document.getElementsByClassName('liveplayer')[0];
    var $video = document.getElementsByTagName('video')[0];
    var $playBtn = document.getElementsByClassName('btn')[0];
    var $loading = document.getElementsByClassName('loading')[0];
    var streamInterval = null;

    $playBtn.addEventListener('click', function() {
        showLoading();
        $video.play();
    })

    function showLoading() {
        $loading.style.display = 'block';
    }

    function hideLoading() {
        $loading.style.display = 'none';
    }

    function showVideo() {
        $livePlayer.style.display = 'block';
        $video.style.display = 'block';
    }

    function hideVideo() {
        $video.style.display = 'none';
        $livePlayer.style.display = 'block';
    }

    $video.addEventListener('play', function() {
       debug('trigger video play status');

       debug('video readyState: '+$video.readyState);

       debug('video networkState: '+$video.networkState);

       showLoading();
    })

    $video.addEventListener('playing', function() {
       debug('trigger video playing status');

       debug('video readyState: '+$video.readyState);

       debug('video networkState: '+$video.networkState);

       debug($video.error && $video.error.code);

       hideLoading();
       showVideo();

       checkVideoStream();
    })

    $video.addEventListener('timeupdate', function() {
       debug('trigger video timeupdate status');

       debug('video readyState: '+$video.readyState);

       debug('video networkState: '+$video.networkState);
    })

    $video.addEventListener('pause', function() {
       debug('trigger video pause status');

       debug('video readyState: '+$video.readyState);

       debug('video networkState: '+$video.networkState);

       debug($video.error && $video.error.code);

       debug('video duration: '+$video.duration);
    })

    $video.addEventListener('ended', function() {
       debug('trigger video ended status');

       debug('video duration: '+$video.duration);

       debug($video.error.code);

       showLoading();
       hideVideo();

       $video.play();

    })

    function debug(con) {
        console.log(con);
        var date = new Date();
        var hour = date.getHours();
        var minutes = date.getMinutes();
        var seconds = date.getSeconds();

        var $debug = document.getElementsByClassName('debug')[0];
        var $beforeP = document.getElementsByTagName('p')[0];
        var $p = document.createElement('p');
        var $textNode = document.createTextNode('['+hour+':'+minutes+':'+seconds+']: '+con);
        $p.appendChild($textNode);

        if($beforeP) {
            $debug.insertBefore($p, $beforeP);
        }else {
            $debug.appendChild($p);
        }
    }

    function checkVideoStream() {
        if(streamInterval) {
            clearInterval(streamInterval);
        }

        streamInterval = setInterval(function() {
            if($video.readyState == 2 || $video.readyState == 1) {
                $video.play();
                clearInterval(streamInterval);
                return;
            }

            if($video.networkState == 3) {
                $video.play();
                clearInterval(streamInterval);
                return;
            }
        })
    }

}, 2000)()

主要思路是:点击播放,展示loading,同时播放视频,此时视频层仍然是隐藏状态。触发playing事件,隐藏loading,显示video,并且启动查询视频状态的定时器streamInterval。如果在播放过程中需要进行处理,则可以在timeupdate事件进行处理。当直播断流时,如果没有触发任何事件,这时可以通过readyState或networkState来判断是否断流,如果触发了ended事件,这时显示loading,隐藏video,播放video,重复之前的过程。

这种续播处理方式,IOS的适用性很高,但是android比较低。因为经过测试,在直播断流这种场景下,android端的表现是,没有触发ended事件,networkState和readyState也是正常,所以没法判断。

视频层级

在实际中,经常会有产品同学过来说,我要在视频上加下按钮,加下信息之类。嗯,理想很美好,但是现实很骨感。至今,除了在IOS的微信上可以做到这种效果之外,其余的主流浏览器都不支持。在这些浏览器里面,视频的层级是最高的。

后语

对于video,还有很多可以发掘的地方,比如制作进度条之类。并且兼容性方面我目前也只是测试了微信、QQ浏览器、UC浏览器和safari,机型也只是在iphone6s和华为机器上测试,其他浏览器和其他机型必定也有不同的兼容问题,希望大家有经验的也可以来互相补充下,一同进步。后续,我也会继续关注这个问题,继续补充。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容