NutUI 視頻組件開發心得

引子

說到在項目中引入一個視頻,咱們確定會想到 HTML5 爲咱們提供的 Video 標籤,它爲咱們提供了許多屬性和方法,使用起來很方便,固然直接使用也會遇到各類兼容問題,在最初學習 Video 標籤時,W3C 官網就給出了這樣的舒適提示:javascript

在 HTML 中播放視頻並不容易!

您須要諳熟大量技巧,以確保您的視頻文件在全部瀏覽器中(Internet Explorer, Chrome, Firefox, Safari, Opera)和全部硬件上(PC, Mac , iPad, iPhone)都可以播放。html

這份提示在以後接觸了一系列視頻項目後,才明白這「不容易」指的是什麼,在移動端,咱們須要深諳的「大量技巧」卻還遠遠不夠......java

背景

NutUI 是一套京東風格的移動端組件庫,開發和服務於移動 Web 界面的企業級前中後臺產品。現擁有 50+ 個高質量組件,GitHub 上已得到 1.9k 的 star,NPM 下載量超過 14k。公司內部已賦能支持 50+ 個項目,外部接入使用項目達到 20+ 個。感興趣的同窗,快來掃碼體驗吧!web

image

言歸正傳,距離 NutUI v2.2.2 版本 Video 視頻組件發佈已有一段時間了,在 NutUI 交流羣和 GitHub 上咱們也收到了一些用戶反饋,在這裏想跟你們聊聊 NutUI Video 組件的開發、使用以及遇到的問題和解決方案。canvas

首先,開發 Vue 視頻組件這個想法是源於一個移動端的項目。項目需求相對簡單,使用的是 Vue 技術棧,只有一個視頻須要點擊播放,因此在最初選擇實現視頻播放的時候沒有考慮要引入第三方的插件。而在項目開發之初調研 Vue 的 Video 視頻組件時,發現 NutUI 組件庫尚未視頻組件,這怎麼能忍呢?因而乎 NutUI Video 組件就這麼誕生了!瀏覽器

image

前期準備

在開發以前,咱們先來再次認識下 video 標籤。相信初識 <video> 標籤時,不少人都是先掌握了使用方法,好比在頁面中添加一個 Video 標籤,再加一個視頻地址。緩存

<video controls width="250">
    <source src="videoname.webm" type="video/webm">
    <source src="videoname.mp4" type="video/mp4">
   您的瀏覽器不支持 video 標籤。
</video>

當視頻能在頁面中能順利播放以後,纔開始關注它的屬性和參數的使用。微信

<video src="videofile.ogg" autoplay muted poster="posterimage.jpg">
  抱歉,您的瀏覽器不支持內嵌視頻
</video>

好比上述代碼中對視頻的播放地址 src 、自動播放屬性 autoplay、靜音屬性 muted 和海報設置屬性 poster 進行了設置。異步

除了基本的可選屬性,Video 標籤還支持 HTML 中的全局屬性和事件屬性。ide

當咱們在 HTML 中建立了一個視頻後,就能夠拿到 Video 標籤的對象屬性和對象方法,好比

  • currentTime 視頻當前播放位置(即當前播放時間,以秒計)
  • duration 視頻的長度(整個視頻的播放長度,以秒計)
  • ended 視頻是否播放結束
  • volume 視頻的播放音量等對象屬性
  • ......

還有一些對象方法:

  • canPlayType() 檢查瀏覽器是否可以播放指定的視頻類型
  • load() 從新加載視頻元素
  • play() 開始播放視頻
  • pause() 暫停當前播放的視頻等這些對象方法。

感興趣的同窗能夠查閱 W3C等相關文檔中,這裏就不一一贅述了。

功能實現

經過對 Video 標籤的重溫,在 Vue 中要實現視頻的播放(僅指播放)能夠說很簡單,但要想「通關」移動端全部的「隱藏關卡」,那能夠說是不可能完成的任務。由於即便流傳度很廣的 Video.js、Vue-video-player 也存在不少待解決的問題,咱們只能具體問題具體分析。因此在實現 NutUI Video 組件這件事上,咱們分爲兩個階段:

image

第一階段是基礎的實現,完成視頻播放的基本功能。第二階段是進階版的自定義控制欄的實現,完成播放、暫停、控制條等操做項的自定義開發。

