摘要:
最近北京三里屯的“优衣库事件”闹得沸沸扬扬,加拿大同样因为一则性爱视频风波不断。据英国明镜16日报道,近日,一则男女街头3P的视频在网上疯狂传播,并引来网友的谩骂,不过事件中的女主角并没有躲起来不敢见人,她选择接受媒体的采访,勇敢面对遭人蓄意偷
… 幻灯播放查看原图 X您已经浏览完所有图片重新播放 下一图集

说起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和华为机器上测试,其他浏览器和其他机型必定也有不同的兼容问题,希望大家有经验的也可以来互相补充下,一同进步。后续,我也会继续关注这个问题,继续补充。