npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

simple-ffmpegjs

v0.1.1

Published

Simple Node.js helper around ffmpeg for video composition, transitions, audio mixing, and text rendering.

Readme

simple-ffmpeg 🎬

Simple lightweight Node.js helper around FFmpeg for quick video composition, transitions, audio mixing, and animated text overlays.

🙌 Credits

Huge shoutout to the original inspiration for this library:

  • John Chen (coldshower): https://github.com/coldshower
  • ezffmpeg: https://github.com/ezffmpeg/ezffmpeg

This project builds on those ideas and extends them with a few opinionated defaults and features to make common video tasks easier.

✨ Why this project

Built for data pipelines: a tiny helper around FFmpeg that makes common edits trivial—clip concatenation with transitions, flexible text overlays, images with Ken Burns effects, and reliable audio mixing—without hiding FFmpeg. It favors safe defaults, scales to long scripts, and stays dependency‑free.

  • ✅ Simple API for building FFmpeg filter graphs
  • 🎞️ Concatenate clips with optional transitions (xfade)
  • 🔊 Mix multiple audio sources and add background music (not affected by transition fades)
  • 📝 Text overlays (static, word-by-word, cumulative) with opt-in animations (fade-in, pop, pop-bounce)
  • 🧰 Safe defaults + guardrails (basic validation, media bounds clamping)
  • 🧱 Scales to long scripts via optional multi-pass text batching
  • 🧩 Ships TypeScript definitions without requiring TS
  • 🪶 No external libraries (other than FFmpeg), no bundled fonts; extremely lightweight
  • 🖼️ Image support with Ken Burns (zoom-in/out, pan-left/right/up/down)
  • 🧑‍💻 Actively maintained; PRs and issues welcome

📦 Install

npm install simple-ffmpegjs

Import syntax

// CommonJS
const SIMPLEFFMPEG = require("simple-ffmpegjs");
// ESM
import SIMPLEFFMPEG from "simple-ffmpegjs";

⚙️ Requirements

Make sure you have ffmpeg installed on your system:

Mac: brew install ffmpeg

Ubuntu/Debian: apt-get install ffmpeg

Windows: Download from ffmpeg.org

Ensure ffmpeg and ffprobe are installed and available on your PATH.

For text overlays with drawtext, use an FFmpeg build that includes libfreetype and fontconfig. Make sure a system font is present so font=Sans resolves, or provide fontFile. Minimal containers often lack fonts, so install one explicitly.

Examples:

Debian/Ubuntu:

apt-get update && apt-get install -y ffmpeg fontconfig fonts-dejavu-core

Alpine:

apk add --no-cache ffmpeg fontconfig ttf-dejavu

🚀 Quick start

const SIMPLEFFMPEG = require("simple-ffmpegjs");

(async () => {
  const project = new SIMPLEFFMPEG({ width: 1080, height: 1920, fps: 30 });

  await project.load([
    { type: "video", url: "./vids/a.mp4", position: 0, end: 5 },
    {
      type: "video",
      url: "./vids/b.mp4",
      position: 5,
      end: 10,
      transition: { type: "fade-in", duration: 0.5 },
    },
    { type: "music", url: "./audio/bgm.wav", volume: 0.2 },
    {
      type: "text",
      text: "Hello world",
      position: 1,
      end: 3,
      fontColor: "white",
    },
  ]);

  await project.export({ outputPath: "./output.mp4" });
})();

📚 Examples

  • 🎞️ Two clips + fade transition + background music
await project.load([
  { type: "video", url: "./a.mp4", position: 0, end: 5 },
  {
    type: "video",
    url: "./b.mp4",
    position: 5,
    end: 10,
    transition: { type: "fade-in", duration: 0.4 },
  },
  { type: "music", url: "./bgm.wav", volume: 0.18 },
]);
  • 📝 Static text (centered by default)
await project.load([
  { type: "video", url: "./clip.mp4", position: 0, end: 5 },
  {
    type: "text",
    text: "Static Title",
    position: 0.5,
    end: 2.5,
    fontColor: "white",
  },
]);
  • 🔤 Word-by-word replacement with fade-in
