使用canvas检测HTML5视频解码错误
乍一看这标题,有点吊炸天的赶脚,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秒后的绘制图片动作会捕捉到异常。没想到,竟然这样成功了!
该方法纯属个人想出来的,还有诸多不完善之处,遇到同样问题的同学可以试试这个思路~