基礎實現

一、屬性的實現

對於屬性的實現,最開始想用一一對應將屬性綁定後拋給用戶使用,用戶操做的就是 video 標籤的原生屬性。但考慮到後期自定義控制欄的迭代,這種方法可能不利於管理,因而咱們仍是將 Video 的操做屬性用 options 對象統一管理,而視頻源則用 source 屬性管理,用集合形式管理視頻源信息,可支持多種格式的視頻源的配置,以便解決不一樣設備視頻格式的兼容問題。

<video ref="video" class="nut-videoplayer"       
        :muted="options.muted" 
        :autoplay="options.autoplay"
        :loop="options.loop" 
        :poster="options.poster"   
        :controls="options.controls"
        :preload="options.preload" 
        >
        <source v-for="source in sources" :src="source.src" :type="source.type" :key="source.src" />
</video>

到了這一步用戶調用組件,配置好參數就能正常播放視頻了。

<nut-video :sources="sources" :options="options"></nut-video>
 data() {
    return {
        sources: [{ src: 'video.mp4', type: 'video/mp4'}],
        options: {
            controls: true,
            autoplay: true,
            volume: 0.6,
            poster: ''
        },
    }
}

效果演示

image

二、自定義屬性

除了 Video 的基本屬性,在基礎版組件中咱們也爲用戶拋出了一些個性化的屬性設置如:disabled 禁止操做,playsinline 行內展現等。

options: {autoplay: true, muted: true,disabled: true, playsinline: true, loop: true,controls: false}

上述配置項規定了一個行內自動播放的背景圖視頻的例子,須要注意的是禁用操做目前只對自動播放時有效,在自動播放中用戶不可操做播放器,點擊播放器無效。而行內展現 playsinline 屬性,目前只有IOS端和個別安卓設備能兼容,要想徹底實現行內播放仍是要具體問題具體分析。

效果演示:

image

三、事件的實現

在事件實現這方面,視頻最重要的操做無非是播放、暫停、播放結束這三個事件,還有 error 事件,在報錯時提示錯誤信息,效果以下。

image

當咱們使用 video 的原生控制欄時,要想實現播放、暫停和播放結束,主要就靠監聽 video 的播放事件了。

//監聽播放
this.videoElm.addEventListener('play', () => {
        this.state.playing = true;
        this.$emit('play', this.videoElm);
});
//監聽暫停
this.videoElm.addEventListener('pause', () => {
        this.state.playing = false;
        this.$emit('pause', this.videoElm);
});
//監聽播放結束
this.videoElm.addEventListener('ended',this.playEnded);

用戶調用方法以下

<nut-video :sources="sources" :options="options" @play="play" @pause="pause" @playend="playend">
</nut-video>
methods: {
    play(elm) {console.log('play', elm);},
    pause(e) {console.log('pause');},
    playend(e) {alert('播放結束');},
}

效果演示:

image

從視頻中能夠看到,當我點擊播放、暫停和播放結束時會觸發回調事件,當視頻播放結束後會提示播放結束。

進階版實現--自定義控件的實現

若是說基礎版是依賴原生 Video 的控制欄,那麼自定義控件的實現就是掌握播放自主權的進階版。由於 Video 標籤在不一樣設備上都會有不一樣的默認設定,咱們很難控制它們,因此自定義一套本身的視頻播放控件,能夠必定程度上避免原生控件被默認修改的問題。下面,咱們來看看它的實現。

一、控制條的重構

關於重構控制條咱們能夠先來看張圖,分析下自定義控制條須要的元素。

image

上圖標註了控制欄須要的元素:

  • 播放按鈕
  • 當前播放時間
  • 整體時間
  • 播放控制條
  • 緩衝時間條
  • 可拖動播放按鈕
  • 靜音控制按鈕
  • 全屏控制按鈕

按照上述控制欄的元素進行重構便可,這裏就很少作贅述,直接上代碼。

