javascript - 为什么使用带有 MediaRecorder 的 drawImage 时 canvas.captureStream 中的视频为空
问题描述
captureStream()
我在画布元素中有一个完美运行的演示动画,我可以使用 MediaRecorder 和元素将其录制为 webm 视频文件<canvas>
。
来自 2d 上下文 api 的动画在生成的视频中表现得很好,但是当我尝试使用drawImage()
以将图像添加到画布时,我似乎无法使其正常工作。在后一种情况下,MediaRecorder.ondataavailable
处理程序不会接收到有效数据,并且生成的视频文件是 0 字节文件。
我什至实现了一个演示,我可以在其中切换是否drawImage()
执行呼叫。在下面的代码中,如果drawImage = false
视频生成没有问题,但如果drawImage
切换到true
,它将生成一个 0 字节的文件。
为了演示,我把这个jsfiddle放在一起https://jsfiddle.net/keyboardsamurai/3tkm0dp6/16/
我在 MacOS 上的“Chrome 版本 75.0.3770.100(官方构建)(64 位)”上运行此代码 - 甚至不确定它是否应该在 Firefox 等上运行,因为 MediaRecorder API 在 FF 上抛出看似无关的错误。
另请参阅此处的完整代码:
<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>
更新:确认上述行为至少可在以下 Chrom(e/ium) 版本上复制:
MacOS Mojave 10.14.5 上的版本 75.0.3770.100(官方构建)(64 位)
MacOS Mojave 10.14.5 上的版本 77.0.3849.0(官方构建)金丝雀(64 位)
Ubuntu 19.04 Disco Dingo 上的版本 77.0.3770.100(官方构建)快照(64 位)
解决方案
发生这种情况是因为您的图像来自跨域资源并且污染了您的画布。
污染从中捕获 MediaStream 的画布将阻止所述 MediaStream 捕获任何新图像。
此外,尝试从这种受污染的画布中捕获 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>
为了规避它,您需要服务器以跨域兼容的方式发送图像,方法是正确设置 Access-control-origin 标头以接受您自己的域,然后使用crossorigin
属性请求此图像。您从中加载此特定图像的服务器确实允许任何人以这种跨域兼容的方式访问他们的数据,因此我们可以演示前端部分:
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>
推荐阅读
- visual-studio - Xamarin 部署到 Android
- ios - Swift - 如何让 NSTimeZone 给我“太平洋标准时间”或“山地时间”等?
- angular - 在 Angular 中正确使用 ngClass
- vue.js - Vue.js - 组件渲染函数中的无限更新循环
- apache-spark - SHOW PARTITIONS 是否不适用于临时视图
- c# - 加载到富文本框中时,富文本文件中的 C# 图像不居中
- mysql - GORM分页和订单问题
- c - cs50 潮人(将信息从一个数组转换为另一个更大的数组)
- ios - 关于在 Swift 中删除对 Delegate Class 和 ViewController 的直接引用的问题
- java - 在 Firestore 中存储子集合数据