Подтвердить что ты не робот

Canvas getImageData возвращает неверные данные на определенные мобильные устройства

Я работаю над видеопроигрывателем на холсте с некоторыми специальными функциями, основанными на кадрах видео. Чтобы преодолеть ненадежное время в видеотеке HTML5, используемые нами видео имеют штрих-код, встроенный в каждый кадр, указывающий текущий номер кадра. Используя метод canvas getImageData, я могу захватить пиксели и прочитать штрих-код, чтобы получить номер кадра. Это отлично работает, и у меня есть JSFiddle, демонстрирующий, что он работает (я не мог обойти CORS в этой скрипке, чтобы показывать видео на холст, чтобы увидеть его работу, вам придется загружать пример видео локально, а затем загружать его с помощью кнопки. Не идеально, но он работает).

На некоторых мобильных устройствах (только Android до сих пор) эта логика ломается. GetImageData возвращает неверные значения.

Он корректно работает на моем Samsung Galaxy S5 v6.0.1, но не работает на Google Pixel, работающем под управлением Android v7.1.2. Я попытаюсь собрать больше данных о том, какие устройства/версии ОС он терпит неудачу.

Например, при воспроизведении на рабочем столе возвращается первая итерация 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]

который правильно вычисляется как номер кадра 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)
})();

ИЗМЕНИТЬ

  • Работает на Nexus 6P под управлением Android v6.0
  • Работает на Samsung 6 (Samsung SM G920A) под управлением Android v 5.0.2.
  • НЕ работает на Samsung Galaxy S7 (SAMSUNG-SM-G935A) под управлением Android v7.0

Возможно, это может быть проблема Android 7?

Изменить 2

В ответ на вопрос в комментариях:

videoNode.videoHeight и videoWidth равны 0 в пикселе google для всего их существования, но это то же самое, что и на рабочем столе. На обоих устройствах, которые не работают, я столкнулся с изображением каждого кадра, полностью раскрашен. Я приложу скриншот из пикселя google. При паузе он последовательно читает один и тот же номер. Другими словами, он не прыгает, так что все, что он читает, действительно находится на кадре видео. введите описание изображения здесь

РЕДАКТИРОВАТЬ 3: Открытие

Я считаю, что я сделал соответствующее открытие/реализацию, которое я должен был увидеть раньше.

При просмотре вывода getImageData на сломанном устройстве я проходил строчку за строкой. То, что я не заметил (и должен был) заметить, что элемент видео продолжал немного после того, как он попал в мои операторы break points/debugger. К тому времени, когда был запущен метод getImageData, видео прошло мимо следующего кадра. Итак, сканированный штрих-код был на самом деле гораздо более ранним, чем ожидалось.

Я добавил несколько консольных журнальных операторов и позволяю им работать естественным образом. Глядя на результат, я вижу гораздо более узнаваемый паттерн.

Вот первые несколько показаний на пикселе google:

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 на пиксель, и он дает тот же самый ответ, что и на пикселе.

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

Выполнение -1 похоже не имеет эффекта.

Итак, по какой-то причине эти устройства либо смещают всю текстуру на пиксель, либо что-то не так с методом getImageData.

EDIT 4

В качестве эксперимента я переконфигурировал свой код для использования текстуры webGL. Такое же поведение на настольных/мобильных устройствах. Это позволило мне использовать -1 в качестве цели x, используя gl.readPixels. Я надеялся, что, пропуская использование холста, все изображение будет храниться в памяти, и я мог бы получить доступ к данным пикселя, которые мне нужны... Не работает, но вот данные, которые он произвел, что показывает, что он также сдвигается с использованием чисто 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

Хорошо, я пообещал, что получу более подробную информацию о том, какие устройства терпят неудачу/нет. У меня было какое-то онлайн-тестирование QA, проведенное с использованием слегка измененного JSFiddle. Я немного изменил его, чтобы помочь сделать его немного более идиотским доказательством для широкой общественности, с которой можно работать.

Ответы, к сожалению, были довольно неоднозначными. Я надеялся, что он будет изолирован от Android 7, но это, похоже, не так.

У меня есть CSV на моем диске Google с результатами этого теста. Не то, чтобы эти тесты были на 100% надежными, но кажется, что это просто некоторые случайные устройства...

4b9b3361

Ответ 1

Ваш код в порядке, поэтому проблема здесь заключается в том, как некоторые андроиды отображают ваш штрих-код. Реализация вашего штрих-кода слишком мала (16x1 пикселей), и она оставила отступом.

Из-за того, что вы оставлены с отступом, любой анти-псевдоним, который устройство создает на внешних пикселях, испортит ваш штрих-код и даст вам неправильные результаты, поэтому при работе с видео-рендерингом вы определенно не хотите работать в одной пиксельной безопасной области.

Мое предложение состояло в том, чтобы переделать штрих-коды к большему размеру - допустим, высота в 18 пикселей - использовать только черно-белый (без серых), центрировать его на видео, а остальная часть линии окрашивает его в зеленый цвет: (в этом например, это штрих-код для "1" )

Пример штрих-кода, который даст вам значение

Затем создайте GetImageData из полного 320x16 и избавьтесь от всего, что имеет RGB 0x255x0 (и приблизительный), и у вас есть свой штрих-код, который сможет использовать ваш FrameNumber.

Я знаю, что вам, вероятно, нужен ответ, где не нужно будет повторять видео, но в этом случае источником является проблема.