<div class="nut-video-controller" >
      <div class="control-play-btn" @click="play"></div> <!-- 播放暫停 -->
      <div class="current-time">01:30</div> <!-- 當前播放時間 -->
      <div class="progress-container"> <!-- 播放控制條 -->
        <div class="progress" ref="progressBar"> <!-- 整體播放時間條 -->
          <div class="buffered" ></div> <!-- 緩衝時間條 -->
          <!-- 可拖動播放按鈕 -->
          <div class="video-ball"
            @touchmove.stop.prevent="touchSlidMove($event)"
            @touchstart.stop="touchSlidSrart($event)"
            @touchend.stop="touchSlidEnd($event)">
            <div class="move-handle"></div>
          </div>
          <div class="played" ref="playedBar"></div>
        </div>
      </div>
      <div class="duration-time">03:30</div> <!-- 整體時間 -->
      <div class="volume" @click="handleMuted"></div> <!-- 靜音按鈕 -->
      <div class="fullscreen-icon" @click="fullScreen"></div> <!-- 全屏按鈕 -->
</div>

二、初始化配置

在控制欄元素重構完成後,咱們須要先獲取到 Video 元素、自定義控制條元素以及用戶配置的屬性的初始狀態。

  • 獲取 Video 標籤
this.videoElm = this.$el.getElementsByTagName('video')[0];

這裏咱們拿到了 video 標籤,這一步很是重要,由於以後全部的操做都是基於它而成行的。

  • 獲取自定義控制條位置
const $player = this.$el;
const $progress = this.$el.getElementsByClassName('progress')[0];
// 播放器位置
this.player.$player = $player;
this.progressBar.progressElm = $progress;
this.progressBar.pos = $progress.getBoundingClientRect();
this.videoSet.progress.width = Math.round($progress.getBoundingClientRect().width);

代碼中咱們獲取到剛纔重構的控制條 progressBar 並對它的位置和寬作了定義。

  • 初始化屬性配置

初始化是將用戶設置的屬性參數綁定到 video 上,好比自動播放設置時要觸發播放事件,行內播放設置時要在 video 上綁上兼容屬性等等。

//自動播放
if (this.options.autoplay) {
    this.videoElm.play();
}
//行內播放
if (this.options.playsinline) {
     this.videoElm.setAttribute('playsinline', this.options.playsinline);
     this.videoElm.setAttribute('webkit-playsinline', this.options.playsinline);
     this.videoElm.setAttribute('x5-playsinline', this.options.playsinline);
     this.videoElm.setAttribute('x5-video-player-type', 'h5');
     this.videoElm.setAttribute('x5-video-player-fullscreen', false);
}

三、播放與暫停

視頻的播放與暫停的在自定義控制欄中咱們統一用 play() 事件控制,在界面渲染上用 data 中的 state.playing 控制。

play() {   
    this.state.playing = !this.state.playing;
    if (this.videoElm) {
        // 播放狀態
        if (this.state.playing) {
            try {
                this.videoElm.play();
                // 監聽緩存進度
                this.videoElm.addEventListener('progress', e => {this.getLoadTime();});
                // 監聽播放進度
                this.videoElm.addEventListener('timeupdate', throttle(this.getPlayTime, 100, 1));
                // 監聽結束
                this.videoElm.addEventListener('ended', this.playEnded);
                this.$emit('play', this.videoElm);
            } catch (e) {
                this.handleError()
            }
        }
        // 中止狀態
        else {
            this.videoElm.pause();
            this.$emit('pause', this.videoElm);
        }
    }
},

當視頻處於播放狀態時觸發 video.play(),咱們會對緩存進度、播放進度和播放結束的狀態進行監聽。當視頻是暫停狀態時,會觸發 video.pause() 暫停事件。

四、音量控制

視頻的音量控制就是在獲取到頁面中的 Video 元素後,設置它的 volume,方法以下。

volumeHandle() {
    this.videoElm.volume = this.state.vol ;
}

五、播放時間的獲取

播放時間的獲取是根據 videodurationcurrentTime 來的。

// 獲取播放時間
    getPlayTime() {
      const percent = this.videoElm.currentTime / this.videoElm.duration;
      this.videoSet.progress.current = Math.round(this.videoSet.progress.width * percent);

      // 賦值時長
      this.videoSet.totalTime = this.timeFormat(this.videoElm.duration);
      this.videoSet.displayTime = this.timeFormat(this.videoElm.currentTime);
    },

