@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.
- package/README.md +164 -0
- package/dist/admin/VideoField.cjs +737 -0
- package/dist/admin/VideoField.cjs.map +1 -0
- package/dist/admin/VideoField.d.cts +44 -0
- package/dist/admin/VideoField.d.ts +44 -0
- package/dist/admin/VideoField.js +731 -0
- package/dist/admin/VideoField.js.map +1 -0
- package/dist/admin/client.d.cts +4 -0
- package/dist/admin/client.d.ts +4 -0
- package/dist/cli/start-worker.cjs +691 -0
- package/dist/cli/start-worker.cjs.map +1 -0
- package/dist/cli/start-worker.js +678 -0
- package/dist/cli/start-worker.js.map +1 -0
- package/dist/exports/client.cjs +737 -0
- package/dist/exports/client.cjs.map +1 -0
- package/dist/exports/client.js +731 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/index.cjs +1141 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +1125 -0
- package/dist/index.js.map +1 -0
- package/dist/queue/worker.cjs +483 -0
- package/dist/queue/worker.cjs.map +1 -0
- package/dist/queue/worker.d.cts +2 -0
- package/dist/queue/worker.d.ts +2 -0
- package/dist/queue/worker.js +472 -0
- package/dist/queue/worker.js.map +1 -0
- package/dist/styles-GMHOOV63.css +8 -0
- package/dist/types-BjFcE25o.d.cts +78 -0
- package/dist/types-BjFcE25o.d.ts +78 -0
- package/package.json +91 -0
- package/types/index.d.ts +6 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import ffmpeg2 from 'fluent-ffmpeg';
|
|
4
|
+
import ffprobeStatic from 'ffprobe-static';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
import payload from 'payload';
|
|
8
|
+
import { mkdir, stat } from 'fs/promises';
|
|
9
|
+
import ffmpegStatic from 'ffmpeg-static';
|
|
10
|
+
import { Worker } from 'bullmq';
|
|
11
|
+
import IORedis from 'ioredis';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import dotenv from 'dotenv';
|
|
14
|
+
|
|
15
|
+
var __defProp = Object.defineProperty;
|
|
16
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
17
|
+
var __esm = (fn, res) => function __init() {
|
|
18
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
|
+
};
|
|
24
|
+
var presetSchema, videoPluginOptionsSchema, ensureOptions;
|
|
25
|
+
var init_options = __esm({
|
|
26
|
+
"src/options.ts"() {
|
|
27
|
+
presetSchema = z.object({
|
|
28
|
+
args: z.array(z.string()),
|
|
29
|
+
label: z.string().optional(),
|
|
30
|
+
enableCrop: z.boolean().optional()
|
|
31
|
+
});
|
|
32
|
+
videoPluginOptionsSchema = z.object({
|
|
33
|
+
presets: z.record(presetSchema).refine((value) => Object.keys(value).length > 0, {
|
|
34
|
+
message: "At least one preset must be defined."
|
|
35
|
+
}),
|
|
36
|
+
queue: z.object({
|
|
37
|
+
name: z.string().min(1).optional(),
|
|
38
|
+
redisUrl: z.string().optional(),
|
|
39
|
+
concurrency: z.number().int().positive().optional()
|
|
40
|
+
}).optional(),
|
|
41
|
+
access: z.any().optional(),
|
|
42
|
+
resolvePaths: z.any().optional()
|
|
43
|
+
});
|
|
44
|
+
ensureOptions = (options) => videoPluginOptionsSchema.parse(options);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
var cropSchema, videoJobSchema;
|
|
48
|
+
var init_job_types = __esm({
|
|
49
|
+
"src/queue/job.types.ts"() {
|
|
50
|
+
cropSchema = z.object({
|
|
51
|
+
x: z.number().min(0).max(1),
|
|
52
|
+
y: z.number().min(0).max(1),
|
|
53
|
+
width: z.number().positive().max(1),
|
|
54
|
+
height: z.number().positive().max(1)
|
|
55
|
+
}).refine((value) => value.width > 0 && value.height > 0, {
|
|
56
|
+
message: "Crop width and height must be > 0"
|
|
57
|
+
});
|
|
58
|
+
videoJobSchema = z.object({
|
|
59
|
+
collection: z.string().min(1),
|
|
60
|
+
id: z.union([z.string(), z.number()]).transform((value) => value.toString()),
|
|
61
|
+
preset: z.string().min(1),
|
|
62
|
+
crop: cropSchema.optional()
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// src/ffmpeg/args.ts
|
|
68
|
+
var FASTSTART_FLAGS, CRF_FLAG, hasCrf, hasFaststart, extractFilters, clamp, buildCropFilter, buildFfmpegArgs;
|
|
69
|
+
var init_args = __esm({
|
|
70
|
+
"src/ffmpeg/args.ts"() {
|
|
71
|
+
FASTSTART_FLAGS = ["-movflags", "+faststart"];
|
|
72
|
+
CRF_FLAG = "-crf";
|
|
73
|
+
hasCrf = (args) => {
|
|
74
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
75
|
+
if (args[i] === CRF_FLAG) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
hasFaststart = (args) => {
|
|
82
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
83
|
+
if (args[i] === "-movflags") {
|
|
84
|
+
const value = args[i + 1];
|
|
85
|
+
if (typeof value === "string" && value.includes("faststart")) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
92
|
+
extractFilters = (args) => {
|
|
93
|
+
const rest = [];
|
|
94
|
+
const filters = [];
|
|
95
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
96
|
+
const current = args[i];
|
|
97
|
+
if (current === "-vf" || current === "-filter:v") {
|
|
98
|
+
const value = args[i + 1];
|
|
99
|
+
if (typeof value === "string") {
|
|
100
|
+
filters.push(value);
|
|
101
|
+
}
|
|
102
|
+
i += 1;
|
|
103
|
+
} else {
|
|
104
|
+
rest.push(current);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { rest, filters };
|
|
108
|
+
};
|
|
109
|
+
clamp = (value, min, max) => {
|
|
110
|
+
if (Number.isNaN(value)) return min;
|
|
111
|
+
if (value < min) return min;
|
|
112
|
+
if (value > max) return max;
|
|
113
|
+
return value;
|
|
114
|
+
};
|
|
115
|
+
buildCropFilter = (crop, dimensions) => {
|
|
116
|
+
if (!dimensions?.width || !dimensions?.height) return void 0;
|
|
117
|
+
const cropWidth = Math.max(1, Math.round(dimensions.width * crop.width));
|
|
118
|
+
const cropHeight = Math.max(1, Math.round(dimensions.height * crop.height));
|
|
119
|
+
const maxX = Math.max(0, dimensions.width - cropWidth);
|
|
120
|
+
const maxY = Math.max(0, dimensions.height - cropHeight);
|
|
121
|
+
const x = clamp(Math.round(dimensions.width * crop.x), 0, maxX);
|
|
122
|
+
const y = clamp(Math.round(dimensions.height * crop.y), 0, maxY);
|
|
123
|
+
return `crop=${cropWidth}:${cropHeight}:${x}:${y}`;
|
|
124
|
+
};
|
|
125
|
+
buildFfmpegArgs = ({
|
|
126
|
+
presetArgs,
|
|
127
|
+
crop,
|
|
128
|
+
dimensions,
|
|
129
|
+
defaultCrf = 24
|
|
130
|
+
}) => {
|
|
131
|
+
const args = [...presetArgs];
|
|
132
|
+
const { rest, filters } = extractFilters(args);
|
|
133
|
+
if (!hasCrf(rest)) {
|
|
134
|
+
rest.push(CRF_FLAG, String(defaultCrf));
|
|
135
|
+
}
|
|
136
|
+
if (!hasFaststart(rest)) {
|
|
137
|
+
rest.push(...FASTSTART_FLAGS);
|
|
138
|
+
}
|
|
139
|
+
if (crop) {
|
|
140
|
+
const cropFilter = buildCropFilter(crop, dimensions);
|
|
141
|
+
if (cropFilter) {
|
|
142
|
+
filters.push(cropFilter);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (filters.length > 0) {
|
|
146
|
+
rest.push("-vf", filters.join(","));
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
globalOptions: ["-y"],
|
|
150
|
+
outputOptions: rest
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
var probeVideo;
|
|
156
|
+
var init_probe = __esm({
|
|
157
|
+
"src/ffmpeg/probe.ts"() {
|
|
158
|
+
if (ffprobeStatic.path) {
|
|
159
|
+
ffmpeg2.setFfprobePath(ffprobeStatic.path);
|
|
160
|
+
}
|
|
161
|
+
probeVideo = async (filePath) => new Promise((resolve, reject) => {
|
|
162
|
+
ffmpeg2.ffprobe(filePath, (error, metadata) => {
|
|
163
|
+
if (error) {
|
|
164
|
+
reject(error);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const videoStream = metadata.streams.find(
|
|
168
|
+
(stream) => stream.codec_type === "video"
|
|
169
|
+
);
|
|
170
|
+
const width = videoStream?.width;
|
|
171
|
+
const height = videoStream?.height;
|
|
172
|
+
const durationRaw = videoStream?.duration ?? metadata.format?.duration;
|
|
173
|
+
const duration = typeof durationRaw !== "undefined" ? Number(durationRaw) : void 0;
|
|
174
|
+
const bitrateRaw = videoStream?.bit_rate ?? metadata.format?.bit_rate;
|
|
175
|
+
const bitrate = typeof bitrateRaw !== "undefined" ? Number(bitrateRaw) : void 0;
|
|
176
|
+
resolve({
|
|
177
|
+
width: width ?? void 0,
|
|
178
|
+
height: height ?? void 0,
|
|
179
|
+
duration: Number.isNaN(duration) ? void 0 : duration,
|
|
180
|
+
bitrate: Number.isNaN(bitrate) ? void 0 : bitrate
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
var normalizeUrl, defaultResolvePaths, buildStoredPath, buildWritePath;
|
|
187
|
+
var init_paths = __esm({
|
|
188
|
+
"src/utils/paths.ts"() {
|
|
189
|
+
normalizeUrl = (input, filename) => {
|
|
190
|
+
if (!input) return filename ?? "";
|
|
191
|
+
const parts = input.split("?");
|
|
192
|
+
const base = parts[0];
|
|
193
|
+
const query = parts[1] ? `?${parts.slice(1).join("?")}` : "";
|
|
194
|
+
const lastSlash = base.lastIndexOf("/");
|
|
195
|
+
if (lastSlash === -1) {
|
|
196
|
+
return filename ?? base;
|
|
197
|
+
}
|
|
198
|
+
const prefix = base.slice(0, lastSlash);
|
|
199
|
+
const sanitized = filename ?? base.slice(lastSlash + 1);
|
|
200
|
+
return `${prefix}/${sanitized}${query}`;
|
|
201
|
+
};
|
|
202
|
+
defaultResolvePaths = ({
|
|
203
|
+
original,
|
|
204
|
+
presetName
|
|
205
|
+
}) => {
|
|
206
|
+
const originalFilename = original.filename ?? path.basename(original.path);
|
|
207
|
+
const extension = path.extname(originalFilename) || path.extname(original.path) || ".mp4";
|
|
208
|
+
const baseName = path.basename(originalFilename, extension);
|
|
209
|
+
const variantFilename = `${baseName}_${presetName}${extension}`;
|
|
210
|
+
const originalDir = path.dirname(original.path);
|
|
211
|
+
const absoluteDir = path.isAbsolute(original.path) ? originalDir : path.join(process.cwd(), originalDir);
|
|
212
|
+
const url = normalizeUrl(original.url, variantFilename);
|
|
213
|
+
return {
|
|
214
|
+
dir: absoluteDir,
|
|
215
|
+
filename: variantFilename,
|
|
216
|
+
url
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
buildStoredPath = (originalPath, variantFilename) => {
|
|
220
|
+
const originalDir = path.dirname(originalPath);
|
|
221
|
+
return path.join(originalDir, variantFilename);
|
|
222
|
+
};
|
|
223
|
+
buildWritePath = (dir, filename) => path.join(dir, filename);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
var cachedClient, localInitialized, normalizeConfigPath, buildAuthHeaders, initLocalPayload, initRestClient, getPayloadClient;
|
|
227
|
+
var init_payload = __esm({
|
|
228
|
+
"src/utils/payload.ts"() {
|
|
229
|
+
cachedClient = null;
|
|
230
|
+
localInitialized = false;
|
|
231
|
+
normalizeConfigPath = (configPath) => {
|
|
232
|
+
if (configPath.startsWith("file://")) return configPath;
|
|
233
|
+
return pathToFileURL(path.resolve(configPath)).href;
|
|
234
|
+
};
|
|
235
|
+
buildAuthHeaders = (token) => ({
|
|
236
|
+
Authorization: `Bearer ${token}`,
|
|
237
|
+
"X-Payload-API-Key": token
|
|
238
|
+
});
|
|
239
|
+
initLocalPayload = async () => {
|
|
240
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
241
|
+
const mongoURL = process.env.MONGODB_URI;
|
|
242
|
+
if (!secret || !mongoURL) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const configPath = process.env.PAYLOAD_CONFIG_PATH;
|
|
247
|
+
let configModule;
|
|
248
|
+
if (configPath) {
|
|
249
|
+
const imported = await import(normalizeConfigPath(configPath));
|
|
250
|
+
configModule = imported?.default ?? imported;
|
|
251
|
+
}
|
|
252
|
+
if (!localInitialized) {
|
|
253
|
+
const initOptions = {
|
|
254
|
+
secret,
|
|
255
|
+
mongoURL,
|
|
256
|
+
local: true
|
|
257
|
+
};
|
|
258
|
+
if (configModule) {
|
|
259
|
+
initOptions.config = configModule;
|
|
260
|
+
}
|
|
261
|
+
await payload.init(initOptions);
|
|
262
|
+
localInitialized = true;
|
|
263
|
+
}
|
|
264
|
+
const instance = payload;
|
|
265
|
+
return {
|
|
266
|
+
findByID: ({ collection, id }) => instance.findByID({
|
|
267
|
+
collection,
|
|
268
|
+
id
|
|
269
|
+
}),
|
|
270
|
+
update: ({ collection, id, data }) => instance.update({
|
|
271
|
+
collection,
|
|
272
|
+
id,
|
|
273
|
+
data
|
|
274
|
+
}),
|
|
275
|
+
getCollectionConfig: (slug) => instance.collections?.[slug]?.config
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.warn(
|
|
279
|
+
"[video-processor] Failed to initialize Payload locally, falling back to REST client.",
|
|
280
|
+
error
|
|
281
|
+
);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
initRestClient = async () => {
|
|
286
|
+
const baseUrl = process.env.PAYLOAD_REST_URL || process.env.PAYLOAD_PUBLIC_URL || process.env.PAYLOAD_SERVER_URL;
|
|
287
|
+
const token = process.env.PAYLOAD_ADMIN_TOKEN;
|
|
288
|
+
if (!baseUrl || !token) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
"Unable to establish Payload REST client. Provide PAYLOAD_REST_URL (or PAYLOAD_PUBLIC_URL) and PAYLOAD_ADMIN_TOKEN."
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
294
|
+
const headers = buildAuthHeaders(token);
|
|
295
|
+
const request = async (url, init) => {
|
|
296
|
+
const response = await fetch(url, {
|
|
297
|
+
...init,
|
|
298
|
+
headers: {
|
|
299
|
+
"Content-Type": "application/json",
|
|
300
|
+
Accept: "application/json",
|
|
301
|
+
...headers,
|
|
302
|
+
...init?.headers
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
const text = await response.text();
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Payload REST request failed (${response.status}): ${text}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return await response.json();
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
findByID: async ({ collection, id }) => {
|
|
315
|
+
const result = await request(
|
|
316
|
+
`${base}/api/${collection}/${id}`
|
|
317
|
+
);
|
|
318
|
+
return result.doc ?? result;
|
|
319
|
+
},
|
|
320
|
+
update: async ({ collection, id, data }) => {
|
|
321
|
+
const result = await request(
|
|
322
|
+
`${base}/api/${collection}/${id}`,
|
|
323
|
+
{
|
|
324
|
+
method: "PATCH",
|
|
325
|
+
body: JSON.stringify(data)
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
return result.doc ?? result;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
getPayloadClient = async () => {
|
|
333
|
+
if (cachedClient) return cachedClient;
|
|
334
|
+
const localClient = await initLocalPayload();
|
|
335
|
+
if (localClient) {
|
|
336
|
+
cachedClient = localClient;
|
|
337
|
+
return localClient;
|
|
338
|
+
}
|
|
339
|
+
const restClient = await initRestClient();
|
|
340
|
+
cachedClient = restClient;
|
|
341
|
+
return restClient;
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// src/queue/createWorker.ts
|
|
347
|
+
var createWorker_exports = {};
|
|
348
|
+
__export(createWorker_exports, {
|
|
349
|
+
createWorker: () => createWorker
|
|
350
|
+
});
|
|
351
|
+
var envFfmpegPath, ffmpegBinary, createWorker;
|
|
352
|
+
var init_createWorker = __esm({
|
|
353
|
+
"src/queue/createWorker.ts"() {
|
|
354
|
+
init_options();
|
|
355
|
+
init_job_types();
|
|
356
|
+
init_args();
|
|
357
|
+
init_probe();
|
|
358
|
+
init_paths();
|
|
359
|
+
init_payload();
|
|
360
|
+
envFfmpegPath = process.env.FFMPEG_BIN?.trim();
|
|
361
|
+
ffmpegBinary = envFfmpegPath && envFfmpegPath.length > 0 ? envFfmpegPath : typeof ffmpegStatic === "string" ? ffmpegStatic : null;
|
|
362
|
+
if (ffmpegBinary) {
|
|
363
|
+
ffmpeg2.setFfmpegPath(ffmpegBinary);
|
|
364
|
+
}
|
|
365
|
+
if (ffprobeStatic.path) {
|
|
366
|
+
ffmpeg2.setFfprobePath(ffprobeStatic.path);
|
|
367
|
+
}
|
|
368
|
+
createWorker = async (rawOptions) => {
|
|
369
|
+
const options = ensureOptions(rawOptions);
|
|
370
|
+
const presets = options.presets;
|
|
371
|
+
const queueName = options.queue?.name ?? "video-transcode";
|
|
372
|
+
const concurrency = options.queue?.concurrency ?? 1;
|
|
373
|
+
const redisUrl = options.queue?.redisUrl ?? process.env.REDIS_URL;
|
|
374
|
+
const connection = redisUrl ? new IORedis(redisUrl, { maxRetriesPerRequest: null }) : new IORedis({ maxRetriesPerRequest: null });
|
|
375
|
+
const worker = new Worker(
|
|
376
|
+
queueName,
|
|
377
|
+
async (job) => {
|
|
378
|
+
const parsed = videoJobSchema.parse(job.data);
|
|
379
|
+
const preset = presets[parsed.preset];
|
|
380
|
+
if (!preset) {
|
|
381
|
+
throw new Error(`Unknown preset ${parsed.preset}`);
|
|
382
|
+
}
|
|
383
|
+
job.updateProgress(5);
|
|
384
|
+
const client = await getPayloadClient();
|
|
385
|
+
const document = await client.findByID({
|
|
386
|
+
collection: parsed.collection,
|
|
387
|
+
id: parsed.id
|
|
388
|
+
});
|
|
389
|
+
if (!document) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Document ${parsed.id} in collection ${parsed.collection} not found`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
const originalPath = document?.path;
|
|
395
|
+
const filename = document?.filename;
|
|
396
|
+
const url = document?.url;
|
|
397
|
+
if (!originalPath) {
|
|
398
|
+
throw new Error("Source document does not expose a `path` property.");
|
|
399
|
+
}
|
|
400
|
+
const absoluteInputPath = path.isAbsolute(originalPath) ? originalPath : path.join(process.cwd(), originalPath);
|
|
401
|
+
const inputMetadata = await probeVideo(absoluteInputPath);
|
|
402
|
+
job.updateProgress(15);
|
|
403
|
+
const resolvePaths = options.resolvePaths ?? defaultResolvePaths;
|
|
404
|
+
const collectionConfig = client.getCollectionConfig?.(parsed.collection) ?? null;
|
|
405
|
+
const resolved = resolvePaths({
|
|
406
|
+
doc: document,
|
|
407
|
+
collection: collectionConfig,
|
|
408
|
+
collectionSlug: parsed.collection,
|
|
409
|
+
original: {
|
|
410
|
+
filename: filename ?? path.basename(originalPath),
|
|
411
|
+
path: originalPath,
|
|
412
|
+
url: url ?? ""
|
|
413
|
+
},
|
|
414
|
+
presetName: parsed.preset
|
|
415
|
+
});
|
|
416
|
+
const writeDir = resolved.dir;
|
|
417
|
+
const writeFilename = resolved.filename;
|
|
418
|
+
const targetUrl = resolved.url;
|
|
419
|
+
const writePath = buildWritePath(writeDir, writeFilename);
|
|
420
|
+
await mkdir(writeDir, { recursive: true });
|
|
421
|
+
const { globalOptions, outputOptions } = buildFfmpegArgs({
|
|
422
|
+
presetArgs: preset.args,
|
|
423
|
+
crop: parsed.crop,
|
|
424
|
+
dimensions: {
|
|
425
|
+
width: inputMetadata.width,
|
|
426
|
+
height: inputMetadata.height
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
await new Promise((resolve, reject) => {
|
|
430
|
+
const command = ffmpeg2(absoluteInputPath);
|
|
431
|
+
globalOptions.forEach((option) => command.addOption(option));
|
|
432
|
+
command.outputOptions(outputOptions);
|
|
433
|
+
command.output(writePath);
|
|
434
|
+
command.on("progress", (progress) => {
|
|
435
|
+
if (typeof progress.percent === "number") {
|
|
436
|
+
const bounded = Math.min(95, 15 + progress.percent * 0.7);
|
|
437
|
+
void job.updateProgress(bounded);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
command.on("end", () => resolve());
|
|
441
|
+
command.on("error", (error) => reject(error));
|
|
442
|
+
command.run();
|
|
443
|
+
});
|
|
444
|
+
const fileStats = await stat(writePath);
|
|
445
|
+
const outputMetadata = await probeVideo(writePath);
|
|
446
|
+
const storedPath = buildStoredPath(originalPath, writeFilename);
|
|
447
|
+
const variant = {
|
|
448
|
+
preset: parsed.preset,
|
|
449
|
+
url: targetUrl,
|
|
450
|
+
path: storedPath,
|
|
451
|
+
size: fileStats.size,
|
|
452
|
+
duration: outputMetadata.duration ?? inputMetadata.duration,
|
|
453
|
+
width: outputMetadata.width ?? inputMetadata.width,
|
|
454
|
+
height: outputMetadata.height ?? inputMetadata.height,
|
|
455
|
+
bitrate: outputMetadata.bitrate,
|
|
456
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
457
|
+
};
|
|
458
|
+
const existingVariants = Array.isArray(document.variants) ? document.variants : [];
|
|
459
|
+
const nextVariants = [
|
|
460
|
+
...existingVariants.filter((item) => item?.preset !== variant.preset),
|
|
461
|
+
variant
|
|
462
|
+
];
|
|
463
|
+
await client.update({
|
|
464
|
+
collection: parsed.collection,
|
|
465
|
+
id: parsed.id,
|
|
466
|
+
data: {
|
|
467
|
+
variants: nextVariants
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
await job.updateProgress(100);
|
|
471
|
+
return variant;
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
connection,
|
|
475
|
+
concurrency
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
worker.on("failed", (job, error) => {
|
|
479
|
+
console.error(`[video-processor] Job ${job?.id} failed`, error);
|
|
480
|
+
});
|
|
481
|
+
worker.on("completed", (job) => {
|
|
482
|
+
console.log(`[video-processor] Job ${job.id} completed`);
|
|
483
|
+
});
|
|
484
|
+
await worker.waitUntilReady();
|
|
485
|
+
console.log(`[video-processor] Worker listening on queue ${queueName}`);
|
|
486
|
+
const shutdown = async () => {
|
|
487
|
+
await worker.close();
|
|
488
|
+
await connection.quit();
|
|
489
|
+
};
|
|
490
|
+
process.once("SIGINT", () => {
|
|
491
|
+
void shutdown().then(() => process.exit(0));
|
|
492
|
+
});
|
|
493
|
+
process.once("SIGTERM", () => {
|
|
494
|
+
void shutdown().then(() => process.exit(0));
|
|
495
|
+
});
|
|
496
|
+
return worker;
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
var helpText = `
|
|
501
|
+
payload-video-worker
|
|
502
|
+
|
|
503
|
+
Start the Payload video processing worker using the same options you pass to the plugin.
|
|
504
|
+
|
|
505
|
+
Usage:
|
|
506
|
+
payload-video-worker --config ./dist-config/videoPluginOptions.js [options]
|
|
507
|
+
|
|
508
|
+
Options:
|
|
509
|
+
-c, --config <path> Path to a module exporting the worker options (defaults to PAYLOAD_VIDEO_WORKER_CONFIG)
|
|
510
|
+
-p, --payload-config <path> Path to a Payload config file for local execution (sets PAYLOAD_CONFIG_PATH)
|
|
511
|
+
-s, --static-dir <path> Override STATIC_DIR before starting the worker
|
|
512
|
+
-e, --env <path> Load additional .env file (can be repeated)
|
|
513
|
+
--no-default-env Skip automatic loading of .env, .env.local, .env.development, .env.production
|
|
514
|
+
-h, --help Show this message and exit
|
|
515
|
+
`.trim();
|
|
516
|
+
var parseArgs = (argv) => {
|
|
517
|
+
const options = {
|
|
518
|
+
envFiles: [],
|
|
519
|
+
loadDefaultEnv: true
|
|
520
|
+
};
|
|
521
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
522
|
+
const arg = argv[index];
|
|
523
|
+
switch (arg) {
|
|
524
|
+
case "-c":
|
|
525
|
+
case "--config": {
|
|
526
|
+
const next = argv[index + 1];
|
|
527
|
+
if (!next) {
|
|
528
|
+
throw new Error("Missing value for --config option.");
|
|
529
|
+
}
|
|
530
|
+
options.configPath = next;
|
|
531
|
+
index += 1;
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case "-p":
|
|
535
|
+
case "--payload-config": {
|
|
536
|
+
const next = argv[index + 1];
|
|
537
|
+
if (!next) {
|
|
538
|
+
throw new Error("Missing value for --payload-config option.");
|
|
539
|
+
}
|
|
540
|
+
options.payloadConfigPath = next;
|
|
541
|
+
index += 1;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
case "-s":
|
|
545
|
+
case "--static-dir": {
|
|
546
|
+
const next = argv[index + 1];
|
|
547
|
+
if (!next) {
|
|
548
|
+
throw new Error("Missing value for --static-dir option.");
|
|
549
|
+
}
|
|
550
|
+
options.staticDir = next;
|
|
551
|
+
index += 1;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case "-e":
|
|
555
|
+
case "--env": {
|
|
556
|
+
const next = argv[index + 1];
|
|
557
|
+
if (!next) {
|
|
558
|
+
throw new Error("Missing value for --env option.");
|
|
559
|
+
}
|
|
560
|
+
options.envFiles.push(next);
|
|
561
|
+
index += 1;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case "--no-default-env": {
|
|
565
|
+
options.loadDefaultEnv = false;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case "-h":
|
|
569
|
+
case "--help": {
|
|
570
|
+
options.showHelp = true;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
default: {
|
|
574
|
+
if (arg.startsWith("-")) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`Unknown option \`${arg}\`. Use --help to list supported flags.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
options.envFiles.push(arg);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return options;
|
|
584
|
+
};
|
|
585
|
+
var printHelp = () => {
|
|
586
|
+
console.log(helpText);
|
|
587
|
+
};
|
|
588
|
+
var resolvePath = (input) => path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
|
|
589
|
+
var loadEnvFile = (filePath) => {
|
|
590
|
+
const resolved = resolvePath(filePath);
|
|
591
|
+
if (!fs.existsSync(resolved)) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
dotenv.config({ path: resolved, override: false });
|
|
595
|
+
console.log(`[video-processor] Loaded env file ${resolved}`);
|
|
596
|
+
return true;
|
|
597
|
+
};
|
|
598
|
+
var ensureEnvVariables = () => {
|
|
599
|
+
if (!process.env.MONGODB_URI && process.env.DATABASE_URI) {
|
|
600
|
+
process.env.MONGODB_URI = process.env.DATABASE_URI;
|
|
601
|
+
}
|
|
602
|
+
if (!process.env.PAYLOAD_SECRET) {
|
|
603
|
+
process.env.PAYLOAD_SECRET = "dev-secret";
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
var toModuleUrl = (input) => pathToFileURL(resolvePath(input)).href;
|
|
607
|
+
var loadWorkerOptions = async (modulePath) => {
|
|
608
|
+
const moduleUrl = toModuleUrl(modulePath);
|
|
609
|
+
const imported = await import(moduleUrl);
|
|
610
|
+
const value = imported?.default ?? imported;
|
|
611
|
+
if (!value || typeof value !== "object") {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Worker options module at ${modulePath} did not export a configuration object.`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
return value;
|
|
617
|
+
};
|
|
618
|
+
void (async () => {
|
|
619
|
+
try {
|
|
620
|
+
const options = parseArgs(process.argv.slice(2));
|
|
621
|
+
if (options.showHelp) {
|
|
622
|
+
printHelp();
|
|
623
|
+
process.exit(0);
|
|
624
|
+
}
|
|
625
|
+
const defaultEnvFiles = [
|
|
626
|
+
".env",
|
|
627
|
+
".env.local",
|
|
628
|
+
".env.development",
|
|
629
|
+
".env.production"
|
|
630
|
+
];
|
|
631
|
+
if (options.loadDefaultEnv) {
|
|
632
|
+
for (const candidate of defaultEnvFiles) {
|
|
633
|
+
loadEnvFile(candidate);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
for (const envFile of options.envFiles) {
|
|
637
|
+
loadEnvFile(envFile);
|
|
638
|
+
}
|
|
639
|
+
if (options.staticDir) {
|
|
640
|
+
process.env.STATIC_DIR = resolvePath(options.staticDir);
|
|
641
|
+
console.log(
|
|
642
|
+
`[video-processor] Using STATIC_DIR=${process.env.STATIC_DIR}`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (options.payloadConfigPath) {
|
|
646
|
+
const resolvedPayloadConfig = resolvePath(options.payloadConfigPath);
|
|
647
|
+
process.env.PAYLOAD_CONFIG_PATH = resolvedPayloadConfig;
|
|
648
|
+
console.log(
|
|
649
|
+
`[video-processor] Using PAYLOAD_CONFIG_PATH=${resolvedPayloadConfig}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const configPath = options.configPath ?? process.env.PAYLOAD_VIDEO_WORKER_CONFIG;
|
|
653
|
+
if (!configPath) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
"No worker config provided. Pass --config or set PAYLOAD_VIDEO_WORKER_CONFIG."
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
process.env.PAYLOAD_VIDEO_WORKER_CONFIG = resolvePath(configPath);
|
|
659
|
+
ensureEnvVariables();
|
|
660
|
+
const workerOptions = await loadWorkerOptions(
|
|
661
|
+
process.env.PAYLOAD_VIDEO_WORKER_CONFIG
|
|
662
|
+
);
|
|
663
|
+
const { createWorker: createWorker2 } = await Promise.resolve().then(() => (init_createWorker(), createWorker_exports));
|
|
664
|
+
await createWorker2(workerOptions);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
if (error instanceof Error) {
|
|
667
|
+
console.error("[video-processor] Worker failed to start:", error.message);
|
|
668
|
+
if (error.stack) {
|
|
669
|
+
console.error(error.stack);
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
console.error("[video-processor] Worker failed to start:", error);
|
|
673
|
+
}
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
})();
|
|
677
|
+
//# sourceMappingURL=start-worker.js.map
|
|
678
|
+
//# sourceMappingURL=start-worker.js.map
|