Skip to main content

Overview

Worker threads enable parallel video encoding by distributing work across multiple CPU cores. This is useful for:
  • Batch processing multiple videos simultaneously
  • Multi-quality encoding (ABR ladder)
  • Keeping main thread responsive during encoding
  • Maximizing CPU utilization on multi-core systems
node-webcodecs encoders already use async mode by default, which offloads encoding to background threads. Worker threads are useful for processing multiple videos in parallel, not individual frames.

Basic Worker Thread Setup

// main.js
const { Worker } = require('worker_threads');
const path = require('path');

async function encodeInWorker(videoData, config) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(path.join(__dirname, 'encoder-worker.js'), {
      workerData: { videoData, config }
    });

    worker.on('message', (result) => {
      resolve(result);
    });

    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) {
        reject(new Error(`Worker stopped with exit code ${code}`));
      }
    });
  });
}

// Usage
const result = await encodeInWorker(videoData, {
  codec: 'avc1.42E01E',
  width: 1920,
  height: 1080,
  bitrate: 5_000_000
});

console.log(`Encoded ${result.chunks.length} chunks`);
// encoder-worker.js
const { parentPort, workerData } = require('worker_threads');
const { VideoEncoder, VideoFrame } = require('node-webcodecs');

async function encodeVideo() {
  const { videoData, config } = workerData;
  const chunks = [];

  const encoder = new VideoEncoder({
    output: (chunk) => {
      const buffer = Buffer.alloc(chunk.byteLength);
      chunk.copyTo(buffer);
      chunks.push(buffer);
    },
    error: (err) => {
      parentPort.postMessage({ error: err.message });
    }
  });

  encoder.configure(config);

  // Encode frames
  for (const frameData of videoData) {
    const frame = new VideoFrame(frameData.data, frameData.options);
    encoder.encode(frame);
    frame.close();
  }

  await encoder.flush();
  encoder.close();

  // Send result back to main thread
  parentPort.postMessage({ chunks });
}

encodeVideo().catch(err => {
  parentPort.postMessage({ error: err.message });
});

Parallel Multi-Video Encoding

Encode multiple videos in parallel:
const { Worker } = require('worker_threads');
const path = require('path');

async function encodeMultipleVideos(videoFiles) {
  console.log(`Encoding ${videoFiles.length} videos in parallel...`);

  const workers = videoFiles.map(file => {
    return new Promise((resolve, reject) => {
      const worker = new Worker(
        path.join(__dirname, 'video-encoder-worker.js'),
        { workerData: { inputFile: file } }
      );

      worker.on('message', resolve);
      worker.on('error', reject);
    });
  });

  const results = await Promise.all(workers);

  console.log(`✓ Encoded ${results.length} videos`);
  return results;
}

// Encode 4 videos at once
const files = ['video1.mp4', 'video2.mp4', 'video3.mp4', 'video4.mp4'];
await encodeMultipleVideos(files);

Worker Pool Pattern

Create a pool of workers for efficient batch processing:
class WorkerPool {
  constructor(workerScript, poolSize = 4) {
    this.workerScript = workerScript;
    this.poolSize = poolSize;
    this.workers = [];
    this.queue = [];
    this.activeWorkers = 0;

    for (let i = 0; i < poolSize; i++) {
      this.workers.push(this.createWorker());
    }
  }

  createWorker() {
    return {
      worker: null,
      busy: false
    };
  }

  async execute(data) {
    return new Promise((resolve, reject) => {
      this.queue.push({ data, resolve, reject });
      this.processQueue();
    });
  }

  processQueue() {
    if (this.queue.length === 0) return;

    const availableWorker = this.workers.find(w => !w.busy);
    if (!availableWorker) return;

    const task = this.queue.shift();
    availableWorker.busy = true;

    const worker = new Worker(this.workerScript, {
      workerData: task.data
    });

    worker.on('message', (result) => {
      availableWorker.busy = false;
      task.resolve(result);
      this.processQueue();
    });

    worker.on('error', (err) => {
      availableWorker.busy = false;
      task.reject(err);
      this.processQueue();
    });

    availableWorker.worker = worker;
  }

  async close() {
    for (const { worker } of this.workers) {
      if (worker) {
        await worker.terminate();
      }
    }
  }
}

// Usage
const pool = new WorkerPool('./encoder-worker.js', 4);

const tasks = videos.map(video =>
  pool.execute({ video, config })
);

const results = await Promise.all(tasks);
await pool.close();

Best Practices

const os = require('os');
const cpuCount = os.cpus().length;

// Use CPU count - 1 to keep main thread responsive
const workerCount = Math.max(1, cpuCount - 1);
Always terminate workers when done:
worker.on('message', (result) => {
  // Process result
  worker.terminate();  // ← Clean up
});
worker.on('error', (err) => {
  console.error('Worker error:', err);
  worker.terminate();
});

worker.on('exit', (code) => {
  if (code !== 0) {
    console.error(`Worker exited with code ${code}`);
  }
});

Next Steps