移動端實踐 - 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和華為機器上測試,其他瀏覽器和其他機型必定也有不同的兼容問題,希望大家有經驗的也可以來互相補充下,一同進步。后續,我也會繼續關注這個問題,繼續補充。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容