# Sketch to video efficiently with ffmpeg

**URL:** https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522
**Category:** Libraries
**Created:** [November 18, 2020, 11:56pm UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522 "2020-11-18T23:56:53Z")
**Posts on this page:** 9
**Page:** 1

<div class="post-metadata">

### Author: ![JuanIrache](https://avatars.discourse-cdn.com/v4/letter/j/a87d85/32.png) [@JuanIrache](https://discourse.processing.org/u/JuanIrache)
#### Post date: [November 18, 2020, 11:56pm UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/1 "2020-11-18T23:56:53Z")

</div>

Hi all,

I’ve got a project where I am drawing a sketch and saving the frames as a video using ffmpeg. My project is an ElectronJS desktop app, so I have access to both the browser and the operating system. The problem I’m facing is performance is very slow due to the way I’m capturing each canvas frame. Here’s my current workflow in a nutshell:

- I create an image stream (new PassThrough) that is piped to my ffmpeg process
- After drawing each frame, I convert it to a blob with canvas.toBlob()
- I convert the blob to arrayBuffer
- And get a buffer with Buffer.from()
- I write the buffer to the image stream with stream.write()

This works well but canvas.toBlob() is incredibly slow compared with everything else. Would anyone know how to optimise this?

I’m guessing there is no way p5 could draw to anything other than a canvas, something that can be converted to a blob or buffer quicker, is there? I think even createGraphics uses a hidden canvas. And I don’t know if we can draw to an OffscreenCanvas (wich may also not be faster to save).

I also want to experiment with canvas.captureStream() + MediaRecorder, but I doubt I can pipe that to ffmpeg directly.

I think the builtin saveFrame would not help either, as that tries to download the file directly. And it may use toBlob under the hood, anyway. Not sure.

Any ideas are appreciated.

---

<div class="post-metadata">

### Author: ![tony](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/tony/32/949_2.png) [@tony](https://discourse.processing.org/u/tony)
#### Post date: [November 19, 2020, 7:10am UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/2 "2020-11-19T07:10:21Z")

</div>

What version of Electron are you using?

It sounds like Chromium \< 83 had a toBlob() performance bug:

> <https://stackoverflow.com/questions/61268950/offscreen-converttoblob-very-slow>

---

<div class="post-metadata">

### Author: ![JuanIrache](https://avatars.discourse-cdn.com/v4/letter/j/a87d85/32.png) [@JuanIrache](https://discourse.processing.org/u/JuanIrache)
#### Post date: [November 19, 2020, 8:33am UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/3 "2020-11-19T08:33:17Z")

</div>

Thanks. That’s a great catch!

I updated Electron to a version with Chromium 87 (was using 80) and unfortunately the issue is not resolved directly, as it was affecting OffscreenCanvas and not HTMLCanvasElement. However, I’m using OffscreenCanvas in a different part of my app and I see a noticeable improvement there.

I think I’ll try to copy the canvas to a worker, reproduce it as an OffscreenCanvas and create the blob there and see what happens. Thanks again

---

<div class="post-metadata">

### Author: ![JuanIrache](https://avatars.discourse-cdn.com/v4/letter/j/a87d85/32.png) [@JuanIrache](https://discourse.processing.org/u/JuanIrache)
#### Post date: [November 19, 2020, 5:48pm UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/4 "2020-11-19T17:48:04Z")

</div>

Doing the canvas to blob conversion in a worker, despite having to send messages back and forth and to recreate a new canvas, seems about 22% faster. Not groundbreaking but an improvement.

I also tried to read the pixels array from the p5 instance with loadPixels(), convert it to a PNG buffer with jimp and save that, but the process was orders of magnitude slower.

My best worflow so far goes like this:

- Draw to HTML Canvas with p5
- Convert canvas to ImageBitmap with createImageBitmap()
- Send bitmap to web worker (and pass its ownership)
- Copy bitmap to OffscreenCanvas in worker with new OffscreenCanvas(width, height).getContext(‘bitmaprenderer’).transferFromImageBitmap(bitmap);
- Convert OffscreenCanvas to blob with convertToBlob()
- Get Array Buffer from blob with arrayBuffer()
- Send back Array Buffer to main thread (and ownership)
- In main thread, convert Array Buffer to buffer with Buffer.from()
- Pipe to ffmpeg with stream.write()

I suppose there are better ways of achieving this. Suggestions are welcome

---

<div class="post-metadata">

### Author: ![UriShX](https://avatars.discourse-cdn.com/v4/letter/u/d07c76/32.png) [@UriShX](https://discourse.processing.org/u/UriShX)
#### Post date: [January 13, 2021, 8:57pm UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/5 "2021-01-13T20:57:04Z")

</div>

Hi Juan,

I’m trying to create an Electron app with similar functionality, but couldn’t get streaming to FFmpeg working at all.

