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?
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