更改正在动画的精灵表时 HTML5 Canvas 挂起(逐帧播放视频)
Posted
技术标签:
【中文标题】更改正在动画的精灵表时 HTML5 Canvas 挂起(逐帧播放视频)【英文标题】:HTML5 Canvas hangs when changing the sprite sheet being animated (frame by frame video playing) 【发布时间】:2012-12-29 07:01:07 【问题描述】:我需要在使用 html5 的移动设备上播放具有透明背景的视频。在 Windows Phone 上,我可以捕获视频标签的当前帧并将其显示在画布中。但这不适用于 android 和 ios 设备(我认为是出于安全原因?)
我的解决方案是使用 FFMPEG 拆分 .flv,然后将这些帧拼接成大图像,例如精灵表。
问题是当我切换到新的框架表时动画“挂起”。我只是在视觉上和通过控制台检查了这一点(通过在我更改当前精灵表行时记录。)我已经通过查看当我更改精灵表时它如何挂起以及当我如何不挂起来测试它只需一遍又一遍地循环同一张纸。
我预先加载了所有图片:
var frameImages = [];
for(var i = 0; i < 35; i++)
frameImages.push(new Image());
frameImages[i].src = 'frame' + i + '.png';
console.log(frameImages[i].src);
frameImages[i].onload = function()
// Notify us that it's been loaded.
console.log("Image loaded");
然后这样玩:
processFrame = function()
outputCanvas.width = outputCanvas.width;
output.drawImage(frameImages[currentFrame], (curCol*153), (curRow*448), 153, 448, 0, 0, 153, 448);
curCol += 1;
if(curCol >= maxCol)
curCol = 0;
curRow += 1;
if(curRow >= maxRow)
curRow = 0;
currentFrame++;
var mozstart = window.mozAnimationStartTime;
step = function(timestamp)
var diff = (new Date().getTime() - start) - time;
if(diff >= frameDelay)
processFrame();
time += frameDelay;
我已经在 Win 7 机器上的 Chrome v 23.0.1271.97 m 和带有 Chrome 的 Nexus 7 上尝试过这个。
在这里查看它的实际效果:http://savedbythecode.com/spokes/mozanimation.php - 这是使用 mozAnimationStartTime 和http://savedbythecode.com/spokes/newplayer.php - 这是使用每一步都会调整的常规 JS 计时器(来自http://www.sitepoint.com/creating-accurate-timers-in-javascript/)
有什么想法吗?问题够清楚了吗?
谢谢, 凯文
【问题讨论】:
对客户来说似乎效果很好。 我的意思是,如果它损害了业务,那么为此付钱给他的人就不会继续付钱给他了。他的生意在增长,而不是在萎缩。最重要的是,你必须牢记观众。我们对技术非常满意,我们这一代人也是。我的客户处理的业务通常拥有对技术不太满意的年龄较大的受众。有时甚至对他们来说是压倒性的。让网站上的某个人与他们交谈,可能会让他们更舒服,更容易消化信息。再说一遍,这不关我的事。 它在 Chrome 中对我来说工作正常,但只有在加载所有帧之后,所以你可能想要延迟动画直到所有精灵完全加载。不确定您是否在其他浏览器中进行过任何调试,但我在 Firefox 中的动画出现延迟(声音不同步),并且在 Opera 中我看不到动画也听不到声音。它在 IE8 中不起作用,但这是意料之中的 LOL。也许为不兼容 HTML5 的 IE 浏览器使用背景声音,并确保那里不调用不支持的功能?不过,我可以预见视频+音频同步问题。到目前为止做得很好!!! ;) 只是一个离题的观察,但演示者的底部几乎没有移动(没有双关语,哈哈),我认为这可能会节省带宽。动画的下半部分只是在演示者“进入”时逐帧真正不同,然后她只是随着她的手移动而轻微摆动。我意识到这意味着很多额外的工作可能没有被请求,或者确实没有付费,但可能会节省大约 40% 的带宽。 这实际上只需要在 Android 和 iOS 设备上工作,客户端已经有一个类似于我的 Windows Phone 解决方案的现有 Web 解决方案。所以我不必担心支持 IE :)。 【参考方案1】:很酷的代码。为什么不能只使用视频标签(参见选项 6)?无论如何,我只会列出我能想到的问题,希望对你有所帮助:
将 ++
更改为 += 1
:为了与您之前的代码保持一致,并且因为 channeling Doug Crockford's benevolent ghost 可能有助于消除您的 javascript 代码中的故障。
您将一堆图像拼接成一个“sprite sheet”。然后,当您想要进入这些工作表中的下一个时,代码会挂起。所以,因为你的代码还没有工作,作为一个测试,删除所有循环遍历每个帧中的子图像,并在每次渲染时直接将你的 frameImages 数组中的每个帧输出到画布,看看它是否仍然挂起。这样做的结果会让您知道您的代码是否可以到达 frameImages 中的下一帧,或者是否不能。这可能会让您更接近错误的根源。
currentFrame 是否用 var 声明并在 processFrame 范围内?如果没有,就放
var currentFrame = 0;
上面定义function processFrame(...
的地方
当您说“然后我就这样播放”时,您是使用 promise 还是在所有 onloads 触发后等效地调用您的动画开始函数? 一种方法是:您可以在每次触发 onload 时增加一个计数器,每次检查计数器是否等于所需的加载总数,然后才调用您的动画开始。 我的预感是这是一个缓存问题,因为由于内部结构不同,这些似乎是不同浏览器之间变化最大的问题。
尝试在 drawImage 调用之间和更新 currentFrame 之间清除画布,因为这可能有助于在更改图像时重置画布的内部状态。
您可以使用 HTML5 视频标签进行 alpha 和透明度。只需使用一些 svg 转换。正如this SO answer
中所说,这些很容易做到【讨论】:
视频需要透明背景,所以我不能使用视频标签。为什么我不应该使用增量?我不明白#2。稍后我会在 3 点和 4 点回复你。至少在下载图像的意义上不是缓存问题。为什么要在绘制后清除画布..? @Banath -- 好的,我很难说出 #2 我会再试一次。为什么要清除画布?好吧,我记得我过去可能通过简单地清除画布来摆脱一些 javascript 图形故障。因为,如您所知,画布是一个状态机,并且渲染被称为单步执行状态的一部分。这当然适用于绘制顶点和形状,我似乎记得它也适用于 drawImage。在内部,画布渲染操作集中在一起,然后以复合方式执行以提高效率,但其具体细节已调整。 @Banath -- 重新缓存:你怎么能确定?如果您只是通过输出“已加载图像”进行测试,那是不够的。我知道,因为我也这样做了。如果你想消除它被缓存的可能性,你必须执行所有的渲染,然后开始你的动画。执行此操作的唯一严格方法(甚至 onload 也不可靠,但仍然如此)是检查有多少图像已触发 onload,然后在加载所有图像时调用您的启动动画。从 onload 处理程序内部调用。你明白我的意思吗? @Banath -- 由于它的同步特性,它可能在该图像完全加载之前达到 currentFrame == 1。如果我没记错的话,您需要将这些图像加载到 DOM 中。即使这不是严格正确的,也可以尝试将 HTMLImageELements 添加到 DOM 中,只需使用可见性隐藏类即可。然后你可以更信任 onload 方法,我怀疑。它可能是缓存,因为这些是最难调试的。 我认为#6,SVG 解决方案效果最好,因为它将是最可靠的,尤其是在音频/视频同步方面,即使透明度剪切有点粗糙。【参考方案2】:这是另一种不太正统的解决方案 - 将视频用作画布输入并使用 getImageData 对每一帧进行绿屏。
基本上你已经有了视频标签和画布
<video id='source' style='display: none;'>
<source>whatever.mp4</source>
<source>whatever.ogg</source>
</video>
<canvas id='screen' width='videoWidth' height='videoHeight></canvas>
然后你设置你的绘图功能
var t = null;
document.getElementById('source').addEventListener('playing', function()
var vid = this;
t = setInterval(function()
if (vid.paused || vid.ended)
clearInterval(t);
return
else
drawFrame();
, 20)
, false)
你的并条机功能是
function drawFrame()
var ctx = document.getElementById('screen').getContext('2d'),
vid = document.getElementById('source');
ctx.drawImage(vid, 0, 0);
var tolerance = 0.2,
bgColor = [0, 255, 0], //or whatever
imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height),
len = imageData.data.length;
for (var i=0; i<len; i+=4)
var r = imageData.data[i],
g = imageData.data[i+1],
b = imageData.data[i+2];
//There's lots of different color comparing methods, just check google
var dif = colorCompare([r, g, b], bgColor);
if (dif < tolerance)
imageData.data[i+3] = 255;
ctx.putImageData(imageData, 0, 0);
您可以使用它的不同部分来达到您想要的性能水平。对于相对较小(尺寸方面)的视频,您的性能应该不是问题。您可能必须使用缓冲画布来避免屏幕闪烁,但我对此表示怀疑。您可以调整容差变量以避免可能得到的任何光环效应。
并不是说它是最好的解决方案,但它绝对是一个可行的解决方案,如果您想通过更改输入视频而不需要额外的工作来更改视频,那么它会起作用。
【讨论】:
在安卓和iOS设备上是不允许的。这基本上就是我为 windows phone 解决方案所做的。 @hobberwickey -- 是的,这是一个很酷的想法,但该死的安全限制。他们确实需要对这种安全模型进行更多的区分——比如,如果视频来自 FaceTime 等用户设备,他们可能只会拒绝抓取视频……否则,它所破坏的安全性是什么?? @hobberwickey -- 没有时间早点找到这篇文章,但你基本上是在用更难更糟糕的方式来做这个视频和画布透明度。假设您已经为透明设置了视频 -> jakearchibald.com/scratch/alphavid(今天在 Chrome 中不起作用;不确定为什么。)当视频看起来像时设置为透明 -> savedbythecode.com/transparentvideo.png 上半部分在哪里用户应该看到的视频,下半部分有视频部分以白色显示。如果视频还没有这样设置,您的方式将是最可行的。 @Banath 我不知道,它们并没有那么不同。唯一的区别是如果以非常一致的方式从视频中抓取透明背景,颜色比较可能会相当简单。它们都遍历图像数据,而我的似乎很少调用 drawImage。我没有意识到安全限制。太愚蠢了,他们为什么不直接实施与图像相同的跨域策略。【参考方案3】:不确定你是否看过这个……但你有没有想过使用 jquery 队列?当我将它加载到我的浏览器中时,我看到一些框架没有显示,或者空白,然后回来等等。我之前使用过队列来同步处理我想要“链”的说 ajax 调用" 以特定的顺序放在一起......所以这可能是一个您可以尝试以同步方式加载每个“图片帧”的区域,确保每个帧都以正确的顺序加载?
http://api.jquery.com/queue/
【讨论】:
以上是关于更改正在动画的精灵表时 HTML5 Canvas 挂起(逐帧播放视频)的主要内容,如果未能解决你的问题,请参考以下文章