kimjansheden

video plugin

by kimjansheden
PayloadVideoPlugin
Media & images
v3
^3.0.0
MIT

Overview

Summary generated from the README

Adds a variants array and a Video processing panel to upload collections that accept video files. Editors enqueue FFmpeg presets, preview or delete variants, and optionally crop frames. Jobs run through a BullMQ and Redis queue processed by a separate worker CLI. Requires Redis and Payload 3; FFmpeg binaries are bundled or can be set via an environment variable.

Features

  • Configurable presets that append FFmpeg arguments per output variant
  • BullMQ and Redis queue so transcoding runs outside the web process
  • Admin panel to enqueue, preview, replace the original with, and delete variants
  • Optional per-preset crop UI, applied only when the editor enables it
  • Auto-enqueue a preset on upload, with optional replacement of the original
  • Worker CLI that starts Payload locally or falls back to REST, plus a custom output path resolver

Use cases

  • Generate compressed mobile and HD versions of uploaded videos without blocking the web server
  • Let editors crop uploaded videos to a target frame from the admin panel
  • Replace large original uploads automatically with a compressed variant on upload

From the author

Queued FFmpeg video variant processing for Payload CMS

Installation

npm i @kimjansheden/payload-video-processor

1

Stars

0

Forks

0

Open Issues

9 months ago

Last Updated

0

Weekly Downloads

v0.1.16

npm Version

1.2 MB

Unpacked Size

8

Dependencies

GitHub stars

Trends appear after 7 days of data. Daily snapshots have not started yet.

npm weekly downloads

Trends appear after 7 days of data. Daily snapshots have not started yet.

Each point is npm's rolling 7-day download count.

Health Score

31/100Fair

Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.

Badge for your README

Payload Directory health badge
Markdown
[![Payload Directory](https://payloaddirectory.dev/badge/1124768439-video-plugin.svg)](https://payloaddirectory.dev/plugins/1124768439-video-plugin)
HTML
<a href="https://payloaddirectory.dev/plugins/1124768439-video-plugin"><img src="https://payloaddirectory.dev/badge/1124768439-video-plugin.svg" alt="Payload Directory"></a>
Updated Dec 31, 2025Created Dec 29, 2025Published Dec 30, 2025

README

@kimjansheden/payload-video-processor

npm

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.

Demo videos

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

OptionTypeNotes
presetsRecord<string, Preset>Required. Keys become preset names.
queueQueueConfigOptional queue name/redis URL/concurrency.
autoEnqueuebooleantrue uses autoEnqueuePreset or the default fallback.
autoEnqueuePresetstringMust match a preset key, not the label.
autoReplaceOriginalbooleanOnly applies to auto-enqueued jobs.
accessAccessControlOptional access control hooks.
resolvePaths(args) => ResolvePathsResultOverride 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

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.

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 typecheck – tsc --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

VariablePurpose
REDIS_URLDefault Redis connection string for queue + worker.
FFMPEG_BINOptional path to a system ffmpeg binary (overrides ffmpeg-static).
STATIC_DIRBase media directory for the worker (used when resolving paths).
PAYLOAD_SECRET / DATABASE_URI / MONGODB_URIRequired to bootstrap the Payload local API from the worker.
PAYLOAD_REST_URL + PAYLOAD_ADMIN_TOKENREST fallback when local init is not possible.
PAYLOAD_PUBLIC_URL / PAYLOAD_SERVER_URLAlternative base URL for REST fallback if PAYLOAD_REST_URL is not set.
PAYLOAD_CONFIG_PATHAbsolute/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_URI ← DATABASE_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.

Similar plugins