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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kimjansheden/payload-video-processor

v0.1.16

Published

Queued FFmpeg video variant processing for Payload CMS

Readme

@kimjansheden/payload-video-processor

Queued FFmpeg-based video variant generation for Payload CMS. The plugin mirrors Payload's image size workflow for videos by adding a variants[] array to video collections and exposing an Admin UI that lets editors enqueue transcoding jobs. Designed to be plug-and-play: no custom endpoints, no extra UI code in the consumer project.

Features

  • ⚙️ Queue backed by BullMQ + Redis so processing runs outside the web process.
  • 🎞️ Configurable presets that append FFmpeg arguments per output variant.
  • ✂️ Optional crop UI powered by react-easy-crop for frame-accurate crops.
  • 🧰 Admin field handles enqueueing, previewing, replacing, and deleting variants without extra glue code.
  • 📁 Outputs written next to the original upload (or via custom path resolver).
  • 🔄 Worker CLI can bootstrap Payload locally or fall back to REST APIs.

Installation

pnpm add @kimjansheden/payload-video-processor

Peer dependencies (payload, react, react-dom) must already exist in your Payload project. The package bundles static FFmpeg/ffprobe binaries via ffmpeg-static; if those are blocked on your platform, set FFMPEG_BIN to a system ffmpeg binary (for example /opt/homebrew/bin/ffmpeg on macOS/Homebrew).

Quick start

Step 1: Register the plugin

Define presets and register the plugin in your payload.config.ts (or wherever you build your Payload config):

import { buildConfig } from "payload";
import { mongooseAdapter } from "@payloadcms/db-mongodb";
import videoPlugin from "@kimjansheden/payload-video-processor";

const videoOptions = {
  presets: {
    mobile360: {
      label: "360p Mobile",
      args: ["-vf", "scale=-2:360", "-crf", "32"],
    },
    hd720: {
      label: "720p HD",
      args: ["-vf", "scale=-2:720", "-crf", "24"],
      enableCrop: true,
    },
  },
  queue: {
    redisUrl: process.env.REDIS_URL,
    concurrency: 1,
  },
  // Auto-enqueue a preset when a new video is uploaded.
  autoEnqueue: true,
  // Optional: override the default preset used on create.
  autoEnqueuePreset: "hd720",
  // Optional: replace the original with the auto-generated variant.
  autoReplaceOriginal: true,
};

export default buildConfig({
  // This plugin works with both DATABASE_URI and MONGODB_URI; the worker CLI maps DATABASE_URI -> MONGODB_URI.
  db: mongooseAdapter({
    url: process.env.DATABASE_URI ?? process.env.MONGODB_URI ?? "",
  }),
  collections: [
    /* … */
  ],
  plugins: [videoPlugin(videoOptions)],
});

Recommended host pattern: export the options object from src/videoPluginOptions.ts and import it in both your Payload config and worker/payload.worker.config.ts, so presets/queue settings stay in one place.

When autoEnqueue is true, the plugin tries a preset named 1080, then hd1080, and finally falls back to the first configured preset. Set autoEnqueuePreset to force a specific preset name when auto-enqueueing.

Cropping behavior

Cropping is optional and configured per preset via enableCrop: true.

  • enableCrop only exposes crop controls in the Admin UI.
  • Cropping is opt-in per enqueue: the generated variant is not cropped unless the editor explicitly enables “Apply crop for this enqueue”.
  • If cropping is not enabled, no crop parameters are sent to the worker and the full frame is preserved.

Type-safe presets (TypeScript)

import videoPlugin, { type VideoPluginOptions } from "@kimjansheden/payload-video-processor";

const presets = {
  mobile360: { label: "360p Mobile", args: ["-vf", "scale=-2:360"] },
  hd1080: { label: "Full HD 1080p", args: ["-vf", "scale=-2:1080"] },
} satisfies VideoPluginOptions["presets"];

type PresetName = keyof typeof presets;

const options: VideoPluginOptions<PresetName> = {
  presets,
  queue: { redisUrl: process.env.REDIS_URL, concurrency: 1 },
  autoEnqueue: true,
  autoEnqueuePreset: "hd1080",
  autoReplaceOriginal: true,
};

