@frkntmbs/strapi-plugin-video-optimizer 1.0.0

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +286 -0
  3. package/admin/custom.d.ts +8 -0
  4. package/admin/src/buildVersion.ts +3 -0
  5. package/admin/src/components/AssetOptimizationLabel.tsx +61 -0
  6. package/admin/src/components/BridgeProviders.tsx +123 -0
  7. package/admin/src/components/MediaLibraryCacheBridge.tsx +24 -0
  8. package/admin/src/components/MediaLibraryCardActionsBridge.tsx +249 -0
  9. package/admin/src/components/MediaLibraryJobWatcher.tsx +136 -0
  10. package/admin/src/components/MediaLibraryProgressBridge.tsx +97 -0
  11. package/admin/src/components/OptimizationChoicePicker.tsx +65 -0
  12. package/admin/src/components/OptimizationResizeFields.tsx +120 -0
  13. package/admin/src/components/OptimizationVideoFields.tsx +217 -0
  14. package/admin/src/components/UploadEnhancerBridge.tsx +205 -0
  15. package/admin/src/components/upload/PendingAssetStep.tsx +97 -0
  16. package/admin/src/defaultGlobalSettings.ts +32 -0
  17. package/admin/src/hooks/useDefaultOptimizationMode.ts +24 -0
  18. package/admin/src/hooks/useUploadWithOptimizer.ts +45 -0
  19. package/admin/src/index.ts +84 -0
  20. package/admin/src/pages/SettingsPage.tsx +208 -0
  21. package/admin/src/pluginId.ts +79 -0
  22. package/admin/src/translations/en.json +74 -0
  23. package/admin/src/translations/tr.json +74 -0
  24. package/admin/src/utils/adminFetch.ts +57 -0
  25. package/admin/src/utils/captureQueryClient.ts +34 -0
  26. package/admin/src/utils/debugMediaLibraryProgress.ts +70 -0
  27. package/admin/src/utils/extractAssetDimensions.ts +22 -0
  28. package/admin/src/utils/initJobPoller.ts +173 -0
  29. package/admin/src/utils/initMediaLibraryCardActions.ts +308 -0
  30. package/admin/src/utils/initMediaLibraryProgress.ts +219 -0
  31. package/admin/src/utils/initUploadEnhancer.ts +447 -0
  32. package/admin/src/utils/invalidateMediaLibrary.ts +203 -0
  33. package/admin/src/utils/jobProgressStore.ts +113 -0
  34. package/admin/src/utils/mediaLibraryCardMatch.ts +414 -0
  35. package/admin/src/utils/mediaLibraryCardStore.ts +223 -0
  36. package/admin/src/utils/mediaLibraryQueryBridge.ts +113 -0
  37. package/admin/src/utils/mediaLibraryRoute.ts +9 -0
  38. package/admin/src/utils/optimizationFields.ts +17 -0
  39. package/admin/src/utils/probeVideoDimensions.ts +94 -0
  40. package/admin/src/utils/uploadAssetStore.ts +670 -0
  41. package/admin/tsconfig.json +8 -0
  42. package/dist/admin/SettingsPage-CN2fR83m.js +150 -0
  43. package/dist/admin/SettingsPage-D6e536P0.mjs +150 -0
  44. package/dist/admin/en-CqM903j3.js +77 -0
  45. package/dist/admin/en-CsHicGzL.mjs +77 -0
  46. package/dist/admin/index-BjWoS0YU.js +2542 -0
  47. package/dist/admin/index-Cs_uiChW.mjs +2541 -0
  48. package/dist/admin/index-DOuHOS2G.js +8799 -0
  49. package/dist/admin/index-rAmxCQz6.mjs +8781 -0
  50. package/dist/admin/index.js +4 -0
  51. package/dist/admin/index.mjs +4 -0
  52. package/dist/admin/tr-Y0-ANilh.mjs +77 -0
  53. package/dist/admin/tr-muzHkdC4.js +77 -0
  54. package/dist/server/index.js +1538 -0
  55. package/dist/server/index.mjs +1533 -0
  56. package/package.json +100 -0
  57. package/server/index.js +1 -0
  58. package/server/src/bootstrap.ts +377 -0
  59. package/server/src/buildVersion.ts +1 -0
  60. package/server/src/config/defaults.ts +91 -0
  61. package/server/src/config/index.ts +51 -0
  62. package/server/src/constants.ts +83 -0
  63. package/server/src/controllers/index.ts +7 -0
  64. package/server/src/controllers/job.ts +102 -0
  65. package/server/src/controllers/preference.ts +206 -0
  66. package/server/src/index.ts +15 -0
  67. package/server/src/register.ts +19 -0
  68. package/server/src/routes/index.ts +103 -0
  69. package/server/src/services/index.ts +9 -0
  70. package/server/src/services/job-queue.ts +663 -0
  71. package/server/src/services/optimizer.ts +284 -0
  72. package/server/src/services/preference.ts +172 -0
  73. package/server/src/utils/request-context.ts +7 -0
  74. package/server/src/utils/upload-preferences-context.ts +202 -0
  75. package/server/tsconfig.json +8 -0
  76. package/strapi-admin.js +7 -0
  77. package/strapi-server.js +7 -0
