1

i'm trying to write a node video app that generates frames using the canvas api (via node-canvas, the project's only npm dependancy right now), and writes it to ffmpeg via a stream to generate a video:

const { createCanvas } = require('canvas');
const { spawn } = require('child_process');
const fs = require('fs');
const canvas = createCanvas(1280, 720);

const ffmpeg = spawn('ffmpeg', [
    '-y',
    '-f', 'rawVideo',
    '-vcodec', 'rawVideo',
    '-pix_fmt', 'rgb24',
    '-s', `${ canvas.width }x${ canvas.height }`,
    '-r', '40',
    '-i', '-', '-f', 'mp4',
    '-q:v', '5',
    '-an', '-vcodec', 'mpeg4', 'output.mp4',
]);

const ctx = canvas.getContext('2d');
ctx.font = '30px Prime';
ctx.fillStyle = 'blue';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Hello Canvas', canvas.width / 2, canvas.height / 2);

for (let i = 0; i < 250; ++i)
{
    console.log(i);
    ffmpeg.stdin.write(Buffer.from(ctx.getImageData(0, 0, canvas.width, canvas.height).data));
}
ffmpeg.stdin.end();

unfortunately, when i run it, the program throws this after writing the frames:

node:events:368
      throw er; // Unhandled 'error' event
      ^

Error: write EPIPE
    at WriteWrap.onWriteComplete [as oncomplete] (node:internal/stream_base_commons:98:16)
Emitted 'error' event on Socket instance at:
    at emitErrorNT (node:internal/streams/destroy:164:8)
    at emitErrorCloseNT (node:internal/streams/destroy:129:3)
    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
  errno: -32,
  code: 'EPIPE',
  syscall: 'write'
}

Node.js v17.1.0

what am i doing wrong?

K. Russell Smith
  • 133
  • 1
  • 11
  • 1
    Have you checked the `stderr` log of ffmpeg? ` '-f', 'rawVideo','-vcodec', 'rawVideo'` jumps out at me as `rawVideo` is neither a format nor codec. Try `rawvideo`. – kesh Sep 24 '22 at 21:32
  • 1
    Writing to streams needs to be properly flow controlled. `ffmpeg.stdin.write()` returns `false` if the stream buffers are full and you should not write any more until you get a `drain` event. Lots of examples of how to write this code in the nodejs stream doc. – jfriend00 Sep 24 '22 at 22:05

1 Answers1

0

so, with the responses i got pointing me in the right direction, i was able to correct the syntax errors in my ffmpeg spawn, and then made it so the canvas data would be reencoded to 24-bit rgb (since mp4 doesn't support an alpha channel); these fixed my initial problems. and then i properly set the write procedure to drain:

const { createCanvas } = require('canvas');
const { spawn } = require('child_process');

const video = {
    title:    'canvas',
    width:    1280,
    height:   720,
    fps:      25,
    duration: 10000,
}
const ffmpeg = spawn('ffmpeg', [
    '-y',
    '-f', 'rawvideo',
    '-vcodec', 'rawvideo',
    '-pix_fmt', 'rgb24',
    '-s', `${ video.width }x${ video.height }`,
    '-r', `${ video.fps }`,
    '-i', '-', '-f', 'mp4',
    '-q:v', '5',
    '-an', '-vcodec', 'mpeg4', `${ video.title }.mp4`,
    '-report',
]);
const canvas = createCanvas(video.width, video.height);
const ctx = canvas.getContext('2d');

const draw = delta =>
{
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.font = '60px Prime';
    ctx.fillStyle = 'blue';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('Hello Canvas', canvas.width / 2, canvas.height / 2);
}
// mp4 does not support transparency:
const canvas_to_raw_rgb24 = ctx =>
{
    const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
    const rgb24 = new Uint8Array(data.length * 0.75);
    for (let i = 0, j = 0; i < data.length; ++i)
    {
        rgb24[j++] = data[i++];
        rgb24[j++] = data[i++];
        rgb24[j++] = data[i++];
    }
    return rgb24;
}

// Write the video
(() =>
{
    const frames = Math.floor(video.duration / 1000 * video.fps);
    let frame = frames;
    write();
    function write()
    {
        let ok = true;
        do
        {
            const delta = video.duration - (video.duration / frames * frame);
            draw(delta);
            --frame;
            const data = canvas_to_raw_rgb24(ctx);
            if (frame === 0)
            {
                ffmpeg.stdin.write(data);
            }
            else
            {
                ok = ffmpeg.stdin.write(data);
            }
        } while (frame > 0 && ok)
        if (frame > 0)
        {
            ffmpeg.stdin.once('drain', write);
        }
        else
        {
            ffmpeg.stdin.end();
        }
    }
})();

so at last, i have a robust base that generates a playable video

K. Russell Smith
  • 133
  • 1
  • 11