Import patterns

ESM:

import videoPlugin from "@kimjansheden/payload-video-processor";

CommonJS:

const videoPlugin = require("@kimjansheden/payload-video-processor").default;

For the worker options module, either export default (ESM) or use module.exports = options (CommonJS), then point the CLI at the built file.

Options reference

| Option | Type | Notes | | --- | --- | --- | | presets | Record<string, Preset> | Required. Keys become preset names. | | queue | QueueConfig | Optional queue name/redis URL/concurrency. | | autoEnqueue | boolean | true uses autoEnqueuePreset or the default fallback. | | autoEnqueuePreset | string | Must match a preset key, not the label. | | autoReplaceOriginal | boolean | Only applies to auto-enqueued jobs. | | access | AccessControl | Optional access control hooks. | | resolvePaths | (args) => ResolvePathsResult | Override output directory/filename/URL. |

Step 2: Ensure the upload collection exposes path (local filesystem)

The worker needs to read the original upload from disk. For local filesystem storage, the worker reads the original file path from doc.path (absolute path on disk). If your upload collection does not already provide a path, add a read-only field and populate it from your upload staticDir + filename:

import path from "node:path";
import { fileURLToPath } from "node:url";
import type { CollectionConfig } from "payload";

const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);

const staticDir =
  process.env.STATIC_DIR ?? path.resolve(dirname, "../../public/media");

export const Media: CollectionConfig = {
  slug: "media",
  upload: {
    staticDir,
    mimeTypes: ["video/mp4", "video/webm", "video/quicktime"],
  },
  fields: [
    {
      name: "path",
      type: "text",
      admin: { readOnly: true, position: "sidebar" },
    },
  ],
  hooks: {
    afterRead: [
      ({ doc }) => {
        if (doc && typeof doc.filename === "string") {
          doc.path = path.join(staticDir, doc.filename);
        }
        return doc;
      },
    ],
  },
};

Step 3: Bundle the plugin options for the worker CLI

Provide a worker options module and bundle it to JS (the CLI needs a JS file). Example:

// src/videoPluginOptions.ts
export default videoOptions;
tsup src/videoPluginOptions.ts --format esm --platform node --target es2022 --out-dir dist-config --minify

Step 4: Add a minimal Payload config for the worker (recommended)

When you pass --payload-config, the worker can initialize Payload locally and update documents via the local Node API.

// worker/payload.worker.config.ts
import { mongooseAdapter } from "@payloadcms/db-mongodb";
import { buildConfig } from "payload";
import videoPlugin from "@kimjansheden/payload-video-processor";

import { Media } from "../src/collections/Media";
import videoPluginOptions from "../src/videoPluginOptions";

export default buildConfig({
  telemetry: false,
  secret: process.env.PAYLOAD_SECRET || "dev-secret",
  db: mongooseAdapter({
    url: process.env.MONGODB_URI || process.env.DATABASE_URI || "",
  }),
  plugins: [videoPlugin(videoPluginOptions)],
  collections: [Media],
});

Bundle it:

tsup worker/payload.worker.config.ts --format esm --platform node --target es2022 --out-dir dist-config --minify

Step 5: Start the worker

Start the worker in a separate process:

payload-video-worker \
  --config ./dist-config/videoPluginOptions.js \
  --payload-config ./dist-config/payload.worker.config.js

To initialize Payload locally, ensure PAYLOAD_SECRET + DATABASE_URI (or MONGODB_URI) are set. If you prefer the REST fallback, omit --payload-config and provide PAYLOAD_REST_URL + PAYLOAD_ADMIN_TOKEN.

The CLI loads .env, .env.local, .env.development, and .env.production automatically (unless you pass --no-default-env). Additional --env flags can point to project-specific files.

Example (explicit env + static dir, useful in monorepos):

FFMPEG_BIN=/opt/homebrew/bin/ffmpeg payload-video-worker \
  --no-default-env \
  --config ./dist-config/videoPluginOptions.js \
  --payload-config ./dist-config/payload.worker.config.js \
  --env .env \
  --env .env.development \
  --static-dir ./public/media

