[英]Why is video from canvas.captureStream empty when using drawImage with MediaRecorder
I have a perfectly working demo animation within a canvas element that I can record as a webm video file with MediaRecorder and captureStream()
from the <canvas>
element. 我在canvas元素中有一个运行良好的演示动画,可以将它与MediaRecorder和<canvas>
元素的captureStream()
一起记录为webm视频文件。
The animations from the 2d context api come out just fine in the resulting video, but when I try to use drawImage()
in order to add an image to the canvas I can't seem to make it work properly. 来自2d上下文api的动画在生成的视频中效果很好,但是当我尝试使用drawImage()
以便将图像添加到画布时,我似乎无法使其正常工作。 In the latter case the MediaRecorder.ondataavailable
handler doesn't receive valid data and the resulting video file is a 0-byte file. 在后一种情况下, MediaRecorder.ondataavailable
处理程序无法接收到有效数据,并且生成的视频文件为0字节文件。
I even implemented a demo where I can toggle whether or not a drawImage()
call is performed. 我什至实现了一个演示,在其中可以切换是否执行drawImage()
调用。 in the code below, if drawImage = false
the video is generated without a problem, but if drawImage
is toggled to true
, it will generate a 0-byte file instead. 在下面的代码中,如果drawImage = false
,则生成视频没有问题,但是如果将drawImage
切换为true
,它将生成一个0字节的文件。
In order to demonstrate, I put together this jsfiddle https://jsfiddle.net/keyboardsamurai/3tkm0dp6/16/ 为了演示,我将这个jsfiddle放在一起https://jsfiddle.net/keyboardsamurai/3tkm0dp6/16/
I am running this code on "Chrome Version 75.0.3770.100 (Official Build) (64-Bit)" on MacOS - not even sure if it is supposed to run on Firefox etc. since the MediaRecorder API throws seemingly unrelated errors on FF. 我在MacOS的“ Chrome版本75.0.3770.100(正式版本)(64位)”上运行此代码-甚至不确定它是否应该在Firefox等上运行,因为MediaRecorder API在FF上引发了看似无关的错误。
See also the full code here: 另请参见完整代码:
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<canvas id="drawing_canvas" width="1280" height="720"></canvas>
<script>
const image = new Image();
image.src = 'https://66.media.tumblr.com/84d332cafeb1052c477c979281e5713b/tumblr_owe3l0tkCj1wxdq3zo1_1280.jpg';
window.requestAnimationFrame(animation);
const drawImage = false; // toggle to 'true' to make this example fail
const canvas = document.getElementById('drawing_canvas');
const allChunks = [];
const recorder = initMediaRecorder(canvas);
recorder.start();
setTimeout(function (e) {
console.log("Video ended");
recorder.stop();
}, 5000);
function initMediaRecorder(canvasElement) {
const stream = canvasElement.captureStream(60);
const recorder = new MediaRecorder(stream, {mimeType: 'video/webm'});
recorder.ondataavailable = function (e) {
console.log("data handler called");
if (e.data) {
console.log("data available: " + e.data.size)
if (e.data.size > 0) {
console.log("data added");
allChunks.push(e.data);
}
} else {
console.error("Data handler received no data in event: " + JSON.stringify(e))
}
};
recorder.onstop = function (e) {
const fullBlob = new Blob(allChunks);
const link = document.createElement('a');
link.style.display = 'none';
link.href = window.URL.createObjectURL(fullBlob);
link.download = 'media.webm';
document.body.appendChild(link);
link.click();
link.remove();
};
return recorder;
}
function animation() {
const now = new Date();
const ctx = document.getElementById('drawing_canvas').getContext('2d');
if (drawImage) {
ctx.drawImage(image, 0, 0);
}
ctx.clearRect(0, 0, 150, 150);
ctx.strokeStyle = 'white';
ctx.fillStyle = 'white';
ctx.rect(0, 0, 1280, 720);
ctx.stroke();
ctx.save();
ctx.translate(75, 75);
ctx.scale(0.4, 0.4);
ctx.rotate(-Math.PI / 2);
ctx.strokeStyle = 'black';
ctx.fillStyle = 'white';
ctx.lineWidth = 8;
ctx.lineCap = 'round';
// Hour marks
ctx.save();
for (var i = 0; i < 12; i++) {
ctx.beginPath();
ctx.rotate(Math.PI / 6);
ctx.moveTo(100, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.restore();
// Minute marks
ctx.save();
ctx.lineWidth = 5;
for (i = 0; i < 60; i++) {
if (i % 5 != 0) {
ctx.beginPath();
ctx.moveTo(117, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.rotate(Math.PI / 30);
}
ctx.restore();
const sec = now.getSeconds();
const min = now.getMinutes();
let hr = now.getHours();
hr = hr >= 12 ? hr - 12 : hr;
ctx.fillStyle = 'black';
// write Hours
ctx.save();
ctx.rotate(hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) * sec);
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(-20, 0);
ctx.lineTo(80, 0);
ctx.stroke();
ctx.restore();
// write Minutes
ctx.save();
ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(-28, 0);
ctx.lineTo(112, 0);
ctx.stroke();
ctx.restore();
// Write seconds
ctx.save();
ctx.rotate(sec * Math.PI / 30);
ctx.strokeStyle = '#D40000';
ctx.fillStyle = '#D40000';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(-30, 0);
ctx.lineTo(83, 0);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
ctx.stroke();
ctx.fillStyle = 'rgba(0, 0, 0, 0)';
ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
ctx.fill();
ctx.restore();
ctx.beginPath();
ctx.lineWidth = 14;
ctx.strokeStyle = '#325FA2';
ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
ctx.stroke();
ctx.restore();
window.requestAnimationFrame(animation);
}
</script>
</body>
</html>
Update : The above behavior is confirmed to be replicable on at least these Chrom(e/ium) versions: 更新 :至少在以下这些Chrom(e / ium)版本上, 确认了上述行为是可复制的 :
Version 75.0.3770.100 (Official Build) (64-Bit) on MacOS Mojave 10.14.5 MacOS Mojave 10.14.5上的版本75.0.3770.100(官方内部版本)(64位)
Version 77.0.3849.0 (Official Build) canary (64-Bit) on MacOS Mojave 10.14.5 MacOS Mojave 10.14.5上的77.0.3849.0版(正式内部版本)金丝雀(64位)
Version 77.0.3770.100 (Official Build) snap (64-bit) on Ubuntu 19.04 Disco Dingo Ubuntu 19.04 Disco Dingo上的版本77.0.3770.100(Official Build)快照(64位)
That happens because your image is coming from a cross-domain resource and has tainted your canvas. 发生这种情况是因为您的图像来自跨域资源,并且污染了画布。
Tainting a canvas from which a MediaStream is being captured will stop said MediaStream from capturing any new image. 污染从中捕获MediaStream的画布将阻止所述MediaStream捕获任何新图像。
Also, trying to capture a MediaStream from such a tainted canvas will throw a SecurityError. 此外,尝试从此类污染的画布捕获MediaStream会引发SecurityError。
const ctx = canvas.getContext('2d'); const stream = canvas.captureStream(); vid.srcObject = stream; const img = new Image(); img.onload = e => { console.log('will taint the canvas') ctx.drawImage(img, 0, 0); // and if we try now to capture a new stream, we have a clear error const stream2 = canvas.captureStream(); } img.src = "https://66.media.tumblr.com/84d332cafeb1052c477c979281e5713b/tumblr_owe3l0tkCj1wxdq3zo1_1280.jpg"; ctx.fillRect(0,0,20,20);
<canvas id="canvas"></canvas> <video id="vid" controls autoplay muted></video>
To circumvent it, you need the server to send the image in a cross-origin compliant way, by setting correctly the Access-control-origin headers to accept your own domain, then requesting this image with the crossorigin
attribute. 要绕过它,您需要服务器以跨域兼容的方式发送图像,方法是正确设置Access-control-origin标头以接受您自己的域,然后使用crossorigin
属性请求此图像。 The server from which you are loading this particular image does allow anyone to access their data in such a cross-origin compliant way, so we can demonstrate the front-end part: 您从中加载该特定图像的服务器确实允许任何人以这种跨域兼容的方式访问其数据,因此我们可以演示前端部分:
const ctx = canvas.getContext('2d'); const stream = canvas.captureStream(); vid.srcObject = stream; const img = new Image(); img.crossOrigin = 'anonymous'; // add this to request the image as cross-origin allowed img.onload = e => { console.log('will not taint the canvas anymore') ctx.drawImage(img, 0, 0); // and if we try now to capture a new stream, we have a clear error const stream2 = canvas.captureStream(); } img.src = "https://66.media.tumblr.com/84d332cafeb1052c477c979281e5713b/tumblr_owe3l0tkCj1wxdq3zo1_1280.jpg"; ctx.fillRect(0,0,20,20);
<canvas id="canvas"></canvas> <video id="vid" controls autoplay muted></video>
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.