await project.load([
  {
    type: "text",
    mode: "word-replace",
    position: 2.0,
    end: 4.0,
    fontColor: "#00ffff",
    centerX: 0,
    centerY: -350,
    animation: { type: "fade-in-out", in: 0.4, out: 0.4 },
    words: [
      { text: "One", start: 2.0, end: 2.5 },
      { text: "Two", start: 2.5, end: 3.0 },
      { text: "Three", start: 3.0, end: 3.5 },
      { text: "Four", start: 3.5, end: 4.0 },
    ],
  },
]);
  • 🔠 Word-by-word (auto) with pop-bounce
await project.load([
  {
    type: "text",
    mode: "word-replace",
    text: "Alpha Beta Gamma Delta",
    position: 4.0,
    end: 6.0,
    fontSize: 64,
    fontColor: "yellow",
    centerX: 0,
    centerY: -100,
    animation: { type: "fade-in-out", in: 0.4, out: 0.4 },
    wordTimestamps: [4.0, 4.5, 5.0, 5.5, 6.0],
  },
]);
  • 🎧 Standalone audio overlay
await project.load([
  { type: "audio", url: "./vo.mp3", position: 0, end: 10, volume: 1 },
]);
  • 🖼️ Images with Ken Burns (zoom + pan)
await project.load([
  // Zoom-in image (2s)
  {
    type: "image",
    url: "./img.png",
    position: 10,
    end: 12,
    kenBurns: "zoom-in",
  },
  // Pan-right image (2s)
  {
    type: "image",
    url: "./img.png",
    position: 12,
    end: 14,
    kenBurns: "pan-right",
  },
]);

🧠 Behavior (in short)

  • Timeline uses clip [position, end); transitions are overlaps that reduce total duration by their length
  • Background music is mixed after other audio, so transition acrossfades don’t attenuate it
  • Clip audio is timeline-aligned (absolute position) and mixed once; avoids early starts and gaps around transitions
  • Text animations are opt-in (none by default)
  • For big scripts, text rendering can be batched into multiple passes automatically
  • Visual gaps are not allowed: if there’s any gap with no video/image between clips (or at the very start), validation throws

🔌 API (at a glance)

  • new SIMPLEFFMPEG({ width?, height?, fps?, validationMode? })
  • await project.load([...clips]) — video/audio/text/music descriptors
  • await project.export({ outputPath?, textMaxNodesPerPass? })

That’s it—keep it simple. See the examples above for common cases.

🔬 API Details

Constructor

new SIMPLEFFMPEG(options?: {
  fps?: number;           // default 30
  width?: number;         // default 1920
  height?: number;        // default 1080
  validationMode?: 'warn' | 'strict'; // default 'warn'
});

project.load(clips)

Loads and pre-validates clips. Accepts an array of clip descriptors (video, audio, background music, text). Returns a Promise that resolves when inputs are prepared (e.g., metadata read, rotation handled later at export).

await project.load(clips: Clip[]);

Clip union

type Clip = VideoClip | AudioClip | BackgroundMusicClip | ImageClip | TextClip;

Video clip

interface VideoClip {
  type: "video";
  url: string; // input video file path/URL
  position: number; // timeline start (seconds)
  end: number; // timeline end (seconds)
  cutFrom?: number; // source offset (seconds), default 0
  volume?: number; // if the source has audio, default 1
  transition?: {
    // optional transition at the boundary before this clip
    type: string; // e.g., 'fade', 'wipeleft', etc. (xfade transitions)
    duration: number; // in seconds
  };
}

Notes:

  • All xfade transitions are supported you can see a list of them here
  • Each transition reduces total output duration by its duration (overlap semantics).
  • Rotation metadata is handled automatically before export.

Audio clip (standalone)

interface AudioClip {
  type: "audio";
  url: string;
  position: number; // timeline start
  end: number; // timeline end
  cutFrom?: number; // default 0
  volume?: number; // default 1
}

Background music clip

interface BackgroundMusicClip {
  type: "music" | "backgroundAudio";
  url: string;
  position?: number; // default 0
  end?: number; // default project duration (video timeline)
  cutFrom?: number; // default 0
  volume?: number; // default 0.2
}

Notes:

  • Mixed after other audio and after acrossfades, so transition fades do not attenuate the background music.
  • If no videos exist, end defaults to the max provided among BGM clips.