經過對獲取當前播放時長佔總體播放時長的比值,對應到當前播放時間按鈕在整個播放條的位置,實現了播放時間的顯示。

六、進度條拖動控制

說到進度條,經過上邊分析的控制欄佈局,咱們知道它有一個可拖動的按鈕,這裏咱們對它的 touchmovetouchend 事件作處理。

// 拖動播放進度
touchSlidMove(e) {
    let currentX = e.targetTouches[0].pageX;
    let offsetX = currentX - this.progressBar.pos.left;
    // 邊界檢測
    if (offsetX <= 0) {
        offsetX = 0;
    }
    if (offsetX >= this.videoSet.progress.width) {
        offsetX = this.videoSet.progress.width;
    }
    this.videoSet.progress.current = offsetX;
    let percent = this.videoSet.progress.current / this.videoSet.progress.width;
    this.videoElm.duration && this.setPlayTime(percent, this.videoElm.duration);

},
touchSlidEnd(e) {
    let currentX = e.changedTouches[0].pageX;
    let offsetX = currentX - this.progressBar.pos.left;
    this.videoSet.progress.current = offsetX;
    let percent = offsetX / this.videoSet.progress.width;
    this.videoElm.duration && this.setPlayTime(percent, this.videoElm.duration);
},
// 設置手動播放時間
setPlayTime(percent, totalTime) {
    this.videoElm.currentTime = Math.floor(percent * totalTime);
},

在拖動開始時獲取控制條的左側位置,並實時監聽偏移量,將偏移量的值賦給 this.videoSet.progress.width 播放控制條的長度,並用百分比轉化成時間,重置當前視頻播放的時間。

七、全屏控制

全屏和退出全屏咱們用 datastate.fullScreen 來控制它的按鈕狀態,默認是 false 表示不全屏,當用戶點擊全屏按鈕時,將其置成 true 並調用進入全屏事件 element.webkitRequestFullScreen() ,再次點擊時調用 document.webkitCancelFullScreen() 退出全屏,並把 state.fullScreen 置成 false 來改變按鈕圖標的樣式。

fullScreen() {
    if (!this.state.fullScreen) {
        this.state.fullScreen = true;
        this.videoElm.webkitRequestFullScreen();
    } else {
        this.state.fullScreen = false;
        document.webkitCancelFullScreen();
    }
}

自定義控制條演示效果:

image

以上是自定義控制欄的實現,固然還有其餘功能待開發,以後會根據業務和用戶的反饋不斷的進行完善。

問題&解決方案

組件開發完了以後,終於能夠在項目中跑起來了,可是隨之而來的問題也接連出現了。這裏爲你們總結了下咱們在項目中遇到問題和解決方案。

自動播放問題

在移動端 Video 的自動播放問題相信必定有許多人都遇到過,在 Video 標籤上加上 autoplay 後 PC 瀏覽器測試的很好,在手機端測試就失效,這是由於 autoplay 的兼容問題。形成這些問題的緣由可能有:

  • 瀏覽器不支持該視頻格式,建議可使用 MP四、WebM、Ogg 這三種視頻格式
  • 出於用戶體驗,節省流量的考慮,移動端禁止自動播放
  • 視頻文件太大,加載時間過長或錯誤

若是必定要作自動播放的功能的話,能夠參照以下方案:

一、檢查視頻格式是否正確,儘可能轉成 MP4,壓縮大小到2M如下

二、IOS 設備中 autoplay 失效,能夠加上靜音屬性 muted:

<video autoplay muted></video>

三、在用戶有觸屏操做後進行模擬播放。

let video = document.getElementById("video"); 
document.removeEventListener('touchstart', function(){
    video.play();
});
  • 這裏須要注意的是必需要等用戶有操做後才能執行模擬播放,不然會有報錯。
  • 安卓機加載完成後進行模擬播放是無效的,用戶必需要有觸屏操做後才能生效,好比點擊、觸摸、滑動屏幕等

四、若是是微信中自動播放失效,能夠考慮安裝微信的 JSSDK,經過監聽 WeixinJSBridgeReady,來控制自動播放,具體操做以下。

<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>

document.addEventListener("WeixinJSBridgeReady", function () {
    document.getElementById('video').play();
}, false);

