說到在項目中引入一個視頻,咱們確定會想到 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
言歸正傳,距離 NutUI v2.2.2 版本 Video 視頻組件發佈已有一段時間了,在 NutUI 交流羣和 GitHub 上咱們也收到了一些用戶反饋,在這裏想跟你們聊聊 NutUI Video 組件的開發、使用以及遇到的問題和解決方案。canvas
首先,開發 Vue 視頻組件這個想法是源於一個移動端的項目。項目需求相對簡單,使用的是 Vue 技術棧,只有一個視頻須要點擊播放,因此在最初選擇實現視頻播放的時候沒有考慮要引入第三方的插件。而在項目開發之初調研 Vue 的 Video 視頻組件時,發現 NutUI 組件庫尚未視頻組件,這怎麼能忍呢?因而乎 NutUI Video 組件就這麼誕生了!瀏覽器
在開發以前,咱們先來再次認識下 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 組件這件事上,咱們分爲兩個階段:
第一階段是基礎的實現,完成視頻播放的基本功能。第二階段是進階版的自定義控制欄的實現,完成播放、暫停、控制條等操做項的自定義開發。
一、屬性的實現
對於屬性的實現,最開始想用一一對應將屬性綁定後拋給用戶使用,用戶操做的就是 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: '' }, } }
效果演示
二、自定義屬性
除了 Video 的基本屬性,在基礎版組件中咱們也爲用戶拋出了一些個性化的屬性設置如:disabled 禁止操做,playsinline 行內展現等。
options: {autoplay: true, muted: true,disabled: true, playsinline: true, loop: true,controls: false}
上述配置項規定了一個行內自動播放的背景圖視頻的例子,須要注意的是禁用操做目前只對自動播放時有效,在自動播放中用戶不可操做播放器,點擊播放器無效。而行內展現 playsinline 屬性,目前只有IOS端和個別安卓設備能兼容,要想徹底實現行內播放仍是要具體問題具體分析。
效果演示:
三、事件的實現
在事件實現這方面,視頻最重要的操做無非是播放、暫停、播放結束這三個事件,還有 error 事件,在報錯時提示錯誤信息,效果以下。
當咱們使用 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('播放結束');}, }
效果演示:
從視頻中能夠看到,當我點擊播放、暫停和播放結束時會觸發回調事件,當視頻播放結束後會提示播放結束。
若是說基礎版是依賴原生 Video 的控制欄,那麼自定義控件的實現就是掌握播放自主權的進階版。由於 Video
標籤在不一樣設備上都會有不一樣的默認設定,咱們很難控制它們,因此自定義一套本身的視頻播放控件,能夠必定程度上避免原生控件被默認修改的問題。下面,咱們來看看它的實現。
一、控制條的重構
關於重構控制條咱們能夠先來看張圖,分析下自定義控制條須要的元素。
上圖標註了控制欄須要的元素:
按照上述控制欄的元素進行重構便可,這裏就很少作贅述,直接上代碼。
<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 元素、自定義控制條元素以及用戶配置的屬性的初始狀態。
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 ; }
五、播放時間的獲取
播放時間的獲取是根據 video
的 duration
和 currentTime
來的。
// 獲取播放時間 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); },
經過對獲取當前播放時長佔總體播放時長的比值,對應到當前播放時間按鈕在整個播放條的位置,實現了播放時間的顯示。
六、進度條拖動控制
說到進度條,經過上邊分析的控制欄佈局,咱們知道它有一個可拖動的按鈕,這裏咱們對它的 touchmove
和 touchend
事件作處理。
// 拖動播放進度 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
播放控制條的長度,並用百分比轉化成時間,重置當前視頻播放的時間。
七、全屏控制
全屏和退出全屏咱們用 data
中 state.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(); } }
自定義控制條演示效果:
以上是自定義控制欄的實現,固然還有其餘功能待開發,以後會根據業務和用戶的反饋不斷的進行完善。
組件開發完了以後,終於能夠在項目中跑起來了,可是隨之而來的問題也接連出現了。這裏爲你們總結了下咱們在項目中遇到問題和解決方案。
在移動端 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問題以下:
一、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 組件的開發之路,道阻且長,咱們一步一步來~