乍一看這標題,有點吊炸天的趕腳,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秒后的繪制圖片動作會捕捉到異常。沒想到,竟然這樣成功了!
該方法純屬個人想出來的,還有諸多不完善之處,遇到同樣問題的同學可以試試這個思路~