@kimjansheden/payload-video-processor 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,472 @@
1
+ import path from 'path';
2
+ import { pathToFileURL } from 'url';
3
+ import { mkdir, stat } from 'fs/promises';
4
+ import ffmpeg2 from 'fluent-ffmpeg';
5
+ import ffmpegStatic from 'ffmpeg-static';
6
+ import ffprobeStatic from 'ffprobe-static';
7
+ import { Worker } from 'bullmq';
8
+ import IORedis from 'ioredis';
9
+ import { z } from 'zod';
10
+ import payload from 'payload';
11
+
12
+ // src/queue/worker.ts
13
+ var presetSchema = z.object({
14
+ args: z.array(z.string()),
15
+ label: z.string().optional(),
16
+ enableCrop: z.boolean().optional()
17
+ });
18
+ var videoPluginOptionsSchema = z.object({
19
+ presets: z.record(presetSchema).refine((value) => Object.keys(value).length > 0, {
20
+ message: "At least one preset must be defined."
21
+ }),
22
+ queue: z.object({
23
+ name: z.string().min(1).optional(),
24
+ redisUrl: z.string().optional(),
25
+ concurrency: z.number().int().positive().optional()
26
+ }).optional(),
27
+ access: z.any().optional(),
28
+ resolvePaths: z.any().optional()
29
+ });
30
+ var ensureOptions = (options) => videoPluginOptionsSchema.parse(options);
31
+ var cropSchema = z.object({
32
+ x: z.number().min(0).max(1),
33
+ y: z.number().min(0).max(1),
34
+ width: z.number().positive().max(1),
35
+ height: z.number().positive().max(1)
36
+ }).refine((value) => value.width > 0 && value.height > 0, {
37
+ message: "Crop width and height must be > 0"
38
+ });
39
+ var videoJobSchema = z.object({
40
+ collection: z.string().min(1),
41
+ id: z.union([z.string(), z.number()]).transform((value) => value.toString()),
42
+ preset: z.string().min(1),
43
+ crop: cropSchema.optional()
44
+ });
45
+
46
+ // src/ffmpeg/args.ts
47
+ var FASTSTART_FLAGS = ["-movflags", "+faststart"];
48
+ var CRF_FLAG = "-crf";
49
+ var hasCrf = (args) => {
50
+ for (let i = 0; i < args.length; i += 1) {
51
+ if (args[i] === CRF_FLAG) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ };
57
+ var hasFaststart = (args) => {
58
+ for (let i = 0; i < args.length; i += 1) {
59
+ if (args[i] === "-movflags") {
60
+ const value = args[i + 1];
61
+ if (typeof value === "string" && value.includes("faststart")) {
62
+ return true;
63
+ }
64
+ }
65
+ }
66
+ return false;
67
+ };
68
+ var extractFilters = (args) => {
69
+ const rest = [];
70
+ const filters = [];
71
+ for (let i = 0; i < args.length; i += 1) {
72
+ const current = args[i];
73
+ if (current === "-vf" || current === "-filter:v") {
74
+ const value = args[i + 1];
75
+ if (typeof value === "string") {
76
+ filters.push(value);
77
+ }
78
+ i += 1;
79
+ } else {
80
+ rest.push(current);
81
+ }
82
+ }
83
+ return { rest, filters };
84
+ };
85
+ var clamp = (value, min, max) => {
86
+ if (Number.isNaN(value)) return min;
87
+ if (value < min) return min;
88
+ if (value > max) return max;
89
+ return value;
90
+ };
91
+ var buildCropFilter = (crop, dimensions) => {
92
+ if (!dimensions?.width || !dimensions?.height) return void 0;
93
+ const cropWidth = Math.max(1, Math.round(dimensions.width * crop.width));
94
+ const cropHeight = Math.max(1, Math.round(dimensions.height * crop.height));
95
+ const maxX = Math.max(0, dimensions.width - cropWidth);
96
+ const maxY = Math.max(0, dimensions.height - cropHeight);
97
+ const x = clamp(Math.round(dimensions.width * crop.x), 0, maxX);
98
+ const y = clamp(Math.round(dimensions.height * crop.y), 0, maxY);
99
+ return `crop=${cropWidth}:${cropHeight}:${x}:${y}`;
100
+ };
101
+ var buildFfmpegArgs = ({
102
+ presetArgs,
103
+ crop,
104
+ dimensions,
105
+ defaultCrf = 24
106
+ }) => {
107
+ const args = [...presetArgs];
108
+ const { rest, filters } = extractFilters(args);
109
+ if (!hasCrf(rest)) {
110
+ rest.push(CRF_FLAG, String(defaultCrf));
111
+ }
112
+ if (!hasFaststart(rest)) {
113
+ rest.push(...FASTSTART_FLAGS);
114
+ }
115
+ if (crop) {
116
+ const cropFilter = buildCropFilter(crop, dimensions);
117
+ if (cropFilter) {
118
+ filters.push(cropFilter);
119
+ }
120
+ }
121
+ if (filters.length > 0) {
122
+ rest.push("-vf", filters.join(","));
123
+ }
124
+ return {
125
+ globalOptions: ["-y"],
126
+ outputOptions: rest
127
+ };
128
+ };
129
+ if (ffprobeStatic.path) {
130
+ ffmpeg2.setFfprobePath(ffprobeStatic.path);
131
+ }
132
+ var probeVideo = async (filePath) => new Promise((resolve, reject) => {
133
+ ffmpeg2.ffprobe(filePath, (error, metadata) => {
134
+ if (error) {
135
+ reject(error);
136
+ return;
137
+ }
138
+ const videoStream = metadata.streams.find(
139
+ (stream) => stream.codec_type === "video"
140
+ );
141
+ const width = videoStream?.width;
142
+ const height = videoStream?.height;
143
+ const durationRaw = videoStream?.duration ?? metadata.format?.duration;
144
+ const duration = typeof durationRaw !== "undefined" ? Number(durationRaw) : void 0;
145
+ const bitrateRaw = videoStream?.bit_rate ?? metadata.format?.bit_rate;
146
+ const bitrate = typeof bitrateRaw !== "undefined" ? Number(bitrateRaw) : void 0;
147
+ resolve({
148
+ width: width ?? void 0,
149
+ height: height ?? void 0,
150
+ duration: Number.isNaN(duration) ? void 0 : duration,
151
+ bitrate: Number.isNaN(bitrate) ? void 0 : bitrate
152
+ });
153
+ });
154
+ });
155
+ var normalizeUrl = (input, filename) => {
156
+ if (!input) return filename ?? "";
157
+ const parts = input.split("?");
158
+ const base = parts[0];
159
+ const query = parts[1] ? `?${parts.slice(1).join("?")}` : "";
160
+ const lastSlash = base.lastIndexOf("/");
161
+ if (lastSlash === -1) {
162
+ return filename ?? base;
163
+ }
164
+ const prefix = base.slice(0, lastSlash);
165
+ const sanitized = filename ?? base.slice(lastSlash + 1);
166
+ return `${prefix}/${sanitized}${query}`;
167
+ };
168
+ var defaultResolvePaths = ({
169
+ original,
170
+ presetName
171
+ }) => {
172
+ const originalFilename = original.filename ?? path.basename(original.path);
173
+ const extension = path.extname(originalFilename) || path.extname(original.path) || ".mp4";
174
+ const baseName = path.basename(originalFilename, extension);
175
+ const variantFilename = `${baseName}_${presetName}${extension}`;
176
+ const originalDir = path.dirname(original.path);
177
+ const absoluteDir = path.isAbsolute(original.path) ? originalDir : path.join(process.cwd(), originalDir);
178
+ const url = normalizeUrl(original.url, variantFilename);
179
+ return {
180
+ dir: absoluteDir,
181
+ filename: variantFilename,
182
+ url
183
+ };
184
+ };
185
+ var buildStoredPath = (originalPath, variantFilename) => {
186
+ const originalDir = path.dirname(originalPath);
187
+ return path.join(originalDir, variantFilename);
188
+ };
189
+ var buildWritePath = (dir, filename) => path.join(dir, filename);
190
+ var cachedClient = null;
191
+ var localInitialized = false;
192
+ var normalizeConfigPath = (configPath) => {
193
+ if (configPath.startsWith("file://")) return configPath;
194
+ return pathToFileURL(path.resolve(configPath)).href;
195
+ };
196
+ var buildAuthHeaders = (token) => ({
197
+ Authorization: `Bearer ${token}`,
198
+ "X-Payload-API-Key": token
199
+ });
200
+ var initLocalPayload = async () => {
201
+ const secret = process.env.PAYLOAD_SECRET;
202
+ const mongoURL = process.env.MONGODB_URI;
203
+ if (!secret || !mongoURL) {
204
+ return null;
205
+ }
206
+ try {
207
+ const configPath = process.env.PAYLOAD_CONFIG_PATH;
208
+ let configModule;
209
+ if (configPath) {
210
+ const imported = await import(normalizeConfigPath(configPath));
211
+ configModule = imported?.default ?? imported;
212
+ }
213
+ if (!localInitialized) {
214
+ const initOptions = {
215
+ secret,
216
+ mongoURL,
217
+ local: true
218
+ };
219
+ if (configModule) {
220
+ initOptions.config = configModule;
221
+ }
222
+ await payload.init(initOptions);
223
+ localInitialized = true;
224
+ }
225
+ const instance = payload;
226
+ return {
227
+ findByID: ({ collection, id }) => instance.findByID({
228
+ collection,
229
+ id
230
+ }),
231
+ update: ({ collection, id, data }) => instance.update({
232
+ collection,
233
+ id,
234
+ data
235
+ }),
236
+ getCollectionConfig: (slug) => instance.collections?.[slug]?.config
237
+ };
238
+ } catch (error) {
239
+ console.warn(
240
+ "[video-processor] Failed to initialize Payload locally, falling back to REST client.",
241
+ error
242
+ );
243
+ return null;
244
+ }
245
+ };
246
+ var initRestClient = async () => {
247
+ const baseUrl = process.env.PAYLOAD_REST_URL || process.env.PAYLOAD_PUBLIC_URL || process.env.PAYLOAD_SERVER_URL;
248
+ const token = process.env.PAYLOAD_ADMIN_TOKEN;
249
+ if (!baseUrl || !token) {
250
+ throw new Error(
251
+ "Unable to establish Payload REST client. Provide PAYLOAD_REST_URL (or PAYLOAD_PUBLIC_URL) and PAYLOAD_ADMIN_TOKEN."
252
+ );
253
+ }
254
+ const base = baseUrl.replace(/\/$/, "");
255
+ const headers = buildAuthHeaders(token);
256
+ const request = async (url, init) => {
257
+ const response = await fetch(url, {
258
+ ...init,
259
+ headers: {
260
+ "Content-Type": "application/json",
261
+ Accept: "application/json",
262
+ ...headers,
263
+ ...init?.headers
264
+ }
265
+ });
266
+ if (!response.ok) {
267
+ const text = await response.text();
268
+ throw new Error(
269
+ `Payload REST request failed (${response.status}): ${text}`
270
+ );
271
+ }
272
+ return await response.json();
273
+ };
274
+ return {
275
+ findByID: async ({ collection, id }) => {
276
+ const result = await request(
277
+ `${base}/api/${collection}/${id}`
278
+ );
279
+ return result.doc ?? result;
280
+ },
281
+ update: async ({ collection, id, data }) => {
282
+ const result = await request(
283
+ `${base}/api/${collection}/${id}`,
284
+ {
285
+ method: "PATCH",
286
+ body: JSON.stringify(data)
287
+ }
288
+ );
289
+ return result.doc ?? result;
290
+ }
291
+ };
292
+ };
293
+ var getPayloadClient = async () => {
294
+ if (cachedClient) return cachedClient;
295
+ const localClient = await initLocalPayload();
296
+ if (localClient) {
297
+ cachedClient = localClient;
298
+ return localClient;
299
+ }
300
+ const restClient = await initRestClient();
301
+ cachedClient = restClient;
302
+ return restClient;
303
+ };
304
+
305
+ // src/queue/createWorker.ts
306
+ var envFfmpegPath = process.env.FFMPEG_BIN?.trim();
307
+ var ffmpegBinary = envFfmpegPath && envFfmpegPath.length > 0 ? envFfmpegPath : typeof ffmpegStatic === "string" ? ffmpegStatic : null;
308
+ if (ffmpegBinary) {
309
+ ffmpeg2.setFfmpegPath(ffmpegBinary);
310
+ }
311
+ if (ffprobeStatic.path) {
312
+ ffmpeg2.setFfprobePath(ffprobeStatic.path);
313
+ }
314
+ var createWorker = async (rawOptions) => {
315
+ const options = ensureOptions(rawOptions);
316
+ const presets = options.presets;
317
+ const queueName = options.queue?.name ?? "video-transcode";
318
+ const concurrency = options.queue?.concurrency ?? 1;
319
+ const redisUrl = options.queue?.redisUrl ?? process.env.REDIS_URL;
320
+ const connection = redisUrl ? new IORedis(redisUrl, { maxRetriesPerRequest: null }) : new IORedis({ maxRetriesPerRequest: null });
321
+ const worker = new Worker(
322
+ queueName,
323
+ async (job) => {
324
+ const parsed = videoJobSchema.parse(job.data);
325
+ const preset = presets[parsed.preset];
326
+ if (!preset) {
327
+ throw new Error(`Unknown preset ${parsed.preset}`);
328
+ }
329
+ job.updateProgress(5);
330
+ const client = await getPayloadClient();
331
+ const document = await client.findByID({
332
+ collection: parsed.collection,
333
+ id: parsed.id
334
+ });
335
+ if (!document) {
336
+ throw new Error(
337
+ `Document ${parsed.id} in collection ${parsed.collection} not found`
338
+ );
339
+ }
340
+ const originalPath = document?.path;
341
+ const filename = document?.filename;
342
+ const url = document?.url;
343
+ if (!originalPath) {
344
+ throw new Error("Source document does not expose a `path` property.");
345
+ }
346
+ const absoluteInputPath = path.isAbsolute(originalPath) ? originalPath : path.join(process.cwd(), originalPath);
347
+ const inputMetadata = await probeVideo(absoluteInputPath);
348
+ job.updateProgress(15);
349
+ const resolvePaths = options.resolvePaths ?? defaultResolvePaths;
350
+ const collectionConfig = client.getCollectionConfig?.(parsed.collection) ?? null;
351
+ const resolved = resolvePaths({
352
+ doc: document,
353
+ collection: collectionConfig,
354
+ collectionSlug: parsed.collection,
355
+ original: {
356
+ filename: filename ?? path.basename(originalPath),
357
+ path: originalPath,
358
+ url: url ?? ""
359
+ },
360
+ presetName: parsed.preset
361
+ });
362
+ const writeDir = resolved.dir;
363
+ const writeFilename = resolved.filename;
364
+ const targetUrl = resolved.url;
365
+ const writePath = buildWritePath(writeDir, writeFilename);
366
+ await mkdir(writeDir, { recursive: true });
367
+ const { globalOptions, outputOptions } = buildFfmpegArgs({
368
+ presetArgs: preset.args,
369
+ crop: parsed.crop,
370
+ dimensions: {
371
+ width: inputMetadata.width,
372
+ height: inputMetadata.height
373
+ }
374
+ });
375
+ await new Promise((resolve, reject) => {
376
+ const command = ffmpeg2(absoluteInputPath);
377
+ globalOptions.forEach((option) => command.addOption(option));
378
+ command.outputOptions(outputOptions);
379
+ command.output(writePath);
380
+ command.on("progress", (progress) => {
381
+ if (typeof progress.percent === "number") {
382
+ const bounded = Math.min(95, 15 + progress.percent * 0.7);
383
+ void job.updateProgress(bounded);
384
+ }
385
+ });
386
+ command.on("end", () => resolve());
387
+ command.on("error", (error) => reject(error));
388
+ command.run();
389
+ });
390
+ const fileStats = await stat(writePath);
391
+ const outputMetadata = await probeVideo(writePath);
392
+ const storedPath = buildStoredPath(originalPath, writeFilename);
393
+ const variant = {
394
+ preset: parsed.preset,
395
+ url: targetUrl,
396
+ path: storedPath,
397
+ size: fileStats.size,
398
+ duration: outputMetadata.duration ?? inputMetadata.duration,
399
+ width: outputMetadata.width ?? inputMetadata.width,
400
+ height: outputMetadata.height ?? inputMetadata.height,
401
+ bitrate: outputMetadata.bitrate,
402
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
403
+ };
404
+ const existingVariants = Array.isArray(document.variants) ? document.variants : [];
405
+ const nextVariants = [
406
+ ...existingVariants.filter((item) => item?.preset !== variant.preset),
407
+ variant
408
+ ];
409
+ await client.update({
410
+ collection: parsed.collection,
411
+ id: parsed.id,
412
+ data: {
413
+ variants: nextVariants
414
+ }
415
+ });
416
+ await job.updateProgress(100);
417
+ return variant;
418
+ },
419
+ {
420
+ connection,
421
+ concurrency
422
+ }
423
+ );
424
+ worker.on("failed", (job, error) => {
425
+ console.error(`[video-processor] Job ${job?.id} failed`, error);
426
+ });
427
+ worker.on("completed", (job) => {
428
+ console.log(`[video-processor] Job ${job.id} completed`);
429
+ });
430
+ await worker.waitUntilReady();
431
+ console.log(`[video-processor] Worker listening on queue ${queueName}`);
432
+ const shutdown = async () => {
433
+ await worker.close();
434
+ await connection.quit();
435
+ };
436
+ process.once("SIGINT", () => {
437
+ void shutdown().then(() => process.exit(0));
438
+ });
439
+ process.once("SIGTERM", () => {
440
+ void shutdown().then(() => process.exit(0));
441
+ });
442
+ return worker;
443
+ };
444
+
445
+ // src/queue/worker.ts
446
+ var loadOptions = async () => {
447
+ const modulePath = process.env.PAYLOAD_VIDEO_WORKER_CONFIG;
448
+ if (!modulePath) {
449
+ throw new Error(
450
+ "PAYLOAD_VIDEO_WORKER_CONFIG must point to a module exporting plugin options."
451
+ );
452
+ }
453
+ const resolved = modulePath.startsWith(".") || modulePath.startsWith("/") ? pathToFileURL(path.resolve(modulePath)).href : modulePath;
454
+ const imported = await import(resolved);
455
+ const options = imported.default ?? imported;
456
+ if (!options || typeof options !== "object" || !("presets" in options)) {
457
+ throw new Error(
458
+ "Invalid worker options module. Ensure it exports VideoPluginOptions."
459
+ );
460
+ }
461
+ return options;
462
+ };
463
+ var main = async () => {
464
+ const options = await loadOptions();
465
+ await createWorker(options);
466
+ };
467
+ void main().catch((error) => {
468
+ console.error("[video-processor] Worker failed to start", error);
469
+ process.exit(1);
470
+ });
471
+ //# sourceMappingURL=worker.js.map
472
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/options.ts","../../src/queue/job.types.ts","../../src/ffmpeg/args.ts","../../src/ffmpeg/probe.ts","../../src/utils/paths.ts","../../src/utils/payload.ts","../../src/queue/createWorker.ts","../../src/queue/worker.ts"],"names":["z","ffmpeg","path","ffprobeStatic","pathToFileURL"],"mappings":"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","file":"worker.js","sourcesContent":["import { z } from \"zod\";\nimport type { Preset, VideoPluginOptions } from \"./types\";\n\nexport const presetSchema = z.object({\n args: z.array(z.string()),\n label: z.string().optional(),\n enableCrop: z.boolean().optional(),\n});\n\nexport const videoPluginOptionsSchema = z.object({\n presets: z\n .record(presetSchema)\n .refine((value) => Object.keys(value).length > 0, {\n message: \"At least one preset must be defined.\",\n }),\n queue: z\n .object({\n name: z.string().min(1).optional(),\n redisUrl: z.string().optional(),\n concurrency: z.number().int().positive().optional(),\n })\n .optional(),\n access: z.any().optional(),\n resolvePaths: z.any().optional(),\n});\n\nexport type NormalizedPresets = Record<string, Preset>;\n\nexport const normalizePresets = (\n presets: Record<string, Preset>,\n): NormalizedPresets => {\n const entries = Object.entries(presets).map(([name, preset]) => [\n name,\n { ...preset },\n ]);\n return Object.fromEntries(entries);\n};\n\nexport const ensureOptions = (\n options: VideoPluginOptions,\n): VideoPluginOptions =>\n videoPluginOptionsSchema.parse(options) as VideoPluginOptions;\n","import { z } from \"zod\";\n\nexport const cropSchema = z\n .object({\n x: z.number().min(0).max(1),\n y: z.number().min(0).max(1),\n width: z.number().positive().max(1),\n height: z.number().positive().max(1),\n })\n .refine((value) => value.width > 0 && value.height > 0, {\n message: \"Crop width and height must be > 0\",\n });\n\nexport const videoJobSchema = z.object({\n collection: z.string().min(1),\n id: z.union([z.string(), z.number()]).transform((value) => value.toString()),\n preset: z.string().min(1),\n crop: cropSchema.optional(),\n});\n\nexport type VideoJobData = z.infer<typeof videoJobSchema>;\nexport type CropData = z.infer<typeof cropSchema>;\n","import { CropRect } from \"../types\";\nimport type { CropData } from \"../queue/job.types\";\n\nexport type Dimensions = {\n width?: number;\n height?: number;\n};\n\nexport type BuildArgsInput = {\n presetArgs: string[];\n crop?: CropRect | CropData;\n dimensions?: Dimensions;\n defaultCrf?: number;\n};\n\nexport type BuildArgsResult = {\n globalOptions: string[];\n outputOptions: string[];\n};\n\nconst FASTSTART_FLAGS = [\"-movflags\", \"+faststart\"];\nconst CRF_FLAG = \"-crf\";\n\nconst hasCrf = (args: string[]): boolean => {\n for (let i = 0; i < args.length; i += 1) {\n if (args[i] === CRF_FLAG) {\n return true;\n }\n }\n\n return false;\n};\n\nconst hasFaststart = (args: string[]): boolean => {\n for (let i = 0; i < args.length; i += 1) {\n if (args[i] === \"-movflags\") {\n const value = args[i + 1];\n if (typeof value === \"string\" && value.includes(\"faststart\")) {\n return true;\n }\n }\n }\n\n return false;\n};\n\nconst extractFilters = (\n args: string[],\n): { rest: string[]; filters: string[] } => {\n const rest: string[] = [];\n const filters: string[] = [];\n\n for (let i = 0; i < args.length; i += 1) {\n const current = args[i];\n if (current === \"-vf\" || current === \"-filter:v\") {\n const value = args[i + 1];\n if (typeof value === \"string\") {\n filters.push(value);\n }\n i += 1;\n } else {\n rest.push(current);\n }\n }\n\n return { rest, filters };\n};\n\nconst clamp = (value: number, min: number, max: number): number => {\n if (Number.isNaN(value)) return min;\n if (value < min) return min;\n if (value > max) return max;\n return value;\n};\n\nconst buildCropFilter = (\n crop: CropRect | CropData,\n dimensions?: Dimensions,\n): string | undefined => {\n if (!dimensions?.width || !dimensions?.height) return undefined;\n\n const cropWidth = Math.max(1, Math.round(dimensions.width * crop.width));\n const cropHeight = Math.max(1, Math.round(dimensions.height * crop.height));\n\n const maxX = Math.max(0, dimensions.width - cropWidth);\n const maxY = Math.max(0, dimensions.height - cropHeight);\n\n const x = clamp(Math.round(dimensions.width * crop.x), 0, maxX);\n const y = clamp(Math.round(dimensions.height * crop.y), 0, maxY);\n\n return `crop=${cropWidth}:${cropHeight}:${x}:${y}`;\n};\n\n/**\n * Build ffmpeg argument lists from preset args, injecting defaults such as CRF\n * and faststart flags. Crop instructions are folded into the video filter chain\n * while preserving any filters defined by the preset.\n */\nexport const buildFfmpegArgs = ({\n presetArgs,\n crop,\n dimensions,\n defaultCrf = 24,\n}: BuildArgsInput): BuildArgsResult => {\n const args = [...presetArgs];\n const { rest, filters } = extractFilters(args);\n\n if (!hasCrf(rest)) {\n rest.push(CRF_FLAG, String(defaultCrf));\n }\n\n if (!hasFaststart(rest)) {\n rest.push(...FASTSTART_FLAGS);\n }\n\n if (crop) {\n const cropFilter = buildCropFilter(crop, dimensions);\n if (cropFilter) {\n filters.push(cropFilter);\n }\n }\n\n if (filters.length > 0) {\n rest.push(\"-vf\", filters.join(\",\"));\n }\n\n return {\n globalOptions: [\"-y\"],\n outputOptions: rest,\n };\n};\n","import ffmpeg from \"fluent-ffmpeg\";\nimport ffprobeStatic from \"ffprobe-static\";\n\nexport type VideoMetadata = {\n width?: number;\n height?: number;\n duration?: number;\n bitrate?: number;\n};\n\nif (ffprobeStatic.path) {\n ffmpeg.setFfprobePath(ffprobeStatic.path);\n}\n\nexport const probeVideo = async (filePath: string): Promise<VideoMetadata> =>\n new Promise((resolve, reject) => {\n ffmpeg.ffprobe(filePath, (error, metadata) => {\n if (error) {\n reject(error);\n return;\n }\n\n const videoStream = metadata.streams.find(\n (stream) => stream.codec_type === \"video\",\n );\n const width = videoStream?.width;\n const height = videoStream?.height;\n\n const durationRaw = videoStream?.duration ?? metadata.format?.duration;\n const duration =\n typeof durationRaw !== \"undefined\" ? Number(durationRaw) : undefined;\n\n const bitrateRaw = videoStream?.bit_rate ?? metadata.format?.bit_rate;\n const bitrate =\n typeof bitrateRaw !== \"undefined\" ? Number(bitrateRaw) : undefined;\n\n resolve({\n width: width ?? undefined,\n height: height ?? undefined,\n duration: Number.isNaN(duration) ? undefined : duration,\n bitrate: Number.isNaN(bitrate) ? undefined : bitrate,\n });\n });\n });\n","import path from \"node:path\";\nimport type { ResolvePathsArgs, ResolvePathsResult } from \"../types\";\n\nconst normalizeUrl = (input?: string, filename?: string): string => {\n if (!input) return filename ?? \"\";\n const parts = input.split(\"?\");\n const base = parts[0];\n const query = parts[1] ? `?${parts.slice(1).join(\"?\")}` : \"\";\n const lastSlash = base.lastIndexOf(\"/\");\n if (lastSlash === -1) {\n return filename ?? base;\n }\n\n const prefix = base.slice(0, lastSlash);\n const sanitized = filename ?? base.slice(lastSlash + 1);\n return `${prefix}/${sanitized}${query}`;\n};\n\nexport const defaultResolvePaths = ({\n original,\n presetName,\n}: ResolvePathsArgs): ResolvePathsResult => {\n const originalFilename = original.filename ?? path.basename(original.path);\n const extension =\n path.extname(originalFilename) || path.extname(original.path) || \".mp4\";\n const baseName = path.basename(originalFilename, extension);\n const variantFilename = `${baseName}_${presetName}${extension || \".mp4\"}`;\n\n const originalDir = path.dirname(original.path);\n const absoluteDir = path.isAbsolute(original.path)\n ? originalDir\n : path.join(process.cwd(), originalDir);\n\n const url = normalizeUrl(original.url, variantFilename);\n\n return {\n dir: absoluteDir,\n filename: variantFilename,\n url,\n };\n};\n\nexport const buildStoredPath = (\n originalPath: string,\n variantFilename: string,\n): string => {\n const originalDir = path.dirname(originalPath);\n return path.join(originalDir, variantFilename);\n};\n\nexport const buildWritePath = (dir: string, filename: string): string =>\n path.join(dir, filename);\n","import path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport payload, { type Payload, type PayloadRequest } from \"payload\";\nimport type { CollectionConfig, SanitizedCollectionConfig } from \"payload\";\n\nexport type PayloadClient = {\n findByID: (args: { collection: string; id: string }) => Promise<any>;\n update: (args: {\n collection: string;\n id: string;\n data: Record<string, any>;\n }) => Promise<any>;\n getCollectionConfig?: (\n slug: string,\n ) => (CollectionConfig | SanitizedCollectionConfig) | undefined;\n};\n\nlet cachedClient: PayloadClient | null = null;\nlet localInitialized = false;\n\nconst normalizeConfigPath = (configPath: string): string => {\n if (configPath.startsWith(\"file://\")) return configPath;\n return pathToFileURL(path.resolve(configPath)).href;\n};\n\nconst buildAuthHeaders = (token: string): Record<string, string> => ({\n Authorization: `Bearer ${token}`,\n \"X-Payload-API-Key\": token,\n});\n\nconst initLocalPayload = async (): Promise<PayloadClient | null> => {\n const secret = process.env.PAYLOAD_SECRET;\n const mongoURL = process.env.MONGODB_URI;\n\n if (!secret || !mongoURL) {\n return null;\n }\n\n try {\n const configPath = process.env.PAYLOAD_CONFIG_PATH;\n let configModule: unknown;\n\n if (configPath) {\n const imported = await import(normalizeConfigPath(configPath));\n configModule = imported?.default ?? imported;\n }\n\n if (!localInitialized) {\n const initOptions: Record<string, unknown> = {\n secret,\n mongoURL,\n local: true,\n };\n\n if (configModule) {\n initOptions.config = configModule;\n }\n\n await payload.init(initOptions as any);\n localInitialized = true;\n }\n\n const instance: Payload = payload;\n\n return {\n findByID: ({ collection, id }) =>\n instance.findByID({\n collection,\n id,\n }),\n update: ({ collection, id, data }) =>\n instance.update({\n collection,\n id,\n data,\n }),\n getCollectionConfig: (slug: string) =>\n instance.collections?.[slug]?.config,\n } satisfies PayloadClient;\n } catch (error) {\n console.warn(\n \"[video-processor] Failed to initialize Payload locally, falling back to REST client.\",\n error,\n );\n return null;\n }\n};\n\nconst initRestClient = async (): Promise<PayloadClient> => {\n const baseUrl =\n process.env.PAYLOAD_REST_URL ||\n process.env.PAYLOAD_PUBLIC_URL ||\n process.env.PAYLOAD_SERVER_URL;\n const token = process.env.PAYLOAD_ADMIN_TOKEN;\n\n if (!baseUrl || !token) {\n throw new Error(\n \"Unable to establish Payload REST client. Provide PAYLOAD_REST_URL (or PAYLOAD_PUBLIC_URL) and PAYLOAD_ADMIN_TOKEN.\",\n );\n }\n\n const base = baseUrl.replace(/\\/$/, \"\");\n const headers = buildAuthHeaders(token);\n\n const request = async <T>(url: string, init?: RequestInit): Promise<T> => {\n const response = await fetch(url, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...headers,\n ...init?.headers,\n },\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(\n `Payload REST request failed (${response.status}): ${text}`,\n );\n }\n\n return (await response.json()) as T;\n };\n\n return {\n findByID: async ({ collection, id }) => {\n const result = await request<{ doc?: any }>(\n `${base}/api/${collection}/${id}`,\n );\n return (result as any).doc ?? result;\n },\n update: async ({ collection, id, data }) => {\n const result = await request<{ doc?: any }>(\n `${base}/api/${collection}/${id}`,\n {\n method: \"PATCH\",\n body: JSON.stringify(data),\n },\n );\n return (result as any).doc ?? result;\n },\n } satisfies PayloadClient;\n};\n\nexport const getPayloadClient = async (): Promise<PayloadClient> => {\n if (cachedClient) return cachedClient;\n\n const localClient = await initLocalPayload();\n if (localClient) {\n cachedClient = localClient;\n return localClient;\n }\n\n const restClient = await initRestClient();\n cachedClient = restClient;\n return restClient;\n};\n\nexport const getCollectionConfigFromRequest = (\n req: PayloadRequest,\n slug: string,\n): CollectionConfig | null => {\n const payloadInstance = req.payload as Payload & {\n config?: Payload[\"config\"];\n };\n\n const collections = (payloadInstance as any)?.collections;\n const fromCollections = collections?.[slug];\n if (fromCollections) {\n if (typeof fromCollections.config === \"object\") {\n return fromCollections.config as CollectionConfig;\n }\n if (typeof fromCollections === \"object\") {\n return fromCollections as CollectionConfig;\n }\n }\n\n const configured = Array.isArray(payloadInstance.config?.collections)\n ? payloadInstance.config?.collections\n : undefined;\n\n if (configured) {\n const match = configured.find(\n (collection) => collection && collection.slug === slug,\n );\n if (match) {\n return match as CollectionConfig;\n }\n }\n\n return null;\n};\n","import path from \"node:path\";\nimport { mkdir, stat } from \"node:fs/promises\";\nimport ffmpeg from \"fluent-ffmpeg\";\nimport ffmpegStatic from \"ffmpeg-static\";\nimport ffprobeStatic from \"ffprobe-static\";\nimport { Worker } from \"bullmq\";\nimport IORedis from \"ioredis\";\nimport type { VideoPluginOptions, VariantRecord } from \"../types\";\nimport { ensureOptions } from \"../options\";\nimport { videoJobSchema, type VideoJobData } from \"./job.types\";\nimport { buildFfmpegArgs } from \"../ffmpeg/args\";\nimport { probeVideo } from \"../ffmpeg/probe\";\nimport {\n buildStoredPath,\n buildWritePath,\n defaultResolvePaths,\n} from \"../utils/paths\";\nimport { getPayloadClient } from \"../utils/payload\";\n\nconst envFfmpegPath = process.env.FFMPEG_BIN?.trim();\nconst ffmpegBinary =\n envFfmpegPath && envFfmpegPath.length > 0\n ? envFfmpegPath\n : typeof ffmpegStatic === \"string\"\n ? ffmpegStatic\n : null;\nif (ffmpegBinary) {\n ffmpeg.setFfmpegPath(ffmpegBinary);\n}\nif (ffprobeStatic.path) {\n ffmpeg.setFfprobePath(ffprobeStatic.path);\n}\n\nexport const createWorker = async (\n rawOptions: VideoPluginOptions,\n): Promise<Worker<VideoJobData>> => {\n const options = ensureOptions(rawOptions);\n const presets = options.presets;\n const queueName = options.queue?.name ?? \"video-transcode\";\n const concurrency = options.queue?.concurrency ?? 1;\n const redisUrl = options.queue?.redisUrl ?? process.env.REDIS_URL;\n\n const connection = redisUrl\n ? new IORedis(redisUrl, { maxRetriesPerRequest: null })\n : new IORedis({ maxRetriesPerRequest: null });\n\n const worker = new Worker<VideoJobData>(\n queueName,\n async (job) => {\n const parsed = videoJobSchema.parse(job.data);\n const preset = presets[parsed.preset];\n if (!preset) {\n throw new Error(`Unknown preset ${parsed.preset}`);\n }\n\n job.updateProgress(5);\n\n const client = await getPayloadClient();\n const document = await client.findByID({\n collection: parsed.collection,\n id: parsed.id,\n });\n\n if (!document) {\n throw new Error(\n `Document ${parsed.id} in collection ${parsed.collection} not found`,\n );\n }\n\n const originalPath: string | undefined = document?.path;\n const filename: string | undefined = document?.filename;\n const url: string | undefined = document?.url;\n\n if (!originalPath) {\n throw new Error(\"Source document does not expose a `path` property.\");\n }\n\n const absoluteInputPath = path.isAbsolute(originalPath)\n ? originalPath\n : path.join(process.cwd(), originalPath);\n\n const inputMetadata = await probeVideo(absoluteInputPath);\n job.updateProgress(15);\n\n const resolvePaths = options.resolvePaths ?? defaultResolvePaths;\n const collectionConfig =\n client.getCollectionConfig?.(parsed.collection) ?? null;\n\n const resolved = resolvePaths({\n doc: document,\n collection: collectionConfig,\n collectionSlug: parsed.collection,\n original: {\n filename: filename ?? path.basename(originalPath),\n path: originalPath,\n url: url ?? \"\",\n },\n presetName: parsed.preset,\n });\n\n const writeDir = resolved.dir;\n const writeFilename = resolved.filename;\n const targetUrl = resolved.url;\n const writePath = buildWritePath(writeDir, writeFilename);\n\n await mkdir(writeDir, { recursive: true });\n\n const { globalOptions, outputOptions } = buildFfmpegArgs({\n presetArgs: preset.args,\n crop: parsed.crop,\n dimensions: {\n width: inputMetadata.width,\n height: inputMetadata.height,\n },\n });\n\n await new Promise<void>((resolve, reject) => {\n const command = ffmpeg(absoluteInputPath);\n globalOptions.forEach((option) => command.addOption(option));\n command.outputOptions(outputOptions);\n command.output(writePath);\n command.on(\"progress\", (progress) => {\n if (typeof progress.percent === \"number\") {\n const bounded = Math.min(95, 15 + progress.percent * 0.7);\n void job.updateProgress(bounded);\n }\n });\n command.on(\"end\", () => resolve());\n command.on(\"error\", (error) => reject(error));\n command.run();\n });\n\n const fileStats = await stat(writePath);\n const outputMetadata = await probeVideo(writePath);\n\n const storedPath = buildStoredPath(originalPath, writeFilename);\n const variant: VariantRecord = {\n preset: parsed.preset,\n url: targetUrl,\n path: storedPath,\n size: fileStats.size,\n duration: outputMetadata.duration ?? inputMetadata.duration,\n width: outputMetadata.width ?? inputMetadata.width,\n height: outputMetadata.height ?? inputMetadata.height,\n bitrate: outputMetadata.bitrate,\n createdAt: new Date().toISOString(),\n };\n\n const existingVariants: VariantRecord[] = Array.isArray(document.variants)\n ? document.variants\n : [];\n\n const nextVariants = [\n ...existingVariants.filter((item) => item?.preset !== variant.preset),\n variant,\n ];\n\n await client.update({\n collection: parsed.collection,\n id: parsed.id,\n data: {\n variants: nextVariants,\n },\n });\n\n await job.updateProgress(100);\n\n return variant;\n },\n {\n connection,\n concurrency,\n },\n );\n\n worker.on(\"failed\", (job, error) => {\n console.error(`[video-processor] Job ${job?.id} failed`, error);\n });\n\n worker.on(\"completed\", (job) => {\n console.log(`[video-processor] Job ${job.id} completed`);\n });\n\n await worker.waitUntilReady();\n console.log(`[video-processor] Worker listening on queue ${queueName}`);\n\n const shutdown = async () => {\n await worker.close();\n await connection.quit();\n };\n\n process.once(\"SIGINT\", () => {\n void shutdown().then(() => process.exit(0));\n });\n process.once(\"SIGTERM\", () => {\n void shutdown().then(() => process.exit(0));\n });\n\n return worker;\n};\n","import path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { VideoPluginOptions } from \"../types\";\nimport { createWorker } from \"./createWorker\";\n\nconst loadOptions = async (): Promise<VideoPluginOptions> => {\n const modulePath = process.env.PAYLOAD_VIDEO_WORKER_CONFIG;\n if (!modulePath) {\n throw new Error(\n \"PAYLOAD_VIDEO_WORKER_CONFIG must point to a module exporting plugin options.\",\n );\n }\n\n const resolved =\n modulePath.startsWith(\".\") || modulePath.startsWith(\"/\")\n ? pathToFileURL(path.resolve(modulePath)).href\n : modulePath;\n\n const imported = await import(resolved);\n const options = (imported.default ?? imported) as VideoPluginOptions;\n\n if (!options || typeof options !== \"object\" || !(\"presets\" in options)) {\n throw new Error(\n \"Invalid worker options module. Ensure it exports VideoPluginOptions.\",\n );\n }\n\n return options;\n};\n\nconst main = async () => {\n const options = await loadOptions();\n await createWorker(options);\n};\n\nvoid main().catch((error) => {\n console.error(\"[video-processor] Worker failed to start\", error);\n process.exit(1);\n});\n"]}
@@ -0,0 +1,8 @@
1
+ .video-crop-wrapper {
2
+ position: relative;
3
+ width: 100%;
4
+ height: 320px;
5
+ border-radius: 0.75rem;
6
+ overflow: hidden;
7
+ background: rgba(15, 23, 42, 0.05);
8
+ }
@@ -0,0 +1,78 @@
1
+ import { PayloadRequest, CollectionConfig as CollectionConfig$1, Plugin, Config } from 'payload';
2
+
3
+ type Preset = {
4
+ args: string[];
5
+ label?: string;
6
+ enableCrop?: boolean;
7
+ };
8
+ type VariantRecord = {
9
+ preset: string;
10
+ url: string;
11
+ path: string;
12
+ size: number;
13
+ duration?: number;
14
+ width?: number;
15
+ height?: number;
16
+ bitrate?: number;
17
+ createdAt: string;
18
+ };
19
+ type QueueConfig = {
20
+ name?: string;
21
+ redisUrl?: string;
22
+ concurrency?: number;
23
+ };
24
+ type AccessEnqueueArgs = {
25
+ req: PayloadRequest;
26
+ };
27
+ type AccessVariantArgs = {
28
+ req: PayloadRequest;
29
+ collection: string;
30
+ id: string;
31
+ preset?: string;
32
+ variantId?: string;
33
+ variantIndex?: number;
34
+ };
35
+ type AccessControl = {
36
+ enqueue?: (args: AccessEnqueueArgs) => boolean | Promise<boolean>;
37
+ replaceOriginal?: (args: AccessVariantArgs) => boolean | Promise<boolean>;
38
+ removeVariant?: (args: AccessVariantArgs) => boolean | Promise<boolean>;
39
+ };
40
+ type CollectionConfig = CollectionConfig$1;
41
+ type ResolvePathsArgs = {
42
+ doc: unknown;
43
+ collection: CollectionConfig | null;
44
+ collectionSlug: string;
45
+ original: {
46
+ filename: string;
47
+ path: string;
48
+ url: string;
49
+ };
50
+ presetName: string;
51
+ };
52
+ type ResolvePathsResult = {
53
+ dir: string;
54
+ filename: string;
55
+ url: string;
56
+ };
57
+ type VideoPluginOptions = {
58
+ presets: Record<string, Preset>;
59
+ queue?: QueueConfig;
60
+ access?: AccessControl;
61
+ resolvePaths?: (args: ResolvePathsArgs) => ResolvePathsResult;
62
+ };
63
+ type VideoVariantFieldConfig = {
64
+ presets: Record<string, {
65
+ label: string;
66
+ enableCrop: boolean;
67
+ }>;
68
+ queueName: string;
69
+ enqueuePath: string;
70
+ statusPath: string;
71
+ removeVariantPath: string;
72
+ replaceOriginalPath: string;
73
+ collectionSlug: string;
74
+ };
75
+ type PayloadConfig = Config;
76
+ type PayloadPluginFactory = Plugin;
77
+
78
+ export type { PayloadPluginFactory as P, ResolvePathsArgs as R, VideoPluginOptions as V, ResolvePathsResult as a, Preset as b, VariantRecord as c, PayloadConfig as d, VideoVariantFieldConfig as e };