Text clip

interface TextClip {
  type: "text";
  // Time window
  position: number; // start on timeline
  end: number; // end on timeline

  // Content & modes
  text?: string; // used for 'static' and as source when auto-splitting words
  mode?: "static" | "word-replace" | "word-sequential";

  // Word timing (choose one form)
  words?: Array<{ text: string; start: number; end: number }>; // explicit per-word timing (absolute seconds)
  wordTimestamps?: number[]; // timestamps to split `text` by whitespace; N or N+1 entries

  // Font & styling
  fontFile?: string; // overrides fontFamily
  fontFamily?: string; // default 'Sans' (fontconfig)
  fontSize?: number; // default 48
  fontColor?: string; // default '#FFFFFF'

  // Positioning (center by default)
  centerX?: number; // pixel offset from center (x)
  centerY?: number; // pixel offset from center (y)
  x?: number; // absolute x (left)
  y?: number; // absolute y (top)

  // Animation (opt-in)
  animation?: {
    type: "none" | "fade-in" | "fade-in-out" | "pop" | "pop-bounce"; // default 'none'
    in?: number; // seconds for intro phase (e.g., fade-in duration)
  };
}

Notes:

  • If both words and wordTimestamps are provided, words takes precedence.
  • For wordTimestamps with a single array: provide either per-word start times (end inferred by next start), or N+1 edge times; whitespace in text defines words.
  • Defaults to centered placement if no explicit x/y or centerX/centerY provided.

Image clip

interface ImageClip {
  type: "image";
  url: string;
  position: number; // timeline start
  end: number; // timeline end
  kenBurns?:
    | "zoom-in"
    | "zoom-out"
    | "pan-left"
    | "pan-right"
    | "pan-up"
    | "pan-down";
}

Notes:

  • Images are treated as video streams. Ken Burns uses zoompan internally with the correct frame count.
  • For pan-only moves, a small base zoom is applied so there’s room to pan across.

project.export(options)

Builds and runs the FFmpeg command. Returns the final outputPath.

await project.export(options?: {
  outputPath?: string;          // default './output.mp4'
  textMaxNodesPerPass?: number; // default 75 (batch size for multi-pass text)
  intermediateVideoCodec?: string; // default 'libx264' (for text passes)
  intermediateCrf?: number;     // default 18 (for text passes)
  intermediatePreset?: string;  // default 'veryfast' (for text passes)
}): Promise<string>;

Behavior:

  • If text overlay count exceeds textMaxNodesPerPass, text is rendered in multiple passes using temporary files; audio is copied between passes; final output is fast-start.
  • Mapping: final video/audio streams are mapped based on what exists; if only audio or only video is present, mapping adapts accordingly.

Timeline semantics

  • Each clip contributes [position, end) to the timeline.
  • For transitions, the overlap reduces the final output duration by the transition duration.
  • Background music defaults to the visual timeline end (max end across video/image clips) and is mixed after other audio and acrossfades.

Animations

  • none (default): plain text, no animation
  • fade-in: alpha 0 → 1 over in seconds (e.g., 0.25–0.4)
  • fade-in-out: alpha 0 → 1 over in seconds, then 1 → 0 over out seconds approaching the end
  • pop: font size scales from ~70% → 100% over in seconds
  • pop-bounce: scales ~70% → 110% during in, then settles to 100%

Tip: small in values (0.2–0.4s) feel snappy for word-by-word displays.

🤝 Contributing

  • PRs and issues welcome
  • Actively being worked on; I’ll review new contributions and iterate

🗺️ Roadmap

  • Visual gap handling (opt-in fillers): optional fillVisualGaps: 'none' | 'black' if requested
  • Additional text effects (typewriter, word-by-word fade-out variants, outlines/shadows presets)
  • Image effects presets (Ken Burns paths presets, ease functions)
  • Ken Burns upgrades: strength parameter, custom positioning, additional ease curves
  • Optional audio transition coupling: tie clip audio fades to xfade boundaries
  • Export options for different containers/codecs (HEVC, VP9/AV1, audio-only)
  • Better error reporting with command dump helpers
  • CLI wrapper for quick local use
  • Performance: smarter batching and parallel intermediate renders

📜 License

MIT