Canvas getImageData在某些移动设备上返回不正确的数据

时间:2017-08-14 18:10:35

标签: javascript html5 canvas

我正在制作一个画布视频播放器,它具有一些基于视频帧的特殊功能。为了克服视频HTML5标签中不可靠的时序,我们使用的视频在每个帧中都嵌入了条形码,指示当前帧编号。使用canvas getImageData方法,我可以抓取像素并读取条形码以获取帧编号。这很有效,我有一个JSFiddle证明它有效(我无法绕过CORS在这个小提琴中将视频提供给画布,所以要看到它正常工作你必须下载示例视频本地然后通过按钮上传。不理想,但它的工作原理)。

在某些移动设备上(到目前为止仅限Android),此逻辑会中断。 getImageData返回不正确的值。

它在我的三星Galaxy S5 v6.0.1上正常工作,但在运行android v7.1.2的Google Pixel上失败。我将尝试收集有关其失败的设备/操作系统版本的更多数据。

例如,在桌面上播放时,getImageData的第一次迭代会返回:

Uint8ClampedArray(64) [3, 2, 3, 255, 255, 255, 255, 255, 246, 245, 247, 255, 243, 242, 243, 255, 241, 239, 241, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255]

正确计算为framenumber 1。

然而在星系中,第一次迭代返回:

Uint8ClampedArray(64) [255, 242, 217, 255, 255, 234, 209, 255, 41, 1, 1, 255, 254, 235, 210, 255, 255, 234, 209, 255, 50, 4, 0, 255, 254, 240, 215, 255, 255, 248, 224, 255, 255, 249, 225, 255, 255, 251, 232, 255, 255, 252, 233, 255, 255, 252, 233, 255, 255, 253, 234, 255, 255, 255, 237, 255, 255, 255, 237, 255, 28, 1, 1, 255] 

我读到某些设备可能正在进行额外的平滑处理,因此我一直在通过以下方式禁用它:

this.ctx.mozImageSmoothingEnabled = false;
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.msImageSmoothingEnabled = false;
this.ctx.imageSmoothingEnabled = false; 

但它没有帮助。

这是JSFiddle中使用的代码。

var frameNumberDiv = document.getElementById('frameNumber');
function load() {
    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    var video = document.getElementById('video');
    canvas.width = 568;
    canvas.height = 640;
    video.addEventListener('play', function() {
        var that = this; //cache
        (function loop() {
            if (!that.paused && !that.ended) {
                ctx.drawImage(that, 0, 0);
                var pixels = ctx.getImageData(0, 320 - 1, 16, 1).data;
                getFrameNumber(pixels);
                setTimeout(loop, 1000 / 30); // drawing at 30fps
            }
        })();
    }, 0);
}

function getFrameNumber(pixels) {

    let j = 0;
    let thisFrameNumber = 0;
    let str = "Pixels: ";
    for (let i = 0; i < 16; i++) {
        str += pixels[j] + " ";
        thisFrameNumber += getBinary(pixels[j], i);
        j += 4;
    }
    document.getElementById('frameNumber').innerHTML = "FrameNumber: " + thisFrameNumber;
}

function getBinary(pixel, binaryPlace) {
    const binary = [1, 2, 4, 8, 16, 32, 64, 128, 256,
        512, 1024, 2048, 4096, 8192, 16384, 32768
    ];
    if (pixel > 128) return 0;
    if (pixel < 128 && binary[binaryPlace]) {
        return binary[binaryPlace]
    } else {
        return 0;
    }
}

(function localFileVideoPlayer() {
    'use strict';
    var URL = window.URL || window.webkitURL;
    var displayMessage = function(message, isError) {
        var element = document.querySelector('#message');
        element.innerHTML = message;
        element.className = isError ? 'error' : 'info';
    }
    var playSelectedFile = function(event) {
            console.log("Playing");
        var file = this.files[0];
        var type = file.type;
        var videoNode = document.querySelector('video');
        var canPlay = videoNode.canPlayType(type);
        if (canPlay === '') canPlay = 'no';
        var message = 'Can play type "' + type + '": ' + canPlay;
        var isError = canPlay === 'no';
        displayMessage(message, isError);

        if (isError) {
            return;
        }

        var fileURL = URL.createObjectURL(file);
        videoNode.src = fileURL;
        load();
    }
    var inputNode = document.querySelector('input')
    inputNode.addEventListener('change', playSelectedFile, false)
})();

修改

  • 适用于运行Android v6.0的Nexus 6P
  • 适用于运行Android v 5.0.2
  • 的Samsung 6(Samsung SM G920A)
  • 在运行Android v7.0的三星Galaxy S7(SAMSUNG-SM-G935A)上工作

这可能是 Android 7 问题吗?

修改2

回应评论中的问题:

videoNode.videoHeight和videoWidth在整个Google存储像素上均为0,但这与桌面上的相同。在我所遇到的两种不能正常工作的设备中,每个帧的图像都被完全绘制。我将附加一个谷歌像素的屏幕截图。 暂停时,它始终读取相同的数字。换句话说,它不会四处跳跃,所以它正在读取的内容确实在视频的帧上。 enter image description here