五、若是安卓機仍是沒法作到自動播放,能夠考慮降級處理,展現控制條引導用戶點擊播放按鈕播放。

全屏播放問題

全屏播放視頻時,咱們可能會遇到屏幕沒有被佔滿,上下會有黑白邊的狀況,此時能夠添加 style= "object-fit: fill;width:100%;height:100%;",控制視頻撐滿屏幕。

行內播放

視頻行內播放,也就是視頻在頁面局部的內嵌播放,像文檔流同樣在它在的位置播放。但在移動端下,Video 視頻播放是默認全屏的,那要禁用全屏要如何實現呢?

一、IOS 設備能夠在 Video 標籤上設置 playsinline 屬性,兼容寫法以下:

<video muted src="video.mp4" autoplay 
    webkit-playsinline 
    playsinline
    x5-playsinline>
</video>

以上寫法在 IOS 中基本能夠解決行內播放的問題,x5-playsinline 可讓部分安卓機也兼容,但添加該屬性後不能再有 x5-video-player-type='h5'x5-video-player-fullscreen='true',不然還會默認全屏。

二、Canvas 模擬視頻播放

安卓設備上行內播放若是上述解決方案不能知足,能夠試試用 canvas 模擬視頻播放,將 video 標籤在頁面中隱藏,經過監聽播放、暫停以及播放結束事件,將視頻在 canvas 中繪畫出來。

initCanvas() {
    //獲取video
    let TestVideo = document.getElementById('videoPlay');
    let videoW = TestVideo.offsetWidth;
    let videoH = TestVideo.offsetHeight;
    //獲取canvas畫布
     let TestCanvas = document.getElementById('videoCanvas');
     //設置畫布
     let TestCanvas2D = TestCanvas.getContext('2d');
     //設置setinterval定時器
     let TestVideoTimer = null;
     //監聽播放
     TestVideo.addEventListener('play', function() {
         TestVideoTimer = setInterval(function() {
             TestCanvas2D.drawImage(TestVideo,0,0,320,180);
         }, 20);
     }, false);
    //監聽暫停
     TestVideo.addEventListener('pause',function() {
         clearInterval(TestVideoTimer);
     }, false);
     //監聽結束
     TestVideo.addEventListener('ended', function() {
            clearInterval(TestVideoTimer);
    }, false);
 }

用 canvas 模擬視頻雖然能夠實現行內展現,但效果不是很理想,視頻播放的清晰度不高,會有卡頓問題,也可能我在試驗這個方法的時候用的視頻源是被壓縮過的緣由,畫質不好,移動端控制畫布大小會有一點問題,感興趣的同窗能夠嘗試下用 canvas 模擬視頻播放。

issue問題

發版以後咱們也陸陸續續收到一些 issue 反饋,針對這些問題咱們也進行了逐一排查和修復。

issue問題以下:

image

一、video 組件運行控制檯會報錯

這個問題是由於開發自定義控件時,代碼遺留未註釋掉引發的,新版發佈已修復該問題。

二、視頻源異步切換

在基礎版發佈以後,NutUI 交流羣裏有反饋當異步切換視頻源時,視頻播放不了。那是由於視頻地址切換時沒有被監聽到,在組件中加上監聽事件重新加載一下就能夠解決。

方法優化以下:

watch: {
    sources: {
        handler(newValue, oldValue) {
            if (newValue && oldValue && newValue != oldValue) {
                this.$nextTick(() => {
                    this.videoElm.load()
                })
            }
        },
        immediate: true
    },
},

該方法已經跟隨新版本上線了,你們能夠更新版本後體驗一下。

感謝你們的反饋,也但願你們能多提寶貴意見,幫助咱們一塊兒捉蟲,讓這個視頻組件可以走得遠一點。

總結

Video 視頻組件第一版雖然已經發布,但功能也僅是基於原生 Video 標籤的封裝,面對移動端複雜的兼容問題,它還須要不斷地打磨。對自定義控制欄的開發目前還處於試驗階段,但願在不遠的未來會有一套兼容原生和自定義的Video 組件與你們見面。若是你們對 NutUI Video 開發有什麼好的建議,也歡迎留言參與 NutUI Video 的開發與設計!移動端 Video 組件的開發之路,道阻且長,咱們一步一步來~

相關文章
相關標籤/搜索