乍一看這標題,有點吊炸天的趕腳,canvas跟<video>能有什麼聯絡?不過請放心我不是標題黨。事情是這樣的:
HTML5的<video>標籤所支援的視訊格式確實有限,mp4檔案必須是H264編碼的才行,若不是H264編碼,在chrome下會只有聲音沒有畫面,在FireFox下直接連聲音也沒有,而且控制檯會顯示警告:
因為瀏覽器使用的解碼器也是H264的。如果使用者在本地可以觀看的mp4視訊上傳後卻無法正常播放,這種體驗是相當糟糕的。
我嘗試在檔案上傳階段進行檢測,然而HTML5的FILE API能力也是有限的,只能獲取檔名稱、大小、MIME型別,對於視訊的編碼卻無法檢測到。
既然無法從上傳階段阻止使用者,那麼退一步講,在視訊無法播放的時候,我們希望可以檢測到,並且給使用者一個提示“視訊解碼錯誤”,這樣他就不會有疑惑“我的視訊為什麼無法播放呢?”。
首先想到的是video的API,video有onerror事件,但是此事件只能在src地址錯誤或其他原因載入不到視訊資源時觸發,當載入到視訊發生解碼錯誤時,並不會觸發。略蛋疼。這麼看來按照標準的東西是無法檢測到了,所以必須另闢蹊徑了。答案就是:
canvas讀取圖片畫素點的能力
前些天看了前端手記的這篇文章印象頗深,http://www.cssha.com/video2txt-canvas。利用canvas讀取圖片畫素點,進而轉化為文字圖片。更厲害的是canvas的drawImage方法還可以傳入視訊,獲取到視訊某一幀的畫素點。於是一個想法在腦中縈繞,解碼錯誤的視訊是沒有畫面的黑屏,我可以用canvas繪製視訊,根據所繪製的內容來判斷畫面是不是在動,遂想到如下思路:
- 在視訊開始播放時,每隔一定時間用canvas繪製一次視訊畫面
- 對每次canvas繪製的圖片進行畫素點取樣,存入陣列
- 掃描幾次後,比較每次取樣的畫素點rgb值是否相同,即檢測畫面是否變化了
- 根據畫面是否在“運動”來檢測是否解碼成功了
這種辦法當然也有侷限,下面是幾個注意事項:
- canvas繪製視訊畫面的次數控制。繪製圖片並取樣獲取畫素點是消耗效能的,所以這個掃描過程不應該伴隨視訊播放的整個時間段。只需在開始播放的幾秒內進行檢測即可。
- 若恰巧有某個視訊,開始的幾秒內就是一個靜止的畫面,那檢測就出錯了。
侷限歸侷限,先把想法寫成程式碼試試,於是有如下程式碼:
//比較兩個長度相等型別相同的陣列是否相等 function arrayEq(a1,a2){ for(var i=0,len=a1.length;i<len;i++){ if(a1[i]!=a2[i]){ return false; } } return true; } //比較取樣得到的陣列是否相等 function arrayAllEq(array){ for(var i=0,len=array.length;i<len;i++){ if(!arrayEq(array[i],array[i+1])){ return false; } } return true; } var can = $('<canvas id="canvas" style="display:none"></canvas>').appendTo('body'); var canvas = can.get(0); var width = $('#vi').width(); var height = $('#vi').height(); var ctxt = canvas.getContext('2d'); var media = document.getElementById('vi'); var resultArray = []; var stopScan = false; var scanImg = function(){ if(stopScan)return false; try{ ctxt.drawImage(media, 0, 0,width,height); var data = ctxt.getImageData(0, 0, width,height).data; var array = []; for(var i =0,len = data.length; i<len;i+=4*100){ var red = data[i], green = data[i+1], blue = data[i+2], alpha = data[i+3]; array.push(red,green,blue,alpha); } resultArray.push(array); return true; } catch(e){ alert('視訊解碼錯誤,請使用H264編碼的mp4檔案!'); stopScan = true; } } $('video').on('play',function(){ //每隔一定時間掃描一次畫面 scanImg(); setTimeout(scanImg,1000); setTimeout(scanImg,2000); setTimeout(scanImg,3000); setTimeout(scanImg,5000); setTimeout(function(){ if(scanImg()){ if(arrayAllEq(resultArray)){ alert('視訊解碼錯誤,請使用H264編碼的mp4檔案!'); } else{ alert('監測結束,視訊正常'); } } },8000); }); $('video').on('pause',function(){ stopScan = true; });
當我懷著激動的心情開始測試時,發現事實真不是想象的那樣。Chrome下,當一個視訊無法解碼時,drawImage方法直接無法執行,會報錯。完了,美好的想法泡湯了。。。
不過轉而一想,視訊解碼錯誤,drawImage方法就報錯,如果寫在try catch語句中,不就可以捕捉到了嗎?看來還沒到死路,這樣連畫素點取樣都省了,可以直接檢測到了。於是乎程式碼就簡化成了下面這樣:
//檢測視訊是否解碼錯誤 function checkVideoParseError(){ var videos = $('video'); if(videos.length>0){ var can = $('<canvas id="canvas" style="display:none"></canvas>').appendTo('body'); var canvas = can.get(0); var ctxt = canvas.getContext('2d'); var scanImg = function(video){ try{ ctxt.drawImage(video, 0, 0); } catch(e){ alert('視訊解碼錯誤,請使用H264編碼的mp4檔案!'); } } videos.on('play',function(){ var _this = this; scanImg(_this); setTimeout(function(){scanImg(_this);},1000); }) } }
在1秒後的繪製圖片動作會捕捉到異常。沒想到,竟然這樣成功了!
該方法純屬個人想出來的,還有諸多不完善之處,遇到同樣問題的同學可以試試這個思路~