【发布时间】:2018-01-22 15:27:41
【问题描述】:
我正在开发一个基于视频帧的具有一些特殊功能的画布视频播放器。为了克服视频 HTML5 标记中不可靠的时序,我们使用的视频在每个帧中嵌入了一个条形码,指示当前帧号。使用画布 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]
正确计算为帧号 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 的三星 6 (Samsung SM G920A)
- 它不能在运行 Android v7.0 的三星 Galaxy S7 (SAMSUNG-SM-G935A) 上工作
这可能是 Android 7 问题吗?
编辑 2
回答cmets中的一个问题:
videoNode.videoHeight 和 videoWidth 在谷歌像素上都是 0,但在桌面上是一样的。在我遇到的两种不工作的设备中,每一帧的图像都是完全绘制的。我将附上谷歌像素的屏幕截图。 暂停时,它始终读取相同的数字。 换句话说,它不会跳来跳去,因此它正在读取的内容确实在视频的帧上。
编辑 3:发现
我相信我已经做出了我应该早点看到的相关发现/实现。
查看损坏设备上 getImageData 的输出时,我正在逐行执行。我没有(并且应该)注意到的是,视频元素在达到我的断点/调试器语句后会稍微继续。到执行 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 方法出现问题。
编辑 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,但似乎并非如此。
我的谷歌驱动器上有一个CSV,上面有这个测试的结果。并不是说这些测试是 100% 可靠的,但它似乎只是一些随机设备......
【问题讨论】:
-
此设备上@987654335@ 和
videoNode.videoWidth的值是多少?如果您不对画布的宽度和高度进行硬编码,而是使用这些值以及drawImage(vid, 0,0,vid.videoWidth,vid.videoHeight),会发生什么情况。所有的图像都画在画布的所有表面上吗?是否仅在播放视频时发生(即在视频暂停时是否有效?) -
嘿@Kaiido 我已经进行了编辑以解决您的问题。谢谢!
-
对于 videoHeight/videoWidth 需要等待 onloadedmetadata 事件。它不能是 0。
-
你写的条码怎么样?
-
@clabe45 很抱歉,我实际上并不知道。它们是由另一个团队制作的。我的猜测是 After Effects。
标签: javascript html canvas