Do you by any chance have some code snippets I could go by? Could you maybe point me to a suitable tutorial for webworkers and offscreen canvases? I couldn’t find anything useful in my searches.

Thanks

---

<div class="post-metadata">

### Author: ![JuanIrache](https://avatars.discourse-cdn.com/v4/letter/j/a87d85/32.png) [@JuanIrache](https://discourse.processing.org/u/JuanIrache)
#### Post date: [January 15, 2021, 11:24am UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/6 "2021-01-15T11:24:39Z")

</div>

Hi @UriShX ,

My implementation is hard to decypher because it includes many details that only apply to my case. I think your best bet is to google for each one of the functions I mention in my previous post. That’s how I found out about them. Some details that may help you, though:

This is what I’m using for the image stream:

```auto
imagesStream = new PassThrough();

```

This is the input I’m specifying to ffmpeg

```auto
-f image2pipe -i -

```

The dash at the end is necessary.

I execute ffmpeg with something like

```auto
const { execFile } = require('child_process');

const child = execFile('path/to/ffmpeg', ffmpeg_args, () => {
  // Callback
});

// And then use the child with the stream like

imagesStream.pipe(child.stdin);

```

This is mostly what I do to write each frame (some of the workflow and error handling is missing)

```auto
// Tell the worker the size of the canvas when sending the first frame
if (frame === 0) {
  saveWorker.postMessage({
    action: 'setSize',
    payload: { size: [canvas.width, canvas.height] }
  });
}

// Convert canvas to bitmap
const bitmap = await createImageBitmap(canvas);

// Prepare for when the worker returns the data
saveWorker.onmessage = ({ data }) => {
  const buf = Buffer.from(data.payload, 'base64');
  // The stream tells me if it's ready to receive more data
  const ok = imagesStream.write(buf, 'utf8', () => {
    // If we know it's the last frame, we close the stream
    if (imagesStream && lastFrame) imagesStream.end();
  });
  if (ok) {
    // Go on with your program, draw the next frame, etc
  } else if (imagesStream)
    imagesStream.once('drain', () => {
      // The stream is not ready, this will run once it is so you can continue processing more frames
    });
};

// We send the bitmap to the worker
saveWorker.postMessage(
  {
    action: 'saveFrame',
    payload: { bitmap, stream: true }
  },
  [bitmap]
);

```

And this is my web worker file

```auto
const canvas = new OffscreenCanvas(300, 300);
const ctx = canvas.getContext('bitmaprenderer');

const saveFrame = async ({ bitmap }) => {
  ctx.transferFromImageBitmap(bitmap);
  const blob = await canvas.convertToBlob();
  const arrBuf = await blob.arrayBuffer();
  self.postMessage({ action: 'success', payload: arrBuf }, [arrBuf]);
};

self.onmessage = ({ data }) => {
  const { action, payload } = data;
  if (action === 'setSize') {
    const { size } = payload;
    canvas.width = size[0];
    canvas.height = size[1];
  }
};

```

---

<div class="post-metadata">

### Author: ![UriShX](https://avatars.discourse-cdn.com/v4/letter/u/d07c76/32.png) [@UriShX](https://discourse.processing.org/u/UriShX)
#### Post date: [January 17, 2021, 10:54am UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/7 "2021-01-17T10:54:49Z")

</div>

Hi Juan,

Thanks mate, that really helps me a lot.

---

<div class="post-metadata">

### Author: ![JuanIrache](https://avatars.discourse-cdn.com/v4/letter/j/a87d85/32.png) [@JuanIrache](https://discourse.processing.org/u/JuanIrache)
#### Post date: [April 10, 2021, 7:50pm UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/8 "2021-04-10T19:50:46Z")

</div>

Still looking for a more efficient solution. I have tried everything that I was able to code.

I wonder if there is a way to tell ffmpeg where the canvas pixels are in memory so it reads them from there, instead of doing format conversions. Maybe studying how createImageBitmap works internally is a step in that direction… Not sure

---

<div class="post-metadata">

### Author: ![JuanIrache](https://avatars.discourse-cdn.com/v4/letter/j/a87d85/32.png) [@JuanIrache](https://discourse.processing.org/u/JuanIrache)
#### Post date: [May 11, 2021, 12:23am UTC](https://discourse.processing.org/t/sketch-to-video-efficiently-with-ffmpeg/25522/9 "2021-05-11T00:23:40Z")

</div>

I created this kind of minimal project that compares the render times of different approaches. [GitHub - JuanIrache/canvas-stream-test: Try to improve transferring canvas frames to ffmpeg](https://github.com/JuanIrache/canvas-stream-test)

There, running ffmpeg in a web worker is much faster than the other ideas, but in real world projects with more video streams and complex calculations within the sketch the original “worker” approach described in this forum is still faster. I don’t know why.