编辑3:发现

我相信我已经做了一个相关的发现/实现,我应该早些看到。

在破坏的设备上查看getImageData的输出时,我逐行地逐步执行。我没注意到(并且应该注意到的)是视频元素在达到我的断点/调试器语句之后仍在继续。到执行getImageData方法时,视频已移过下一帧。因此,扫描的条形码实际上是比预期更晚的帧。

我添加了一些控制台日志语句,让它自然运行。看看输出,我可以看到一个更容易识别的模式。

以下是谷歌像素的前几个读数:

Uint8ClampedArray(64) [255, 255, 255, 255, 246, 246, 246, 255, 243, 243, 243, 255, 240, 240, 241, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 2, 2, 2, 255]

Uint8ClampedArray(64) [5, 5, 5, 255, 255, 255, 255, 255, 251, 251, 251, 255, 247, 247, 248, 255, 245, 245, 245, 255, 245, 245, 245, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 6, 3, 2, 255]

Uint8ClampedArray(64) [235, 231, 230, 255, 17, 12, 12, 255, 252, 247, 247, 255, 255, 255, 255, 255, 255, 254, 254, 255, 255, 253, 253, 255, 255, 252, 251, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 7, 3, 1, 255]

Uint8ClampedArray(64) [26, 15, 14, 255, 4, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 10, 0, 0, 255]

正如您可能注意到的那样,结果似乎是正确的,但是它们向左移动了一个像素。

我稍微修改了JSFiddle以将读取的getImageData移位一个像素,它给出了与Pixel完全相同的响应。

var pixels = ctx.getImageData(1, 320 - 1, 16, 1).data;

做-1似乎没有效果。

因此,由于某种原因,这些设备要么将整个纹理移动一个像素,要么getImageData方法有问题。

编辑4

作为一项实验,我重新配置了我的代码以使用webGL纹理。桌面/移动设备上的行为相同。这允许我使用gl.readPixels将-1用作x目标。我希望通过跳过使用画布,整个图像将存储在内存中,我可以访问我需要的像素数据....没有工作,但这里是它生成的数据,表明它也被移动使用纯粹的webGL。

Uint8Array(64) [0, 0, 0, 0, 255, 248, 248, 255, 25, 18, 18, 255, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]

Uint8Array(64) [0, 0, 0, 0, 255, 254, 247, 255, 255, 244, 236, 255, 48, 18, 10, 255, 254, 246, 238, 255, 255, 247, 239, 255, 255, 247, 239, 255, 255, 248, 241, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 250, 240, 255, 255, 250, 240, 255]

Uint8Array(64) [0, 0, 0, 0, 31, 0, 0, 255, 254, 243, 230, 255, 43, 6, 1, 255, 254, 243, 230, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 229, 255, 255, 244, 229, 255]

使用:

gl.readPixels(-1, height, 16, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

编辑5

好的,我保证会得到更多有关哪些设备出现故障/未出现故障的详细信息。我使用略微修改的JSFiddle进行了一些在线QA测试。我对它进行了一些修改,以帮助公众更好地证明这一点。

不幸的是,答复相当复杂。我希望它能与Android 7隔离,但似乎并非如此。

我的google云端硬盘上有一个CSV,其中包含此测试的结果。并不是说这些测试是100%可靠的,但它似乎只是一些随机设备......

2 个答案:

答案 0 :(得分:3)

我一直在经历同样的问题而无法得到它。我终于在我的案例中得到了答案,听起来也像你的问题一样99%。

这是像素密度!!

您提到的所有设备上的像素密度都不同,可能有1 dpr(设备像素比)的设备正常工作,而其他设备没有。

所以在我的情况下使用p5js 我将像素密度设置为1,它就像一个魅力;

pixelDensity(1);

所以把它设置为1 dpr,你很可能好了!

我希望这可以帮助一些人,因为我花了很长时间才解决这个问题。

答案 1 :(得分:1)

您的代码很好,所以这里的问题在于一些机器人如何呈现您的条形码。条形码的实现太小(16x1像素),并且它的左侧缩进。

留下缩进设备在外部像素上做的任何反别名都会弄乱您的条形码并给您不正确的结果,因此,在处理视频渲染时,您绝对不想在一个像素安全区域上工作

我的建议是将条形码重做为更大尺寸 - 让我们说18像素高度 - 只使用黑白(不灰),将其置于视频中心,其余部分将其涂成绿色:(在这个例子中,它是#34; 1&#34;的条形码)

Barcode example that will give you the value "1"

然后制作一个完整的320x16的GetImageData并摆脱RGB为0x255x0(和近似值)的所有内容,并且你有条形码可以用来获取你的FrameNumber。

我知道你可能想要一个答案,重新录制视频是不必要的,但在这种情况下,来源就是问题。