Prefer a fully programmatic setup? Import createWorker directly and pass the same options object you provide to the plugin.

Step 6: Use the Admin UI

In the Admin UI a "Video processing" panel appears on any upload collection that accepts video/* mime types. Editors can enqueue presets, preview variants, replace the original file with a processed version, or delete unwanted variants without writing custom endpoints.

Recommended host project scripts (example)

Most projects bundle both the plugin options and a minimal Payload config for the worker:

{
  "scripts": {
    "bundle:video-plugin-options": "tsup src/videoPluginOptions.ts --format esm --platform node --target es2022 --out-dir dist-config --minify",
    "bundle:payload-worker-config": "tsup worker/payload.worker.config.ts --format esm --platform node --target es2022 --out-dir dist-config --minify",
    "video:worker": "pnpm bundle:payload-worker-config && pnpm bundle:video-plugin-options && payload-video-worker --config ./dist-config/videoPluginOptions.js --payload-config ./dist-config/payload.worker.config.js",
    "video:worker:dev": "pnpm bundle:payload-worker-config && pnpm bundle:video-plugin-options && FFMPEG_BIN=/opt/homebrew/bin/ffmpeg payload-video-worker --no-default-env --config ./dist-config/videoPluginOptions.js --payload-config ./dist-config/payload.worker.config.js --env .env --env .env.development --static-dir ./public/media"
  }
}

Example project (repo)

This repository also includes apps/example-payload, a CLI-only reference project that demonstrates plugin configuration + worker processing without shipping a full /admin UI app. See apps/example-payload/README.md.

Scripts

  • pnpm build – bundles ESM/CJS + admin assets via tsup.
  • pnpm dev – watch mode build useful during plugin development.
  • pnpm typechecktsc --noEmit for type validation.
  • pnpm test – unit tests for helper utilities using Vitest.
  • pnpm worker – development helper that runs the compiled worker entry. (Application projects should prefer the payload-video-worker CLI.)

Environment variables

| Variable | Purpose | | ------------------------------------------- | ---------------------------------------------------------------------------- | | REDIS_URL | Default Redis connection string for queue + worker. | | FFMPEG_BIN | Optional path to a system ffmpeg binary (overrides ffmpeg-static). | | STATIC_DIR | Base media directory for the worker (used when resolving paths). | | PAYLOAD_SECRET / DATABASE_URI / MONGODB_URI | Required to bootstrap the Payload local API from the worker. | | PAYLOAD_REST_URL + PAYLOAD_ADMIN_TOKEN | REST fallback when local init is not possible. | | PAYLOAD_PUBLIC_URL / PAYLOAD_SERVER_URL | Alternative base URL for REST fallback if PAYLOAD_REST_URL is not set. | | PAYLOAD_CONFIG_PATH | Absolute/relative path to the host payload.config.ts for worker bootstrap. |

Customising output paths

Provide a resolvePaths function to control where variants are written:

videoPlugin({
  presets,
  resolvePaths: ({ original, presetName }) => ({
    dir: path.join("/data/videos", presetName),
    filename: `${path.parse(original.filename).name}.${presetName}.mp4`,
    url: `/videos/${presetName}/${path.parse(original.filename).name}.mp4`,
  }),
});

Worker CLI

The installed binary payload-video-worker bootstraps environment variables, loads your exported plugin options, and starts the queue worker. It automatically loads .env, .env.local, .env.development, and .env.production (unless you pass --no-default-env). Supply --env to load additional files, --config to point at the bundled options module, --payload-config when you want the worker to initialise Payload locally, and --static-dir if your media folder is not ./public/media.

payload-video-worker \
  --config ./dist-config/videoPluginOptions.js \
  --payload-config ./dist-config/payload.worker.config.js \
  --env .env \
  --env cms/.env

The CLI sets common fallbacks (STATIC_DIR, PAYLOAD_CONFIG_PATH, MONGODB_URIDATABASE_URI, default PAYLOAD_SECRET) before invoking createWorker. It shuts down gracefully on SIGINT/SIGTERM.

Contributing

Contributions are welcome. Please open an issue or PR with a clear description of the change and how to test it. If you add features, include a short README note so onboarding stays accurate.