使用canvas檢測HTML5視訊解碼錯誤

呂大豹發表於2013-11-08

  乍一看這標題,有點吊炸天的趕腳,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繪製視訊,根據所繪製的內容來判斷畫面是不是在動,遂想到如下思路:

  1. 在視訊開始播放時,每隔一定時間用canvas繪製一次視訊畫面
  2. 對每次canvas繪製的圖片進行畫素點取樣,存入陣列
  3. 掃描幾次後,比較每次取樣的畫素點rgb值是否相同,即檢測畫面是否變化了
  4. 根據畫面是否在“運動”來檢測是否解碼成功了

  這種辦法當然也有侷限,下面是幾個注意事項:

  1. canvas繪製視訊畫面的次數控制。繪製圖片並取樣獲取畫素點是消耗效能的,所以這個掃描過程不應該伴隨視訊播放的整個時間段。只需在開始播放的幾秒內進行檢測即可。
  2. 若恰巧有某個視訊,開始的幾秒內就是一個靜止的畫面,那檢測就出錯了。

  侷限歸侷限,先把想法寫成程式碼試試,於是有如下程式碼:

//比較兩個長度相等型別相同的陣列是否相等
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;
});
View Code

  當我懷著激動的心情開始測試時,發現事實真不是想象的那樣。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秒後的繪製圖片動作會捕捉到異常。沒想到,竟然這樣成功了!

  該方法純屬個人想出來的,還有諸多不完善之處,遇到同樣問題的同學可以試試這個思路~

相關文章