Почему видео из canvas.captureStream пустое при использовании drawImage с MediaRecorder - PullRequest
0 голосов
/ 10 июля 2019

У меня есть отлично работающая демонстрационная анимация в элементе canvas, которую я могу записать в виде веб-видеофайла с помощью MediaRecorder и captureStream() из элемента <canvas>.

Анимации из API 2-контекста контекста прекрасно воспроизводятся в полученном видео, но когда я пытаюсь использовать drawImage() для добавления изображения на холст, я не могу заставить его работать должным образом,В последнем случае обработчик MediaRecorder.ondataavailable не получает действительные данные, а полученный видеофайл представляет собой 0-байтовый файл.

Я даже реализовал демонстрационную программу, в которой я могу переключаться независимо от того, вызывается ли drawImage()выполняется.в приведенном ниже коде, если drawImage = false видео генерируется без проблем, но если drawImage переключено на true, вместо этого будет сгенерирован 0-байтовый файл.

Для демонстрации,Я собрал этот jsfiddle https://jsfiddle.net/keyboardsamurai/3tkm0dp6/16/

Я выполняю этот код на "Chrome версии 75.0.3770.100 (Официальная сборка) (64-разрядная версия)" на MacOS - даже не уверен, что он должен работать на Firefoxи т. д., поскольку API MediaRecorder генерирует кажущиеся несвязанными ошибки в 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):

Версия 75.0.3770.100 (официальная сборка) (64-разрядная версия) в MacOS Mojave 10.14.5

Версия 77.0.3849.0 (Официальная сборка) (64-разрядная версия) на MacOS Mojave 10.14.5

Версия 77.0.3770.100 (Официальная сборка) (64-разрядная версия) на Ubuntu 19.04 Disco Dingo

1 Ответ

1 голос
/ 11 июля 2019

Это происходит потому, что ваше изображение исходит из междоменного ресурса и испортило ваш холст.
Нарушение холста, с которого захватывается 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>

Чтобы обойти это, вам нужен сервер для отправки изображения в соответствии с требованиями к разным источникам, правильно настроив заголовки источника контроля доступа для принятия вашего собственного домена, а затем запросив это изображение с атрибутом 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>
...