首页 文章

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

提问于
浏览
25

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

在某些移动设备上(到目前为止只有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)
})();

EDIT

  • 适用于运行Android v6.0的Nexus 6P

  • 适用于运行Android v 5.0.2的Samsung 6(Samsung SM G920A)

  • It DOES NOT 在运行Android v7.0的三星Galaxy S7(SAMSUNG-SM-G935A)上工作

这可能是一个_1811089问题吗?

Edit 2

回答评论中的问题:

videoNode.videoHeight和videoWidth在谷歌像素上都是0,但它们与桌面上的相同 . 在两个设备中,每个框架的图像都被完全涂上了 . 我将附上谷歌像素的屏幕截图 . When paused it consistently reads the same number. 换句话说,它不会四处跳跃,所以无论它正在阅读什么,都是真正的视频帧 .
enter image description here

EDIT 3: Discovery

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

当在破碎的设备上查看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方法有问题 .

EDIT 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);

Edit 5

好的,我保证我会得到更多有关哪些设备发生故障/未发生故障的细节 . 我使用稍微修改过的JSFiddle进行了一些在线QA测试 . 我对它进行了一些修改,以帮助公众更好地证明这一点 .

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

我的google驱动器上有CSV,带有此测试的结果 . 并不是说这些测试是100%可靠的,但它似乎只是一些随机设备......

2 回答

  • 1

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

    It's the pixel density!!

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

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

    pixelDensity(1);
    

    所以把它设置为1 dpr,你最有可能去!

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

  • 2

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

    如果设备在外部像素上留下缩进的任何反别名会使条形码变得混乱并给你不正确的结果,因此,在处理视频渲染时,你肯定不想在一个像素安全区域上工作 .

    我的建议是将条形码重做为更大的尺寸 - 比如高度为18像素 - 仅使用黑色和白色(不灰色),将其置于视频中心,其余部分将其绘制为绿色:(在此示例中,它是一个条形码为“1”)

    Barcode example that will give you the value "1"

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

    我知道你可能想要一个没有必要重做视频的答案,但在这种情况下,源是问题所在 .

相关问题