@@ -0,0 +1,1533 @@
1
+ import { AsyncLocalStorage } from "async_hooks";
2
+ import fs from "fs";
3
+ import { execSync } from "child_process";
4
+ import path, { join } from "path";
5
+ import { tmpdir } from "os";
6
+ import { randomUUID } from "crypto";
7
+ import ffmpeg from "fluent-ffmpeg";
8
+ import ffmpegStatic from "ffmpeg-static";
9
+ import { file } from "@strapi/utils";
10
+ const PLUGIN_ID = "video-optimizer";
11
+ const OPTIMIZATION_CHOICES = ["original", "global", "custom"];
12
+ const VIDEO_FORMATS = ["mp4", "webm"];
13
+ const VIDEO_CODECS = ["h264", "vp9"];
14
+ const AUDIO_MODES = ["keep", "remove", "compress"];
15
+ const FFMPEG_PRESETS = [
16
+ "ultrafast",
17
+ "superfast",
18
+ "veryfast",
19
+ "faster",
20
+ "fast",
21
+ "medium",
22
+ "slow",
23
+ "slower",
24
+ "veryslow"
25
+ ];
26
+ const MAX_CONCURRENT_JOBS_LIMIT = 32;
27
+ const MAX_FFMPEG_THREADS_LIMIT = 8;
28
+ const clampMaxConcurrentJobs = (value) => Math.min(MAX_CONCURRENT_JOBS_LIMIT, Math.max(1, Math.round(value)));
29
+ const clampMaxFfmpegThreads = (value) => Math.min(MAX_FFMPEG_THREADS_LIMIT, Math.max(1, Math.round(value)));
30
+ const isVideoMime = (mime) => typeof mime === "string" && mime.startsWith("video/");
31
+ const codecForFormat = (format) => format === "webm" ? "vp9" : "h264";
32
+ const formatForCodec = (codec) => codec === "vp9" ? "webm" : "mp4";
33
+ const register = async ({ strapi }) => {
34
+ await strapi.admin.services.permission.actionProvider.registerMany([
35
+ {
36
+ section: "plugins",
37
+ displayName: "Read Video Optimizer settings",
38
+ uid: "settings.read",
39
+ pluginName: PLUGIN_ID
40
+ },
41
+ {
42
+ section: "plugins",
43
+ displayName: "Update Video Optimizer settings",
44
+ uid: "settings.update",
45
+ pluginName: PLUGIN_ID
46
+ }
47
+ ]);
48
+ };
49
+ const SERVER_BUILD_MARKER = "test-8";
50
+ const uploadContext = new AsyncLocalStorage();
51
+ const optimizerUploadContext = new AsyncLocalStorage();
52
+ let fallbackContext = null;
53
+ let batchPreferences = null;
54
+ const registerUploadBatchPreferences = (preferences) => {
55
+ batchPreferences = preferences;
56
+ };
57
+ const clearUploadBatchPreferences = () => {
58
+ batchPreferences = null;
59
+ };
60
+ const normalizeFileName = (value) => value.trim().toLowerCase();
61
+ const resolveUploadBatchPreference = (fileName, assetId) => {
62
+ if (!batchPreferences?.length) {
63
+ return null;
64
+ }
65
+ const normalizedTarget = normalizeFileName(fileName);
66
+ const match = batchPreferences.find((entry) => {
67
+ if (assetId && entry.assetId === assetId) {
68
+ return true;
69
+ }
70
+ return normalizeFileName(entry.fileName) === normalizedTarget;
71
+ });
72
+ return match?.preference ?? null;
73
+ };
74
+ const createUploadContext = (preferences) => ({
75
+ preferences,
76
+ nextIndex: 0
77
+ });
78
+ const setFallbackUploadPreferences = (preferences) => {
79
+ fallbackContext = createUploadContext(preferences);
80
+ };
81
+ const clearFallbackUploadPreferences = () => {
82
+ fallbackContext = null;
83
+ };
84
+ const getActivePreferences = () => {
85
+ const store = optimizerUploadContext.getStore();
86
+ if (store?.preferences.length) {
87
+ return store;
88
+ }
89
+ return fallbackContext;
90
+ };
91
+ const parseUploadPreferences = (body) => {
92
+ const raw = body?.videoOptimizerPreferences;
93
+ if (!raw) {
94
+ return [];
95
+ }
96
+ if (typeof raw === "string") {
97
+ try {
98
+ const parsed = JSON.parse(raw);
99
+ return Array.isArray(parsed) ? parsed : [];
100
+ } catch {
101
+ return [];
102
+ }
103
+ }
104
+ return Array.isArray(raw) ? raw : [];
105
+ };
106
+ const consumeUploadPreference = (fileName, assetId) => {
107
+ const batchPreference = resolveUploadBatchPreference(fileName, assetId);
108
+ if (batchPreference) {
109
+ return batchPreference;
110
+ }
111
+ const store = getActivePreferences();
112
+ if (!store) {
113
+ return null;
114
+ }
115
+ const matched = store.preferences.find((entry) => {
116
+ if (assetId && entry.assetId === assetId) {
117
+ return true;
118
+ }
119
+ return normalizeFileName(entry.fileName) === normalizeFileName(fileName);
120
+ });
121
+ if (matched) {
122
+ return matched.preference;
123
+ }
124
+ const indexed = store.preferences[store.nextIndex];
125
+ if (indexed) {
126
+ store.nextIndex += 1;
127
+ return indexed.preference;
128
+ }
129
+ return null;
130
+ };
131
+ const UPLOAD_ROUTE_SUFFIXES = ["/upload", "/upload/unstable/upload-file"];
132
+ const isUploadRoute = (method, path2) => {
133
+ if (method.toUpperCase() !== "POST") {
134
+ return false;
135
+ }
136
+ const normalizedPath = path2.replace(/\/+$/, "") || "/";
137
+ return UPLOAD_ROUTE_SUFFIXES.some(
138
+ (suffix) => normalizedPath === suffix || normalizedPath.endsWith(suffix)
139
+ );
140
+ };
141
+ const pendingPreferencesByKey = /* @__PURE__ */ new Map();
142
+ const buildPreferenceKey = (fileName, assetId) => assetId ? `asset:${assetId}` : `file:${fileName}`;
143
+ const stashUploadPreference = (fileName, assetId, preference2) => {
144
+ pendingPreferencesByKey.set(buildPreferenceKey(fileName, assetId), preference2);
145
+ if (assetId) {
146
+ pendingPreferencesByKey.set(buildPreferenceKey(fileName), preference2);
147
+ }
148
+ pendingPreferencesByKey.set(`file:${fileName}`, preference2);
149
+ };
150
+ const takePendingPreference = (fileName, assetId) => {
151
+ const keys = [
152
+ assetId ? buildPreferenceKey(fileName, assetId) : null,
153
+ buildPreferenceKey(fileName),
154
+ `file:${fileName}`
155
+ ].filter(Boolean);
156
+ for (const key of keys) {
157
+ const preference2 = pendingPreferencesByKey.get(key);
158
+ if (preference2) {
159
+ pendingPreferencesByKey.delete(key);
160
+ return preference2;
161
+ }
162
+ }
163
+ return null;
164
+ };
165
+ const isValidChoice$4 = (choice) => typeof choice === "string" && ["original", "global", "custom"].includes(choice);
166
+ const isValidFormat$3 = (format) => typeof format === "string" && VIDEO_FORMATS.includes(format);
167
+ const isValidCodec$3 = (codec) => typeof codec === "string" && VIDEO_CODECS.includes(codec);
168
+ const isValidAudioMode$3 = (mode) => typeof mode === "string" && AUDIO_MODES.includes(mode);
169
+ const isValidPreset$3 = (preset) => typeof preset === "string" && FFMPEG_PRESETS.includes(preset);
170
+ const normalizeCustomSettings = (custom, global) => {
171
+ if (!custom || typeof custom !== "object") {
172
+ return void 0;
173
+ }
174
+ const value = custom;
175
+ if (!isValidFormat$3(value.defaultFormat) && !isValidCodec$3(value.videoCodec)) {
176
+ return void 0;
177
+ }
178
+ const defaultFormat = isValidFormat$3(value.defaultFormat) ? value.defaultFormat : isValidCodec$3(value.videoCodec) ? formatForCodec(value.videoCodec) : global.defaultFormat;
179
+ return {
180
+ defaultFormat,
181
+ videoCodec: isValidCodec$3(value.videoCodec) ? value.videoCodec : codecForFormat(defaultFormat),
182
+ crf: typeof value.crf === "number" ? value.crf : Number(value.crf ?? global.crf),
183
+ preset: isValidPreset$3(value.preset) ? value.preset : global.preset,
184
+ maxWidth: typeof value.maxWidth === "number" && value.maxWidth >= 1 ? value.maxWidth : global.maxWidth,
185
+ maxHeight: typeof value.maxHeight === "number" && value.maxHeight >= 1 ? value.maxHeight : global.maxHeight,
186
+ audioMode: isValidAudioMode$3(value.audioMode) ? value.audioMode : global.audioMode,
187
+ audioBitrate: typeof value.audioBitrate === "string" ? value.audioBitrate : String(value.audioBitrate ?? global.audioBitrate)
188
+ };
189
+ };
190
+ const configToSettings = (config2) => ({
191
+ defaultFormat: config2.defaultFormat,
192
+ videoCodec: config2.videoCodec,
193
+ crf: config2.crf,
194
+ preset: config2.preset,
195
+ maxWidth: config2.maxWidth,
196
+ maxHeight: config2.maxHeight,
197
+ audioMode: config2.audioMode,
198
+ audioBitrate: config2.audioBitrate
199
+ });
200
+ const runWithUser = (userId, fn) => {
201
+ if (userId === void 0) {
202
+ return fn();
203
+ }
204
+ return uploadContext.run({ userId }, fn);
205
+ };
206
+ const applyPreferenceToEntity = (entity, fileInfo, mime, globalDefaults) => {
207
+ if (!isVideoMime(mime)) {
208
+ const batchPreference = resolveUploadBatchPreference(
209
+ String(entity.name ?? fileInfo.name ?? ""),
210
+ typeof fileInfo.optimizerAssetId === "string" ? fileInfo.optimizerAssetId : void 0
211
+ );
212
+ if (!batchPreference) {
213
+ return;
214
+ }
215
+ }
216
+ const fileName = String(entity.name ?? fileInfo.name ?? "");
217
+ const assetId = typeof fileInfo.optimizerAssetId === "string" ? fileInfo.optimizerAssetId : void 0;
218
+ const preference2 = consumeUploadPreference(fileName, assetId);
219
+ const applyPreference = (choice, custom) => {
220
+ entity.optimizationChoice = choice;
221
+ const normalizedCustom = choice === "custom" ? normalizeCustomSettings(custom, globalDefaults) : void 0;
222
+ if (normalizedCustom) {
223
+ entity.optimizationCustom = normalizedCustom;
224
+ } else {
225
+ delete entity.optimizationCustom;
226
+ }
227
+ if (assetId) {
228
+ entity.optimizerAssetId = assetId;
229
+ }
230
+ stashUploadPreference(fileName, assetId, { choice, custom: normalizedCustom });
231
+ };
232
+ if (preference2) {
233
+ applyPreference(preference2.choice, preference2.custom);
234
+ return;
235
+ }
236
+ if (fileInfo.optimizationChoice && isValidChoice$4(fileInfo.optimizationChoice)) {
237
+ applyPreference(fileInfo.optimizationChoice, fileInfo.optimizationCustom);
238
+ }
239
+ };
240
+ const enqueueVideoJobs = async (strapi, files, globalDefaults) => {
241
+ const preferenceService = strapi.plugin(PLUGIN_ID).service("preference");
242
+ const jobQueue2 = strapi.plugin(PLUGIN_ID).service("job-queue");
243
+ try {
244
+ for (const file2 of files) {
245
+ if (!isVideoMime(file2.mime)) {
246
+ continue;
247
+ }
248
+ const fileName = String(file2.name ?? "");
249
+ const assetId = typeof file2.optimizerAssetId === "string" ? file2.optimizerAssetId : void 0;
250
+ const batchPreference = resolveUploadBatchPreference(fileName, assetId);
251
+ const pending = takePendingPreference(fileName, assetId);
252
+ const optimizationChoice = batchPreference?.choice ?? pending?.choice ?? (isValidChoice$4(file2.optimizationChoice) ? file2.optimizationChoice : void 0);
253
+ const optimizationCustom = (batchPreference?.choice === "custom" ? normalizeCustomSettings(batchPreference.custom, globalDefaults) : void 0) ?? pending?.custom ?? file2.optimizationCustom;
254
+ const resolved = await preferenceService.resolveOptimization({
255
+ optimizationChoice,
256
+ optimizationCustom,
257
+ mime: file2.mime
258
+ });
259
+ if (resolved.skip || !resolved.settings || !file2.id) {
260
+ strapi.log.debug(
261
+ `[video-optimizer] Skipping file ${file2.id} (${fileName}): choice=${optimizationChoice ?? "default"}, skip=${resolved.skip}`
262
+ );
263
+ continue;
264
+ }
265
+ strapi.log.info(
266
+ `[video-optimizer] Queueing optimization for file ${file2.id} (${fileName}) choice=${optimizationChoice ?? "default"} → ${resolved.settings.defaultFormat}`
267
+ );
268
+ await jobQueue2.enqueue({
269
+ fileId: Number(file2.id),
270
+ settings: resolved.settings,
271
+ choice: optimizationChoice
272
+ });
273
+ }
274
+ } finally {
275
+ clearUploadBatchPreferences();
276
+ }
277
+ };
278
+ const wrapUploadController = (strapi, handler) => {
279
+ return async (ctx) => {
280
+ const preferences = parseUploadPreferences(ctx.request.body);
281
+ if (preferences.length === 0) {
282
+ return handler(ctx);
283
+ }
284
+ registerUploadBatchPreferences(preferences);
285
+ strapi.log.info(
286
+ `[video-optimizer] Controller preferences (${preferences.length}, ${SERVER_BUILD_MARKER}): ${preferences.map((entry) => {
287
+ const format = entry.preference.choice === "custom" ? entry.preference.custom?.defaultFormat ?? "?" : entry.preference.choice;
288
+ return `${entry.fileName}=${format}`;
289
+ }).join(", ")}`
290
+ );
291
+ return optimizerUploadContext.run(createUploadContext(preferences), () => handler(ctx));
292
+ };
293
+ };
294
+ const bootstrap = async ({ strapi }) => {
295
+ const uploadService = strapi.plugin("upload").service("upload");
296
+ const globalDefaults = configToSettings(strapi.plugin(PLUGIN_ID).config);
297
+ strapi.log.info(`[video-optimizer] Server bootstrap loaded (${SERVER_BUILD_MARKER})`);
298
+ const adminUploadController = strapi.plugin("upload").controllers["admin-upload"];
299
+ if (adminUploadController?.uploadFiles) {
300
+ const originalUploadFiles = adminUploadController.uploadFiles.bind(adminUploadController);
301
+ adminUploadController.uploadFiles = wrapUploadController(strapi, originalUploadFiles);
302
+ }
303
+ if (adminUploadController?.unstable_uploadFile) {
304
+ const originalUnstableUpload = adminUploadController.unstable_uploadFile.bind(adminUploadController);
305
+ adminUploadController.unstable_uploadFile = wrapUploadController(strapi, originalUnstableUpload);
306
+ }
307
+ strapi.server.use(async (ctx, next) => {
308
+ if (!isUploadRoute(ctx.method, ctx.path)) {
309
+ return next();
310
+ }
311
+ const preferences = parseUploadPreferences(ctx.request.body);
312
+ if (preferences.length === 0) {
313
+ return next();
314
+ }
315
+ registerUploadBatchPreferences(preferences);
316
+ strapi.log.debug(
317
+ `[video-optimizer] Upload preferences received (${preferences.length} file(s)) on ${ctx.path}: ${preferences.map((entry) => {
318
+ const format = entry.preference.choice === "custom" ? entry.preference.custom?.defaultFormat ?? "?" : entry.preference.choice;
319
+ return `${entry.fileName}=${format}`;
320
+ }).join(", ")}`
321
+ );
322
+ setFallbackUploadPreferences(preferences);
323
+ try {
324
+ return await optimizerUploadContext.run(createUploadContext(preferences), () => next());
325
+ } finally {
326
+ clearFallbackUploadPreferences();
327
+ }
328
+ });
329
+ const originalFormatFileInfo = uploadService.formatFileInfo.bind(uploadService);
330
+ uploadService.formatFileInfo = async (fileProps, fileInfo = {}, metas = {}) => {
331
+ const entity = await originalFormatFileInfo(fileProps, fileInfo, metas);
332
+ applyPreferenceToEntity(entity, fileInfo, fileProps.type, globalDefaults);
333
+ return entity;
334
+ };
335
+ const originalUpload = uploadService.upload.bind(uploadService);
336
+ uploadService.upload = async (args, opts) => {
337
+ const result = await runWithUser(opts?.user?.id, () => originalUpload(args, opts));
338
+ const files = Array.isArray(result) ? result : [result];
339
+ if (files.length) {
340
+ setImmediate(() => {
341
+ void enqueueVideoJobs(strapi, files, globalDefaults).catch(
342
+ (error) => {
343
+ const message = error instanceof Error ? error.message : String(error);
344
+ strapi.log.error(`[video-optimizer] Failed to enqueue upload jobs: ${message}`);
345
+ }
346
+ );
347
+ });
348
+ }
349
+ return result;
350
+ };
351
+ const originalReplace = uploadService.replace.bind(uploadService);
352
+ uploadService.replace = async (id, args, opts) => {
353
+ const result = await runWithUser(opts?.user?.id, () => originalReplace(id, args, opts));
354
+ const files = Array.isArray(result) ? result : [result];
355
+ if (files.length) {
356
+ setImmediate(() => {
357
+ void enqueueVideoJobs(strapi, files, globalDefaults).catch(
358
+ (error) => {
359
+ const message = error instanceof Error ? error.message : String(error);
360
+ strapi.log.error(`[video-optimizer] Failed to enqueue replace jobs: ${message}`);
361
+ }
362
+ );
363
+ });
364
+ }
365
+ return result;
366
+ };
367
+ const jobQueue2 = strapi.plugin(PLUGIN_ID).service("job-queue");
368
+ const originalRemove = uploadService.remove.bind(uploadService);
369
+ uploadService.remove = async (file2) => {
370
+ await jobQueue2.cancelJobsForFile(Number(file2.id));
371
+ return originalRemove(file2);
372
+ };
373
+ strapi.eventHub.on("media.delete", ({ media }) => {
374
+ const fileId = Number(media?.id);
375
+ if (!Number.isFinite(fileId) || fileId <= 0) {
376
+ return;
377
+ }
378
+ void jobQueue2.cancelJobsForFile(fileId);
379
+ });
380
+ await strapi.plugin(PLUGIN_ID).service("preference").ensureGlobalSettingsDefaults();
381
+ await jobQueue2.clearJobsOnStartup();
382
+ };
383
+ const DEFAULT_PLUGIN_CONFIG = {
384
+ defaultChoice: "original",
385
+ defaultFormat: "mp4",
386
+ videoCodec: "h264",
387
+ crf: 23,
388
+ preset: "medium",
389
+ maxWidth: 1920,
390
+ maxHeight: 1080,
391
+ audioMode: "compress",
392
+ audioBitrate: "128k",
393
+ maxConcurrentJobs: 1,
394
+ maxFfmpegThreads: 2
395
+ };
396
+ const isValidChoice$3 = (choice) => typeof choice === "string" && OPTIMIZATION_CHOICES.includes(choice);
397
+ const isValidFormat$2 = (format) => typeof format === "string" && VIDEO_FORMATS.includes(format);
398
+ const isValidCodec$2 = (codec) => typeof codec === "string" && VIDEO_CODECS.includes(codec);
399
+ const isValidAudioMode$2 = (mode) => typeof mode === "string" && AUDIO_MODES.includes(mode);
400
+ const isValidPreset$2 = (preset) => typeof preset === "string" && FFMPEG_PRESETS.includes(preset);
401
+ const normalizePluginConfig = (...sources) => {
402
+ const merged = Object.assign({}, DEFAULT_PLUGIN_CONFIG, ...sources.filter(Boolean));
403
+ const defaultFormat = isValidFormat$2(merged.defaultFormat) ? merged.defaultFormat : DEFAULT_PLUGIN_CONFIG.defaultFormat;
404
+ return {
405
+ defaultChoice: isValidChoice$3(merged.defaultChoice) ? merged.defaultChoice : DEFAULT_PLUGIN_CONFIG.defaultChoice,
406
+ defaultFormat,
407
+ videoCodec: isValidCodec$2(merged.videoCodec) ? merged.videoCodec : codecForFormat(defaultFormat),
408
+ crf: typeof merged.crf === "number" && merged.crf >= 0 && merged.crf <= 51 ? merged.crf : DEFAULT_PLUGIN_CONFIG.crf,
409
+ preset: isValidPreset$2(merged.preset) ? merged.preset : DEFAULT_PLUGIN_CONFIG.preset,
410
+ maxWidth: typeof merged.maxWidth === "number" && merged.maxWidth >= 1 ? merged.maxWidth : DEFAULT_PLUGIN_CONFIG.maxWidth,
411
+ maxHeight: typeof merged.maxHeight === "number" && merged.maxHeight >= 1 ? merged.maxHeight : DEFAULT_PLUGIN_CONFIG.maxHeight,
412
+ audioMode: isValidAudioMode$2(merged.audioMode) ? merged.audioMode : DEFAULT_PLUGIN_CONFIG.audioMode,
413
+ audioBitrate: typeof merged.audioBitrate === "string" && merged.audioBitrate.trim() ? merged.audioBitrate : DEFAULT_PLUGIN_CONFIG.audioBitrate,
414
+ maxConcurrentJobs: clampMaxConcurrentJobs(
415
+ merged.maxConcurrentJobs ?? DEFAULT_PLUGIN_CONFIG.maxConcurrentJobs
416
+ ),
417
+ maxFfmpegThreads: clampMaxFfmpegThreads(
418
+ merged.maxFfmpegThreads ?? DEFAULT_PLUGIN_CONFIG.maxFfmpegThreads
419
+ )
420
+ };
421
+ };
422
+ const config = {
423
+ default: () => ({ ...DEFAULT_PLUGIN_CONFIG }),
424
+ validator(config2) {
425
+ if (config2.crf !== void 0 && (config2.crf < 0 || config2.crf > 51)) {
426
+ throw new Error("crf must be between 0 and 51");
427
+ }
428
+ if (config2.maxWidth !== void 0 && config2.maxWidth < 1) {
429
+ throw new Error("maxWidth must be at least 1");
430
+ }
431
+ if (config2.maxHeight !== void 0 && config2.maxHeight < 1) {
432
+ throw new Error("maxHeight must be at least 1");
433
+ }
434
+ if (config2.maxConcurrentJobs !== void 0 && clampMaxConcurrentJobs(config2.maxConcurrentJobs) !== config2.maxConcurrentJobs) {
435
+ throw new Error(`maxConcurrentJobs must be between 1 and ${MAX_CONCURRENT_JOBS_LIMIT}`);
436
+ }
437
+ if (config2.maxFfmpegThreads !== void 0 && clampMaxFfmpegThreads(config2.maxFfmpegThreads) !== config2.maxFfmpegThreads) {
438
+ throw new Error(`maxFfmpegThreads must be between 1 and ${MAX_FFMPEG_THREADS_LIMIT}`);
439
+ }
440
+ }
441
+ };
442
+ const isValidChoice$2 = (choice) => typeof choice === "string" && OPTIMIZATION_CHOICES.includes(choice);
443
+ const isValidFormat$1 = (format) => typeof format === "string" && VIDEO_FORMATS.includes(format);
444
+ const isValidCodec$1 = (codec) => typeof codec === "string" && VIDEO_CODECS.includes(codec);
445
+ const isValidAudioMode$1 = (mode) => typeof mode === "string" && AUDIO_MODES.includes(mode);
446
+ const isValidPreset$1 = (preset) => typeof preset === "string" && FFMPEG_PRESETS.includes(preset);
447
+ const preference$1 = ({ strapi }) => ({
448
+ async getDefaultMode(ctx) {
449
+ const settings = await strapi.plugin("video-optimizer").service("preference").getGlobalSettings();
450
+ ctx.body = {
451
+ defaultChoice: settings.defaultChoice,
452
+ defaultFormat: settings.defaultFormat,
453
+ videoCodec: settings.videoCodec,
454
+ crf: settings.crf,
455
+ preset: settings.preset,
456
+ maxWidth: settings.maxWidth,
457
+ maxHeight: settings.maxHeight,
458
+ audioMode: settings.audioMode,
459
+ audioBitrate: settings.audioBitrate,
460
+ maxConcurrentJobs: settings.maxConcurrentJobs,
461
+ maxFfmpegThreads: settings.maxFfmpegThreads
462
+ };
463
+ },
464
+ async getPreference(ctx) {
465
+ const userId = ctx.state.user?.id;
466
+ if (!userId) {
467
+ return ctx.unauthorized();
468
+ }
469
+ const preferenceService = strapi.plugin("video-optimizer").service("preference");
470
+ const defaultFormat = await preferenceService.getUserPreference(userId);
471
+ const settings = await preferenceService.getGlobalSettings();
472
+ ctx.body = {
473
+ defaultFormat: defaultFormat ?? settings.defaultFormat,
474
+ defaultChoice: settings.defaultChoice
475
+ };
476
+ },
477
+ async updatePreference(ctx) {
478
+ const userId = ctx.state.user?.id;
479
+ if (!userId) {
480
+ return ctx.unauthorized();
481
+ }
482
+ const { defaultFormat } = ctx.request.body ?? {};
483
+ if (!isValidFormat$1(defaultFormat)) {
484
+ return ctx.badRequest("Invalid video format");
485
+ }
486
+ await strapi.plugin("video-optimizer").service("preference").setUserPreference(userId, defaultFormat);
487
+ ctx.body = { defaultFormat };
488
+ },
489
+ async getSettings(ctx) {
490
+ const settings = await strapi.plugin("video-optimizer").service("preference").getGlobalSettings();
491
+ ctx.body = settings;
492
+ },
493
+ async updateSettings(ctx) {
494
+ const {
495
+ defaultChoice,
496
+ defaultFormat,
497
+ videoCodec,
498
+ crf,
499
+ preset,
500
+ maxWidth,
501
+ maxHeight,
502
+ audioMode,
503
+ audioBitrate,
504
+ maxConcurrentJobs,
505
+ maxFfmpegThreads
506
+ } = ctx.request.body ?? {};
507
+ if (defaultChoice !== void 0 && !isValidChoice$2(defaultChoice)) {
508
+ return ctx.badRequest("Invalid default choice");
509
+ }
510
+ if (defaultFormat !== void 0 && !isValidFormat$1(defaultFormat)) {
511
+ return ctx.badRequest("Invalid default format");
512
+ }
513
+ if (videoCodec !== void 0 && !isValidCodec$1(videoCodec)) {
514
+ return ctx.badRequest("Invalid video codec");
515
+ }
516
+ if (crf !== void 0 && (crf < 0 || crf > 51)) {
517
+ return ctx.badRequest("Invalid CRF value");
518
+ }
519
+ if (preset !== void 0 && !isValidPreset$1(preset)) {
520
+ return ctx.badRequest("Invalid preset");
521
+ }
522
+ if (maxWidth !== void 0 && maxWidth < 1) {
523
+ return ctx.badRequest("Invalid max width");
524
+ }
525
+ if (maxHeight !== void 0 && maxHeight < 1) {
526
+ return ctx.badRequest("Invalid max height");
527
+ }
528
+ if (audioMode !== void 0 && !isValidAudioMode$1(audioMode)) {
529
+ return ctx.badRequest("Invalid audio mode");
530
+ }
531
+ if (maxConcurrentJobs !== void 0 && clampMaxConcurrentJobs(Number(maxConcurrentJobs)) !== Number(maxConcurrentJobs)) {
532
+ return ctx.badRequest(`maxConcurrentJobs must be between 1 and ${MAX_CONCURRENT_JOBS_LIMIT}`);
533
+ }
534
+ if (maxFfmpegThreads !== void 0 && clampMaxFfmpegThreads(Number(maxFfmpegThreads)) !== Number(maxFfmpegThreads)) {
535
+ return ctx.badRequest(`maxFfmpegThreads must be between 1 and ${MAX_FFMPEG_THREADS_LIMIT}`);
536
+ }
537
+ const payload = {};
538
+ if (defaultChoice !== void 0) payload.defaultChoice = defaultChoice;
539
+ if (defaultFormat !== void 0) {
540
+ payload.defaultFormat = defaultFormat;
541
+ if (videoCodec === void 0) {
542
+ payload.videoCodec = codecForFormat(defaultFormat);
543
+ }
544
+ }
545
+ if (videoCodec !== void 0) payload.videoCodec = videoCodec;
546
+ if (crf !== void 0) payload.crf = Number(crf);
547
+ if (preset !== void 0) payload.preset = preset;
548
+ if (maxWidth !== void 0) payload.maxWidth = Number(maxWidth);
549
+ if (maxHeight !== void 0) payload.maxHeight = Number(maxHeight);
550
+ if (audioMode !== void 0) payload.audioMode = audioMode;
551
+ if (audioBitrate !== void 0) payload.audioBitrate = String(audioBitrate);
552
+ if (maxConcurrentJobs !== void 0) payload.maxConcurrentJobs = Number(maxConcurrentJobs);
553
+ if (maxFfmpegThreads !== void 0) payload.maxFfmpegThreads = Number(maxFfmpegThreads);
554
+ await strapi.plugin("video-optimizer").service("preference").setGlobalSettings(payload);
555
+ if (maxConcurrentJobs !== void 0) {
556
+ void strapi.plugin("video-optimizer").service("job-queue").drainQueue();
557
+ }
558
+ const settings = await strapi.plugin("video-optimizer").service("preference").getGlobalSettings();
559
+ ctx.body = settings;
560
+ }
561
+ });
562
+ const isValidChoice$1 = (choice) => typeof choice === "string" && OPTIMIZATION_CHOICES.includes(choice);
563
+ const job = ({ strapi }) => ({
564
+ async getJob(ctx) {
565
+ const { id } = ctx.params;
566
+ const job2 = await strapi.plugin("video-optimizer").service("job-queue").getJob(id);
567
+ if (!job2) {
568
+ return ctx.notFound("Job not found");
569
+ }
570
+ ctx.body = job2;
571
+ },
572
+ async getJobsByFiles(ctx) {
573
+ const raw = ctx.query.fileIds;
574
+ if (!raw || typeof raw !== "string") {
575
+ return ctx.badRequest("fileIds query parameter is required");
576
+ }
577
+ const fileIds = raw.split(",").map((value) => Number(value.trim())).filter((value) => Number.isFinite(value) && value > 0);
578
+ const jobs = await strapi.plugin("video-optimizer").service("job-queue").getJobsByFileIds(fileIds);
579
+ ctx.body = { jobs };
580
+ },
581
+ async listActive(ctx) {
582
+ const jobs = await strapi.plugin("video-optimizer").service("job-queue").listActiveJobs();
583
+ ctx.body = { jobs };
584
+ },
585
+ async enqueueFile(ctx) {
586
+ const body = ctx.request.body;
587
+ const fileId = Number(body.fileId);
588
+ if (!Number.isFinite(fileId) || fileId <= 0) {
589
+ return ctx.badRequest("fileId is required");
590
+ }
591
+ const file2 = await strapi.db.query("plugin::upload.file").findOne({
592
+ where: { id: fileId }
593
+ });
594
+ if (!file2) {
595
+ return ctx.notFound("File not found");
596
+ }
597
+ if (!file2.mime || !String(file2.mime).startsWith("video/")) {
598
+ return ctx.badRequest("Only video files can be optimized");
599
+ }
600
+ const preferenceService = strapi.plugin("video-optimizer").service("preference");
601
+ const resolved = await preferenceService.resolveOptimization({
602
+ optimizationChoice: isValidChoice$1(body.optimizationChoice) ? body.optimizationChoice : void 0,
603
+ optimizationCustom: body.optimizationCustom,
604
+ mime: String(file2.mime)
605
+ });
606
+ if (resolved.skip || !resolved.settings) {
607
+ return ctx.badRequest("Optimization is disabled for the selected choice");
608
+ }
609
+ const job2 = await strapi.plugin("video-optimizer").service("job-queue").enqueue({
610
+ fileId,
611
+ settings: resolved.settings,
612
+ choice: isValidChoice$1(body.optimizationChoice) ? body.optimizationChoice : void 0
613
+ });
614
+ ctx.body = { job: job2 };
615
+ },
616
+ async cancelForFile(ctx) {
617
+ const body = ctx.request.body;
618
+ const fileId = Number(body.fileId);
619
+ if (!Number.isFinite(fileId) || fileId <= 0) {
620
+ return ctx.badRequest("fileId is required");
621
+ }
622
+ await strapi.plugin("video-optimizer").service("job-queue").cancelJobsForFile(fileId);
623
+ ctx.body = { ok: true };
624
+ }
625
+ });
626
+ const controllers = {
627
+ preference: preference$1,
628
+ job
629
+ };
630
+ const routes = {
631
+ admin: {
632
+ type: "admin",
633
+ routes: [
634
+ {
635
+ method: "GET",
636
+ path: "/default-mode",
637
+ handler: "preference.getDefaultMode",
638
+ config: {
639
+ policies: ["admin::isAuthenticatedAdmin"]
640
+ }
641
+ },
642
+ {
643
+ method: "GET",
644
+ path: "/preference",
645
+ handler: "preference.getPreference",
646
+ config: {
647
+ policies: ["admin::isAuthenticatedAdmin"]
648
+ }
649
+ },
650
+ {
651
+ method: "PUT",
652
+ path: "/preference",
653
+ handler: "preference.updatePreference",
654
+ config: {
655
+ policies: ["admin::isAuthenticatedAdmin"]
656
+ }
657
+ },
658
+ {
659
+ method: "GET",
660
+ path: "/settings",
661
+ handler: "preference.getSettings",
662
+ config: {
663
+ policies: [
664
+ "admin::isAuthenticatedAdmin",
665
+ {
666
+ name: "admin::hasPermissions",
667
+ config: {
668
+ actions: ["plugin::video-optimizer.settings.read"]
669
+ }
670
+ }
671
+ ]
672
+ }
673
+ },
674
+ {
675
+ method: "PUT",
676
+ path: "/settings",
677
+ handler: "preference.updateSettings",
678
+ config: {
679
+ policies: [
680
+ "admin::isAuthenticatedAdmin",
681
+ {
682
+ name: "admin::hasPermissions",
683
+ config: {
684
+ actions: ["plugin::video-optimizer.settings.update"]
685
+ }
686
+ }
687
+ ]
688
+ }
689
+ },
690
+ {
691
+ method: "GET",
692
+ path: "/jobs/active",
693
+ handler: "job.listActive",
694
+ config: {
695
+ policies: ["admin::isAuthenticatedAdmin"]
696
+ }
697
+ },
698
+ {
699
+ method: "GET",
700
+ path: "/jobs/by-files",
701
+ handler: "job.getJobsByFiles",
702
+ config: {
703
+ policies: ["admin::isAuthenticatedAdmin"]
704
+ }
705
+ },
706
+ {
707
+ method: "POST",
708
+ path: "/jobs/enqueue",
709
+ handler: "job.enqueueFile",
710
+ config: {
711
+ policies: ["admin::isAuthenticatedAdmin"]
712
+ }
713
+ },
714
+ {
715
+ method: "POST",
716
+ path: "/jobs/cancel",
717
+ handler: "job.cancelForFile",
718
+ config: {
719
+ policies: ["admin::isAuthenticatedAdmin"]
720
+ }
721
+ },
722
+ {
723
+ method: "GET",
724
+ path: "/jobs/:id",
725
+ handler: "job.getJob",
726
+ config: {
727
+ policies: ["admin::isAuthenticatedAdmin"]
728
+ }
729
+ }
730
+ ]
731
+ }
732
+ };
733
+ const GLOBAL_SETTINGS_KEY = "global-settings";
734
+ const JOBS_STORE_KEY = "jobs";
735
+ const userPreferenceKey = (userId) => `user-pref-${userId}`;
736
+ const isValidChoice = (choice) => typeof choice === "string" && OPTIMIZATION_CHOICES.includes(choice);
737
+ const isValidFormat = (format) => typeof format === "string" && VIDEO_FORMATS.includes(format);
738
+ const isValidCodec = (codec) => typeof codec === "string" && VIDEO_CODECS.includes(codec);
739
+ const isValidAudioMode = (mode) => typeof mode === "string" && AUDIO_MODES.includes(mode);
740
+ const isValidPreset = (preset) => typeof preset === "string" && FFMPEG_PRESETS.includes(preset);
741
+ const buildSettings = (source) => {
742
+ const defaultFormat = isValidFormat(source.defaultFormat) ? source.defaultFormat : "mp4";
743
+ const videoCodec = isValidCodec(source.videoCodec) ? source.videoCodec : codecForFormat(defaultFormat);
744
+ return {
745
+ defaultFormat,
746
+ videoCodec,
747
+ crf: source.crf ?? 23,
748
+ preset: isValidPreset(source.preset) ? source.preset : "medium",
749
+ maxWidth: source.maxWidth ?? 1920,
750
+ maxHeight: source.maxHeight ?? 1080,
751
+ audioMode: isValidAudioMode(source.audioMode) ? source.audioMode : "compress",
752
+ audioBitrate: source.audioBitrate ?? "128k"
753
+ };
754
+ };
755
+ const preference = ({ strapi }) => ({
756
+ async getUserPreference(userId) {
757
+ const stored = await strapi.store({
758
+ type: "plugin",
759
+ name: PLUGIN_ID,
760
+ key: userPreferenceKey(userId)
761
+ }).get();
762
+ if (stored?.defaultFormat && isValidFormat(stored.defaultFormat)) {
763
+ return stored.defaultFormat;
764
+ }
765
+ return null;
766
+ },
767
+ async setUserPreference(userId, defaultFormat) {
768
+ await strapi.store({
769
+ type: "plugin",
770
+ name: PLUGIN_ID,
771
+ key: userPreferenceKey(userId)
772
+ }).set({ value: { defaultFormat } });
773
+ },
774
+ async getGlobalSettings() {
775
+ const pluginConfig = strapi.plugin(PLUGIN_ID).config;
776
+ const stored = await strapi.store({
777
+ type: "plugin",
778
+ name: PLUGIN_ID,
779
+ key: GLOBAL_SETTINGS_KEY
780
+ }).get();
781
+ return normalizePluginConfig(DEFAULT_PLUGIN_CONFIG, pluginConfig, stored ?? void 0);
782
+ },
783
+ async ensureGlobalSettingsDefaults() {
784
+ const store = strapi.store({
785
+ type: "plugin",
786
+ name: PLUGIN_ID,
787
+ key: GLOBAL_SETTINGS_KEY
788
+ });
789
+ const stored = await store.get();
790
+ if (!stored || Object.keys(stored).length === 0) {
791
+ const pluginConfig = strapi.plugin(PLUGIN_ID).config;
792
+ const defaults = normalizePluginConfig(DEFAULT_PLUGIN_CONFIG, pluginConfig);
793
+ await store.set({ value: defaults });
794
+ }
795
+ },
796
+ async setGlobalSettings(settings) {
797
+ const current = await this.getGlobalSettings();
798
+ const next = { ...current, ...settings };
799
+ await strapi.store({
800
+ type: "plugin",
801
+ name: PLUGIN_ID,
802
+ key: GLOBAL_SETTINGS_KEY
803
+ }).set({ value: next });
804
+ },
805
+ async resolveOptimization(file2) {
806
+ const global = await this.getGlobalSettings();
807
+ if (!file2.mime || !file2.mime.startsWith("video/")) {
808
+ return { skip: true };
809
+ }
810
+ const choice = isValidChoice(file2.optimizationChoice) ? file2.optimizationChoice : global.defaultChoice;
811
+ if (choice === "original") {
812
+ return { skip: true };
813
+ }
814
+ if (choice === "global") {
815
+ return {
816
+ skip: false,
817
+ settings: buildSettings(global)
818
+ };
819
+ }
820
+ const custom = file2.optimizationCustom ?? {};
821
+ return {
822
+ skip: false,
823
+ settings: buildSettings({
824
+ defaultFormat: custom.defaultFormat ?? global.defaultFormat,
825
+ videoCodec: custom.videoCodec ?? global.videoCodec,
826
+ crf: custom.crf ?? global.crf,
827
+ preset: custom.preset ?? global.preset,
828
+ maxWidth: custom.maxWidth ?? global.maxWidth,
829
+ maxHeight: custom.maxHeight ?? global.maxHeight,
830
+ audioMode: custom.audioMode ?? global.audioMode,
831
+ audioBitrate: custom.audioBitrate ?? global.audioBitrate
832
+ })
833
+ };
834
+ },
835
+ getJobsStore() {
836
+ return strapi.store({
837
+ type: "plugin",
838
+ name: PLUGIN_ID,
839
+ key: JOBS_STORE_KEY
840
+ });
841
+ }
842
+ });
843
+ const { bytesToKbytes } = file;
844
+ let ffmpegPathConfigured = false;
845
+ const activeCommands = /* @__PURE__ */ new Map();
846
+ const configureFfmpegPath = () => {
847
+ if (ffmpegPathConfigured) {
848
+ return;
849
+ }
850
+ if (ffmpegStatic) {
851
+ ffmpeg.setFfmpegPath(ffmpegStatic);
852
+ ffmpegPathConfigured = true;
853
+ return;
854
+ }
855
+ try {
856
+ const systemPath = execSync("which ffmpeg", { encoding: "utf8" }).trim();
857
+ if (systemPath) {
858
+ ffmpeg.setFfmpegPath(systemPath);
859
+ ffmpegPathConfigured = true;
860
+ }
861
+ } catch {
862
+ }
863
+ };
864
+ const probeVideo = (inputPath) => new Promise((resolve, reject) => {
865
+ ffmpeg.ffprobe(inputPath, (error, metadata) => {
866
+ if (error) {
867
+ reject(error);
868
+ return;
869
+ }
870
+ const videoStream = metadata.streams.find((stream) => stream.codec_type === "video");
871
+ resolve({
872
+ width: videoStream?.width ?? void 0,
873
+ height: videoStream?.height ?? void 0,
874
+ duration: metadata.format.duration ?? void 0
875
+ });
876
+ });
877
+ });
878
+ const buildScaleFilter = (settings, metadata, resizeMode = "exact") => {
879
+ const targetWidth = settings.maxWidth;
880
+ const targetHeight = settings.maxHeight;
881
+ const sourceWidth = metadata.width ?? 0;
882
+ const sourceHeight = metadata.height ?? 0;
883
+ if (!targetWidth || !targetHeight || !sourceWidth || !sourceHeight) {
884
+ return null;
885
+ }
886
+ if (targetWidth === sourceWidth && targetHeight === sourceHeight) {
887
+ return null;
888
+ }
889
+ if (resizeMode === "fit-within") {
890
+ if (sourceWidth <= targetWidth && sourceHeight <= targetHeight) {
891
+ return null;
892
+ }
893
+ return `scale='min(${targetWidth},iw)':'min(${targetHeight},ih)':force_original_aspect_ratio=decrease`;
894
+ }
895
+ return `scale=${targetWidth}:${targetHeight}`;
896
+ };
897
+ const buildVideoCodecOptions = (settings, maxThreads) => {
898
+ if (settings.defaultFormat === "webm" || settings.videoCodec === "vp9") {
899
+ return {
900
+ videoCodec: "libvpx-vp9",
901
+ outputOptions: [
902
+ "-crf",
903
+ String(settings.crf),
904
+ "-b:v",
905
+ "0",
906
+ "-row-mt",
907
+ maxThreads > 1 ? "1" : "0",
908
+ "-speed",
909
+ "4"
910
+ ]
911
+ };
912
+ }
913
+ return {
914
+ videoCodec: "libx264",
915
+ outputOptions: [`-crf`, String(settings.crf), "-preset", settings.preset]
916
+ };
917
+ };
918
+ const buildThreadOptions = (maxThreads) => {
919
+ const threads = clampMaxFfmpegThreads(maxThreads);
920
+ return ["-threads", String(threads)];
921
+ };
922
+ const buildAudioOptions = (settings) => {
923
+ if (settings.audioMode === "remove") {
924
+ return { audioCodec: void 0, outputOptions: ["-an"] };
925
+ }
926
+ if (settings.audioMode === "compress") {
927
+ const codec = settings.defaultFormat === "webm" ? "libopus" : "aac";
928
+ return {
929
+ audioCodec: codec,
930
+ outputOptions: ["-b:a", settings.audioBitrate]
931
+ };
932
+ }
933
+ return { audioCodec: void 0, outputOptions: [] };
934
+ };
935
+ const optimizer = ({ strapi }) => ({
936
+ cancel(jobId) {
937
+ const active = activeCommands.get(jobId);
938
+ if (!active) {
939
+ return false;
940
+ }
941
+ try {
942
+ active.command.kill("SIGKILL");
943
+ } catch {
944
+ }
945
+ try {
946
+ if (fs.existsSync(active.outputPath)) {
947
+ fs.unlinkSync(active.outputPath);
948
+ }
949
+ } catch {
950
+ }
951
+ activeCommands.delete(jobId);
952
+ return true;
953
+ },
954
+ async process({
955
+ jobId,
956
+ inputPath,
957
+ settings,
958
+ resizeMode = "exact",
959
+ onProgress
960
+ }) {
961
+ configureFfmpegPath();
962
+ const metadata = await probeVideo(inputPath);
963
+ const outputExt = settings.defaultFormat === "webm" ? ".webm" : ".mp4";
964
+ const outputMime = settings.defaultFormat === "webm" ? "video/webm" : "video/mp4";
965
+ const outputPath = join(tmpdir(), `video-optimizer-${randomUUID()}${outputExt}`);
966
+ const scaleFilter = buildScaleFilter(settings, metadata, resizeMode);
967
+ const globalSettings = await strapi.plugin("video-optimizer").service("preference").getGlobalSettings();
968
+ const maxThreads = globalSettings.maxFfmpegThreads;
969
+ const videoOptions = buildVideoCodecOptions(settings, maxThreads);
970
+ const audioOptions = buildAudioOptions(settings);
971
+ const threadOptions = buildThreadOptions(maxThreads);
972
+ const duration = metadata.duration ?? 0;
973
+ await new Promise((resolve, reject) => {
974
+ let command = ffmpeg(inputPath);
975
+ if (scaleFilter) {
976
+ command = command.videoFilters(scaleFilter);
977
+ }
978
+ command = command.format(settings.defaultFormat).videoCodec(videoOptions.videoCodec).outputOptions([
979
+ ...threadOptions,
980
+ ...videoOptions.outputOptions,
981
+ ...audioOptions.outputOptions
982
+ ]);
983
+ if (audioOptions.audioCodec) {
984
+ command = command.audioCodec(audioOptions.audioCodec);
985
+ }
986
+ activeCommands.set(jobId, { command, outputPath });
987
+ command.on("start", () => {
988
+ onProgress?.(5, "encoding");
989
+ }).on("progress", (progress) => {
990
+ if (!duration || !progress.timemark) {
991
+ onProgress?.(50, "encoding");
992
+ return;
993
+ }
994
+ const parts = progress.timemark.split(":").map(Number);
995
+ const seconds = (parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0);
996
+ const ratio = Math.min(1, seconds / duration);
997
+ onProgress?.(Math.round(5 + ratio * 90), "encoding");
998
+ }).on("end", () => {
999
+ onProgress?.(98, "finalizing");
1000
+ resolve();
1001
+ }).on("error", (error) => {
1002
+ reject(error);
1003
+ }).save(outputPath);
1004
+ }).finally(() => {
1005
+ activeCommands.delete(jobId);
1006
+ });
1007
+ const outputMetadata = await probeVideo(outputPath);
1008
+ const stats = fs.statSync(outputPath);
1009
+ const baseName = inputPath.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "video";
1010
+ return {
1011
+ outputPath,
1012
+ width: outputMetadata.width,
1013
+ height: outputMetadata.height,
1014
+ sizeInBytes: stats.size,
1015
+ ext: outputExt,
1016
+ mime: outputMime,
1017
+ name: `${baseName}${outputExt}`
1018
+ };
1019
+ },
1020
+ async cleanup(path2) {
1021
+ try {
1022
+ if (fs.existsSync(path2)) {
1023
+ fs.unlinkSync(path2);
1024
+ }
1025
+ } catch {
1026
+ }
1027
+ },
1028
+ bytesToKbytes
1029
+ });
1030
+ const FILE_MODEL_UID = "plugin::upload.file";
1031
+ const emptyStore = () => ({ byId: {}, byFileId: {} });
1032
+ const now = () => (/* @__PURE__ */ new Date()).toISOString();
1033
+ const jobQueue = ({ strapi }) => {
1034
+ const activeJobs = /* @__PURE__ */ new Set();
1035
+ const cancelledJobIds = /* @__PURE__ */ new Set();
1036
+ let draining = false;
1037
+ const getPreferenceService = () => strapi.plugin(PLUGIN_ID).service("preference");
1038
+ const getOptimizerService = () => strapi.plugin(PLUGIN_ID).service("optimizer");
1039
+ const loadStore = async () => {
1040
+ const stored = await getPreferenceService().getJobsStore().get();
1041
+ return stored ?? emptyStore();
1042
+ };
1043
+ const saveStore = async (store) => {
1044
+ await getPreferenceService().getJobsStore().set({ value: store });
1045
+ };
1046
+ const updateJob = async (jobId, patch) => {
1047
+ const store = await loadStore();
1048
+ const current = store.byId[jobId];
1049
+ if (!current) {
1050
+ return null;
1051
+ }
1052
+ const next = {
1053
+ ...current,
1054
+ ...patch,
1055
+ updatedAt: now()
1056
+ };
1057
+ if (activeJobs.has(jobId) && patch.status !== "completed" && patch.status !== "failed") {
1058
+ next.status = "processing";
1059
+ }
1060
+ store.byId[jobId] = next;
1061
+ await saveStore(store);
1062
+ return next;
1063
+ };
1064
+ const removeStaleJobsForFile = (store, fileId, keepJobId) => {
1065
+ for (const [jobId, job2] of Object.entries(store.byId)) {
1066
+ if (job2.fileId !== fileId) {
1067
+ continue;
1068
+ }
1069
+ if (jobId === keepJobId) {
1070
+ continue;
1071
+ }
1072
+ if (activeJobs.has(jobId)) {
1073
+ continue;
1074
+ }
1075
+ delete store.byId[jobId];
1076
+ }
1077
+ };
1078
+ const isCancellationError = (jobId, error) => {
1079
+ if (cancelledJobIds.has(jobId)) {
1080
+ return true;
1081
+ }
1082
+ if (!(error instanceof Error)) {
1083
+ return false;
1084
+ }
1085
+ return /SIGKILL|SIGTERM|ffmpeg was killed|cancelled|canceled/i.test(error.message);
1086
+ };
1087
+ const cancelJobsForFile = async (fileId) => {
1088
+ const store = await loadStore();
1089
+ const matchingJobs = Object.values(store.byId).filter((job2) => job2.fileId === fileId);
1090
+ if (!matchingJobs.length) {
1091
+ return;
1092
+ }
1093
+ for (const job2 of matchingJobs) {
1094
+ cancelledJobIds.add(job2.id);
1095
+ activeJobs.delete(job2.id);
1096
+ getOptimizerService().cancel(job2.id);
1097
+ delete store.byId[job2.id];
1098
+ }
1099
+ delete store.byFileId[String(fileId)];
1100
+ await saveStore(store);
1101
+ const file2 = await strapi.db.query("plugin::upload.file").findOne({
1102
+ where: { id: fileId }
1103
+ });
1104
+ if (file2) {
1105
+ const providerMetadata = {
1106
+ ...file2.provider_metadata ?? {}
1107
+ };
1108
+ delete providerMetadata.videoOptimizer;
1109
+ await strapi.db.query("plugin::upload.file").update({
1110
+ where: { id: fileId },
1111
+ data: {
1112
+ provider_metadata: providerMetadata
1113
+ }
1114
+ });
1115
+ }
1116
+ strapi.log.info(`[video-optimizer] Cancelled ${matchingJobs.length} job(s) for file ${fileId}`);
1117
+ void drainQueue();
1118
+ };
1119
+ const removeJobFromStore = async (jobId, fileId) => {
1120
+ const store = await loadStore();
1121
+ delete store.byId[jobId];
1122
+ if (fileId !== void 0 && store.byFileId[String(fileId)] === jobId) {
1123
+ delete store.byFileId[String(fileId)];
1124
+ }
1125
+ await saveStore(store);
1126
+ };
1127
+ const getMaxConcurrentJobs = async () => {
1128
+ const settings = await getPreferenceService().getGlobalSettings();
1129
+ return settings.maxConcurrentJobs;
1130
+ };
1131
+ const resolveInputPath = async (fileId) => {
1132
+ const file2 = await strapi.db.query("plugin::upload.file").findOne({
1133
+ where: { id: fileId }
1134
+ });
1135
+ if (!file2?.url) {
1136
+ return null;
1137
+ }
1138
+ const uploadConfig = strapi.config.get("plugin::upload");
1139
+ const provider = uploadConfig?.provider ?? "local";
1140
+ if (provider === "local") {
1141
+ const publicDir = strapi.dirs.static.public;
1142
+ const relativePath = String(file2.url).replace(/^\//, "");
1143
+ const absolutePath = `${publicDir}/${relativePath}`;
1144
+ if (fs.existsSync(absolutePath)) {
1145
+ return absolutePath;
1146
+ }
1147
+ }
1148
+ const uploadFolder = strapi.dirs.static.public;
1149
+ const uploadsPath = `${uploadFolder}/uploads/${file2.hash}${file2.ext}`;
1150
+ if (fs.existsSync(uploadsPath)) {
1151
+ return uploadsPath;
1152
+ }
1153
+ return null;
1154
+ };
1155
+ const applyOptimizedFile = async (fileId, result) => {
1156
+ const file2 = await strapi.db.query("plugin::upload.file").findOne({
1157
+ where: { id: fileId }
1158
+ });
1159
+ if (!file2) {
1160
+ throw new Error(`File ${fileId} not found`);
1161
+ }
1162
+ const publicDir = strapi.dirs.static.public;
1163
+ const oldRelativePath = String(file2.url).replace(/^\//, "");
1164
+ const oldAbsolutePath = path.join(publicDir, oldRelativePath);
1165
+ const baseName = String(file2.name).replace(/\.[^.]+$/, "");
1166
+ const newRelativePath = `/uploads/${file2.hash}${result.ext}`;
1167
+ const newAbsolutePath = path.join(publicDir, newRelativePath.replace(/^\//, ""));
1168
+ const originalSizeInBytes = Number(
1169
+ file2.sizeInBytes ?? (typeof file2.size === "number" ? Math.round(file2.size * 1024) : 0)
1170
+ );
1171
+ if (oldAbsolutePath !== newAbsolutePath && fs.existsSync(oldAbsolutePath)) {
1172
+ fs.unlinkSync(oldAbsolutePath);
1173
+ }
1174
+ fs.mkdirSync(path.dirname(newAbsolutePath), { recursive: true });
1175
+ fs.copyFileSync(result.outputPath, newAbsolutePath);
1176
+ const updatePayload = {
1177
+ name: `${baseName}${result.ext}`,
1178
+ ext: result.ext,
1179
+ mime: result.mime,
1180
+ url: newRelativePath,
1181
+ size: getOptimizerService().bytesToKbytes(result.sizeInBytes),
1182
+ sizeInBytes: result.sizeInBytes,
1183
+ width: result.width ?? file2.width,
1184
+ height: result.height ?? file2.height,
1185
+ provider_metadata: {
1186
+ ...file2.provider_metadata ?? {},
1187
+ videoOptimizer: {
1188
+ status: "completed",
1189
+ optimizedAt: now(),
1190
+ originalSizeInBytes,
1191
+ optimizedSizeInBytes: result.sizeInBytes,
1192
+ format: result.ext.replace(".", "")
1193
+ }
1194
+ }
1195
+ };
1196
+ const updated = await strapi.db.query(FILE_MODEL_UID).update({
1197
+ where: { id: fileId },
1198
+ data: updatePayload
1199
+ });
1200
+ strapi.eventHub.emit("media.update", { media: updated });
1201
+ strapi.log.info(
1202
+ `[video-optimizer] File ${fileId} updated in Media Library (${Math.round(originalSizeInBytes / 1024 / 1024)}MB → ${Math.round(result.sizeInBytes / 1024 / 1024)}MB, ${result.ext})`
1203
+ );
1204
+ };
1205
+ const getTargetExt = (job2) => job2.settings?.defaultFormat === "webm" ? ".webm" : ".mp4";
1206
+ const getOptimizerMeta = (file2) => file2.provider_metadata?.videoOptimizer;
1207
+ const runJob = async (jobId) => {
1208
+ if (!activeJobs.has(jobId)) {
1209
+ activeJobs.add(jobId);
1210
+ }
1211
+ let fileId;
1212
+ try {
1213
+ const store = await loadStore();
1214
+ const job2 = store.byId[jobId];
1215
+ if (!job2) {
1216
+ return;
1217
+ }
1218
+ fileId = job2.fileId;
1219
+ strapi.log.info(`[video-optimizer] Job ${jobId} started for file ${job2.fileId}`);
1220
+ await updateJob(jobId, { status: "processing", stage: "preparing", progress: 0 });
1221
+ const file2 = await strapi.db.query("plugin::upload.file").findOne({
1222
+ where: { id: job2.fileId }
1223
+ });
1224
+ if (!file2) {
1225
+ throw new Error("Uploaded file not found");
1226
+ }
1227
+ const settings = job2.settings;
1228
+ if (!settings) {
1229
+ await updateJob(jobId, {
1230
+ status: "completed",
1231
+ stage: "skipped",
1232
+ progress: 100
1233
+ });
1234
+ return;
1235
+ }
1236
+ const optimizerMeta = getOptimizerMeta(file2);
1237
+ const resizeMode = optimizerMeta?.choice === "global" ? "fit-within" : "exact";
1238
+ const inputPath = await resolveInputPath(job2.fileId);
1239
+ if (!inputPath) {
1240
+ throw new Error("Could not resolve uploaded file path");
1241
+ }
1242
+ strapi.log.info(
1243
+ `[video-optimizer] Job ${jobId} encoding file ${job2.fileId} as ${settings.defaultFormat} (input: ${inputPath})`
1244
+ );
1245
+ let lastLoggedProgress = -1;
1246
+ const result = await getOptimizerService().process({
1247
+ jobId,
1248
+ inputPath,
1249
+ settings,
1250
+ resizeMode,
1251
+ onProgress: (progress, stage) => {
1252
+ void updateJob(jobId, { status: "processing", progress, stage });
1253
+ if (progress >= lastLoggedProgress + 10 || progress >= 95) {
1254
+ lastLoggedProgress = progress;
1255
+ strapi.log.info(
1256
+ `[video-optimizer] Job ${jobId} progress ${progress}% (${stage}, ${settings.defaultFormat})`
1257
+ );
1258
+ }
1259
+ }
1260
+ });
1261
+ await applyOptimizedFile(job2.fileId, result);
1262
+ await getOptimizerService().cleanup(result.outputPath);
1263
+ await updateJob(jobId, {
1264
+ status: "completed",
1265
+ stage: "completed",
1266
+ progress: 100
1267
+ });
1268
+ await removeJobFromStore(jobId, job2.fileId);
1269
+ strapi.log.info(
1270
+ `[video-optimizer] Job ${jobId} completed for file ${job2.fileId} → ${result.ext} (${result.sizeInBytes} bytes)`
1271
+ );
1272
+ } catch (error) {
1273
+ if (isCancellationError(jobId, error)) {
1274
+ cancelledJobIds.delete(jobId);
1275
+ await removeJobFromStore(jobId, fileId);
1276
+ strapi.log.info(
1277
+ `[video-optimizer] Job ${jobId} cancelled${fileId ? ` for file ${fileId}` : ""}`
1278
+ );
1279
+ return;
1280
+ }
1281
+ const message = error instanceof Error ? error.message : "Video optimization failed";
1282
+ await updateJob(jobId, {
1283
+ status: "failed",
1284
+ stage: "failed",
1285
+ progress: 0,
1286
+ error: message
1287
+ });
1288
+ if (fileId) {
1289
+ const file2 = await strapi.db.query("plugin::upload.file").findOne({
1290
+ where: { id: fileId }
1291
+ });
1292
+ if (file2) {
1293
+ await strapi.db.query("plugin::upload.file").update({
1294
+ where: { id: fileId },
1295
+ data: {
1296
+ provider_metadata: {
1297
+ ...file2.provider_metadata ?? {},
1298
+ videoOptimizer: {
1299
+ status: "failed",
1300
+ error: message,
1301
+ failedAt: now()
1302
+ }
1303
+ }
1304
+ }
1305
+ });
1306
+ }
1307
+ }
1308
+ strapi.log.error(`[video-optimizer] Job ${jobId} failed: ${message}`);
1309
+ } finally {
1310
+ activeJobs.delete(jobId);
1311
+ void drainQueue();
1312
+ }
1313
+ };
1314
+ const drainQueue = async () => {
1315
+ if (draining) {
1316
+ return;
1317
+ }
1318
+ draining = true;
1319
+ try {
1320
+ const maxConcurrent = await getMaxConcurrentJobs();
1321
+ const store = await loadStore();
1322
+ const queued = Object.values(store.byId).filter((job2) => job2.status === "queued").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1323
+ for (const job2 of queued) {
1324
+ if (activeJobs.size >= maxConcurrent) {
1325
+ break;
1326
+ }
1327
+ if (activeJobs.has(job2.id)) {
1328
+ continue;
1329
+ }
1330
+ activeJobs.add(job2.id);
1331
+ void runJob(job2.id);
1332
+ }
1333
+ } finally {
1334
+ draining = false;
1335
+ }
1336
+ };
1337
+ return {
1338
+ async enqueue({ fileId, settings, choice }) {
1339
+ const store = await loadStore();
1340
+ const existingId = store.byFileId[String(fileId)];
1341
+ const existingJob = existingId ? store.byId[existingId] : void 0;
1342
+ if (existingJob?.status === "processing" && activeJobs.has(existingId)) {
1343
+ return existingJob;
1344
+ }
1345
+ removeStaleJobsForFile(store, fileId);
1346
+ const file2 = await strapi.db.query("plugin::upload.file").findOne({
1347
+ where: { id: fileId }
1348
+ });
1349
+ const job2 = {
1350
+ id: randomUUID(),
1351
+ fileId,
1352
+ fileName: file2?.name ? String(file2.name) : void 0,
1353
+ fileHash: file2?.hash ? String(file2.hash) : void 0,
1354
+ status: "queued",
1355
+ stage: "queued",
1356
+ progress: 0,
1357
+ settings,
1358
+ createdAt: now(),
1359
+ updatedAt: now()
1360
+ };
1361
+ store.byId[job2.id] = job2;
1362
+ store.byFileId[String(fileId)] = job2.id;
1363
+ await saveStore(store);
1364
+ strapi.log.info(
1365
+ `[video-optimizer] Job ${job2.id} queued for file ${fileId} (${settings.defaultFormat}, crf=${settings.crf})`
1366
+ );
1367
+ if (file2) {
1368
+ await strapi.db.query("plugin::upload.file").update({
1369
+ where: { id: fileId },
1370
+ data: {
1371
+ provider_metadata: {
1372
+ ...file2.provider_metadata ?? {},
1373
+ videoOptimizer: {
1374
+ status: "queued",
1375
+ jobId: job2.id,
1376
+ queuedAt: now(),
1377
+ ...choice ? { choice } : {}
1378
+ }
1379
+ }
1380
+ }
1381
+ });
1382
+ }
1383
+ void drainQueue();
1384
+ return job2;
1385
+ },
1386
+ async getJob(jobId) {
1387
+ const store = await loadStore();
1388
+ return store.byId[jobId] ?? null;
1389
+ },
1390
+ async getJobsByFileIds(fileIds) {
1391
+ const store = await loadStore();
1392
+ const jobs = [];
1393
+ for (const fileId of fileIds) {
1394
+ const jobId = store.byFileId[String(fileId)];
1395
+ if (jobId && store.byId[jobId]) {
1396
+ jobs.push(store.byId[jobId]);
1397
+ }
1398
+ }
1399
+ return jobs;
1400
+ },
1401
+ async listActiveJobs() {
1402
+ const store = await loadStore();
1403
+ const result = [];
1404
+ let dirty = false;
1405
+ for (const job2 of Object.values(store.byId)) {
1406
+ if (job2.status === "completed" || job2.status === "failed") {
1407
+ delete store.byId[job2.id];
1408
+ if (store.byFileId[String(job2.fileId)] === job2.id) {
1409
+ delete store.byFileId[String(job2.fileId)];
1410
+ }
1411
+ dirty = true;
1412
+ }
1413
+ }
1414
+ for (const job2 of Object.values(store.byId)) {
1415
+ if (job2.status !== "queued" && job2.status !== "processing") {
1416
+ continue;
1417
+ }
1418
+ const canonicalJobId = store.byFileId[String(job2.fileId)];
1419
+ const isRunning = activeJobs.has(job2.id);
1420
+ const isCanonical = canonicalJobId === job2.id;
1421
+ if (!isCanonical && !isRunning) {
1422
+ delete store.byId[job2.id];
1423
+ dirty = true;
1424
+ continue;
1425
+ }
1426
+ if (isRunning && !isCanonical) {
1427
+ store.byFileId[String(job2.fileId)] = job2.id;
1428
+ if (canonicalJobId && canonicalJobId !== job2.id && !activeJobs.has(canonicalJobId)) {
1429
+ delete store.byId[canonicalJobId];
1430
+ }
1431
+ dirty = true;
1432
+ }
1433
+ const file2 = await strapi.db.query("plugin::upload.file").findOne({
1434
+ where: { id: job2.fileId }
1435
+ });
1436
+ if (!file2) {
1437
+ await cancelJobsForFile(job2.fileId);
1438
+ continue;
1439
+ }
1440
+ const optimizerMeta = getOptimizerMeta(file2);
1441
+ if (optimizerMeta?.status === "completed" || optimizerMeta?.status === "failed") {
1442
+ delete store.byId[job2.id];
1443
+ if (store.byFileId[String(job2.fileId)] === job2.id) {
1444
+ delete store.byFileId[String(job2.fileId)];
1445
+ }
1446
+ dirty = true;
1447
+ continue;
1448
+ }
1449
+ let current = store.byId[job2.id] ?? job2;
1450
+ if (!current.fileName && file2.name) {
1451
+ current = {
1452
+ ...current,
1453
+ fileName: String(file2.name),
1454
+ updatedAt: now()
1455
+ };
1456
+ store.byId[job2.id] = current;
1457
+ dirty = true;
1458
+ }
1459
+ if (!current.fileHash && file2.hash) {
1460
+ current = {
1461
+ ...current,
1462
+ fileHash: String(file2.hash),
1463
+ updatedAt: now()
1464
+ };
1465
+ store.byId[job2.id] = current;
1466
+ dirty = true;
1467
+ }
1468
+ if (isRunning && current.status !== "processing") {
1469
+ current = {
1470
+ ...current,
1471
+ status: "processing",
1472
+ updatedAt: now()
1473
+ };
1474
+ store.byId[job2.id] = current;
1475
+ dirty = true;
1476
+ }
1477
+ if (current.status === "processing" && !isRunning) {
1478
+ if (optimizerMeta?.status === "completed" || optimizerMeta?.status === "failed") {
1479
+ delete store.byId[job2.id];
1480
+ if (store.byFileId[String(job2.fileId)] === job2.id) {
1481
+ delete store.byFileId[String(job2.fileId)];
1482
+ }
1483
+ dirty = true;
1484
+ continue;
1485
+ }
1486
+ const targetExt = getTargetExt(current);
1487
+ if (file2.ext === targetExt) {
1488
+ delete store.byId[job2.id];
1489
+ if (store.byFileId[String(job2.fileId)] === job2.id) {
1490
+ delete store.byFileId[String(job2.fileId)];
1491
+ }
1492
+ dirty = true;
1493
+ continue;
1494
+ }
1495
+ }
1496
+ result.push(store.byId[job2.id] ?? current);
1497
+ }
1498
+ if (dirty) {
1499
+ await saveStore(store);
1500
+ }
1501
+ return result;
1502
+ },
1503
+ async clearJobsOnStartup() {
1504
+ const store = await loadStore();
1505
+ const jobCount = Object.keys(store.byId).length;
1506
+ if (jobCount === 0) {
1507
+ return;
1508
+ }
1509
+ await saveStore(emptyStore());
1510
+ strapi.log.info(
1511
+ `[video-optimizer] Cleared ${jobCount} persisted job(s) on startup (jobs are not resumed after restart)`
1512
+ );
1513
+ },
1514
+ drainQueue,
1515
+ cancelJobsForFile
1516
+ };
1517
+ };
1518
+ const services = {
1519
+ preference,
1520
+ optimizer,
1521
+ "job-queue": jobQueue
1522
+ };
1523
+ const index = {
1524
+ register,
1525
+ bootstrap,
1526
+ config,
1527
+ controllers,
1528
+ routes,
1529
+ services
1530
+ };
1531
+ export {
1532
+ index as default
1533
+ };