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.
Queued FFmpeg video variant processing for Payload CMS
Installation
npm i @kimjansheden/payload-video-processor1
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
Trends appear after 7 days of data. Daily snapshots have not started yet.
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
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1124768439-video-plugin)<a href="https://payloaddirectory.dev/plugins/1124768439-video-plugin"><img src="https://payloaddirectory.dev/badge/1124768439-video-plugin.svg" alt="Payload Directory"></a>README
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.
react-easy-crop for frame-accurate crops.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).
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 is optional and configured per preset via enableCrop: true.
enableCrop only exposes crop controls in the Admin UI.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,
};
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.
| 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. |
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;
},
],
},
};
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
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.
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",
},
}
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.
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.)| 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. |
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`,
}),
});
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.
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.
payload media
Search Unsplash, Pexels and Pixabay stock images and videos from Payload upload collections
680/wk8v4
payloadcms plugin image optimizer
Client-side pre-resize, ThumbHash blur placeholders, filename strategies and bulk regeneration for Payload image uploads
480/wk27v3
sirv payload
Use Sirv as a media source in the Payload admin and render assets on the frontend with @sirv/react
250v?
mux video
Upload and manage Mux videos from the Payload admin with public or signed playback
872.7k/wk83v3
image search
Search and import images from Unsplash, Pexels and Pixabay without leaving the Payload admin
78533/wk114v3
payload plugin aspect preview
Crop and focal point editing with live multi-aspect-ratio previews for Payload upload collections
7534/wk6v3