@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
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "@frkntmbs/strapi-plugin-video-optimizer",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.0.0",
7
+ "description": "Per-video optimization controls for the Strapi Media Library upload flow with async FFmpeg processing.",
8
+ "license": "MIT",
9
+ "keywords": [
10
+ "strapi",
11
+ "strapi-plugin",
12
+ "plugin",
13
+ "video",
14
+ "optimizer",
15
+ "ffmpeg",
16
+ "upload",
17
+ "media-library",
18
+ "h264",
19
+ "webm"
20
+ ],
21
+ "author": "frkntmbs",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/frkntmbs/strapi-plugin-video-optimizer.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/frkntmbs/strapi-plugin-video-optimizer/issues"
28
+ },
29
+ "homepage": "https://github.com/frkntmbs/strapi-plugin-video-optimizer#readme",
30
+ "files": [
31
+ "dist",
32
+ "admin",
33
+ "server",
34
+ "strapi-admin.js",
35
+ "strapi-server.js",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "strapi-plugin build",
41
+ "verify": "strapi-plugin verify",
42
+ "prepublishOnly": "npm run build && npm run verify",
43
+ "watch": "strapi-plugin watch",
44
+ "watch:link": "strapi-plugin watch:link"
45
+ },
46
+ "exports": {
47
+ "./strapi-admin": {
48
+ "source": "./admin/src/index.ts",
49
+ "import": "./dist/admin/index.mjs",
50
+ "require": "./dist/admin/index.js",
51
+ "default": "./dist/admin/index.js"
52
+ },
53
+ "./strapi-server": {
54
+ "source": "./server/index.js",
55
+ "import": "./dist/server/index.mjs",
56
+ "require": "./dist/server/index.js",
57
+ "default": "./dist/server/index.js"
58
+ },
59
+ "./package.json": "./package.json"
60
+ },
61
+ "dependencies": {
62
+ "ffmpeg-static": "^5.2.0",
63
+ "fluent-ffmpeg": "^2.1.3"
64
+ },
65
+ "devDependencies": {
66
+ "@strapi/sdk-plugin": "^6.1.1",
67
+ "@strapi/strapi": "^5.48.0",
68
+ "@strapi/typescript-utils": "^5.48.0",
69
+ "@tanstack/react-query": "^5.64.0",
70
+ "@types/fluent-ffmpeg": "^2.1.27",
71
+ "esbuild-register": "^3.6.0",
72
+ "react": "^18.3.1",
73
+ "react-dom": "^18.3.1",
74
+ "react-intl": "^6.6.2",
75
+ "react-router-dom": "^6.30.3",
76
+ "styled-components": "^6.1.0",
77
+ "typescript": "^5.7.0",
78
+ "yalc": "^1.0.0-pre.53"
79
+ },
80
+ "peerDependencies": {
81
+ "@strapi/design-system": "^2.0.0",
82
+ "@strapi/icons": "^2.0.0",
83
+ "@strapi/strapi": "^5.0.0",
84
+ "@strapi/utils": "^5.0.0",
85
+ "react": "^17.0.0 || ^18.0.0",
86
+ "react-dom": "^17.0.0 || ^18.0.0",
87
+ "react-router-dom": "^6.0.0",
88
+ "styled-components": "^6.0.0"
89
+ },
90
+ "engines": {
91
+ "node": ">=20.0.0 <=24.x.x",
92
+ "npm": ">=6.0.0"
93
+ },
94
+ "strapi": {
95
+ "name": "video-optimizer",
96
+ "displayName": "Video Optimizer",
97
+ "description": "Per-video optimization controls for Media Library uploads with async FFmpeg processing",
98
+ "kind": "plugin"
99
+ }
100
+ }
@@ -0,0 +1 @@
1
+ export { default } from './src/index.ts';
@@ -0,0 +1,377 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ import { SERVER_BUILD_MARKER } from './buildVersion';
3
+ import {
4
+ AUDIO_MODES,
5
+ FFMPEG_PRESETS,
6
+ PLUGIN_ID,
7
+ VIDEO_CODECS,
8
+ VIDEO_FORMATS,
9
+ codecForFormat,
10
+ formatForCodec,
11
+ isVideoMime,
12
+ type OptimizationChoice,
13
+ type OptimizationSettings,
14
+ } from './constants';
15
+ import { uploadContext } from './utils/request-context';
16
+ import {
17
+ clearFallbackUploadPreferences,
18
+ clearUploadBatchPreferences,
19
+ consumeUploadPreference,
20
+ createUploadContext,
21
+ isUploadRoute,
22
+ optimizerUploadContext,
23
+ parseUploadPreferences,
24
+ registerUploadBatchPreferences,
25
+ resolveUploadBatchPreference,
26
+ setFallbackUploadPreferences,
27
+ stashUploadPreference,
28
+ takePendingPreference,
29
+ } from './utils/upload-preferences-context';
30
+
31
+ const isValidChoice = (choice: unknown): choice is OptimizationChoice =>
32
+ typeof choice === 'string' && ['original', 'global', 'custom'].includes(choice);
33
+
34
+ const isValidFormat = (format: unknown): format is OptimizationSettings['defaultFormat'] =>
35
+ typeof format === 'string' && VIDEO_FORMATS.includes(format as OptimizationSettings['defaultFormat']);
36
+
37
+ const isValidCodec = (codec: unknown): codec is OptimizationSettings['videoCodec'] =>
38
+ typeof codec === 'string' && VIDEO_CODECS.includes(codec as OptimizationSettings['videoCodec']);
39
+
40
+ const isValidAudioMode = (mode: unknown): mode is OptimizationSettings['audioMode'] =>
41
+ typeof mode === 'string' && AUDIO_MODES.includes(mode as OptimizationSettings['audioMode']);
42
+
43
+ const isValidPreset = (preset: unknown): preset is OptimizationSettings['preset'] =>
44
+ typeof preset === 'string' && FFMPEG_PRESETS.includes(preset as OptimizationSettings['preset']);
45
+
46
+ const normalizeCustomSettings = (
47
+ custom: unknown,
48
+ global: OptimizationSettings
49
+ ): OptimizationSettings | undefined => {
50
+ if (!custom || typeof custom !== 'object') {
51
+ return undefined;
52
+ }
53
+
54
+ const value = custom as Partial<OptimizationSettings>;
55
+
56
+ if (!isValidFormat(value.defaultFormat) && !isValidCodec(value.videoCodec)) {
57
+ return undefined;
58
+ }
59
+
60
+ const defaultFormat = isValidFormat(value.defaultFormat)
61
+ ? value.defaultFormat
62
+ : isValidCodec(value.videoCodec)
63
+ ? formatForCodec(value.videoCodec)
64
+ : global.defaultFormat;
65
+
66
+ return {
67
+ defaultFormat,
68
+ videoCodec: isValidCodec(value.videoCodec) ? value.videoCodec : codecForFormat(defaultFormat),
69
+ crf: typeof value.crf === 'number' ? value.crf : Number(value.crf ?? global.crf),
70
+ preset: isValidPreset(value.preset) ? value.preset : global.preset,
71
+ maxWidth:
72
+ typeof value.maxWidth === 'number' && value.maxWidth >= 1
73
+ ? value.maxWidth
74
+ : global.maxWidth,
75
+ maxHeight:
76
+ typeof value.maxHeight === 'number' && value.maxHeight >= 1
77
+ ? value.maxHeight
78
+ : global.maxHeight,
79
+ audioMode: isValidAudioMode(value.audioMode) ? value.audioMode : global.audioMode,
80
+ audioBitrate:
81
+ typeof value.audioBitrate === 'string' ? value.audioBitrate : String(value.audioBitrate ?? global.audioBitrate),
82
+ };
83
+ };
84
+
85
+ const configToSettings = (config: {
86
+ defaultFormat: OptimizationSettings['defaultFormat'];
87
+ videoCodec: OptimizationSettings['videoCodec'];
88
+ crf: number;
89
+ preset: OptimizationSettings['preset'];
90
+ maxWidth: number;
91
+ maxHeight: number;
92
+ audioMode: OptimizationSettings['audioMode'];
93
+ audioBitrate: string;
94
+ }): OptimizationSettings => ({
95
+ defaultFormat: config.defaultFormat,
96
+ videoCodec: config.videoCodec,
97
+ crf: config.crf,
98
+ preset: config.preset,
99
+ maxWidth: config.maxWidth,
100
+ maxHeight: config.maxHeight,
101
+ audioMode: config.audioMode,
102
+ audioBitrate: config.audioBitrate,
103
+ });
104
+
105
+ const runWithUser = <T>(userId: number | undefined, fn: () => Promise<T>) => {
106
+ if (userId === undefined) {
107
+ return fn();
108
+ }
109
+ return uploadContext.run({ userId }, fn);
110
+ };
111
+
112
+ const applyPreferenceToEntity = (
113
+ entity: Record<string, unknown>,
114
+ fileInfo: Record<string, unknown>,
115
+ mime: string,
116
+ globalDefaults: OptimizationSettings
117
+ ) => {
118
+ if (!isVideoMime(mime)) {
119
+ const batchPreference = resolveUploadBatchPreference(
120
+ String(entity.name ?? fileInfo.name ?? ''),
121
+ typeof fileInfo.optimizerAssetId === 'string' ? fileInfo.optimizerAssetId : undefined
122
+ );
123
+
124
+ if (!batchPreference) {
125
+ return;
126
+ }
127
+ }
128
+
129
+ const fileName = String(entity.name ?? fileInfo.name ?? '');
130
+ const assetId =
131
+ typeof fileInfo.optimizerAssetId === 'string' ? fileInfo.optimizerAssetId : undefined;
132
+
133
+ const preference = consumeUploadPreference(fileName, assetId);
134
+
135
+ const applyPreference = (choice: OptimizationChoice, custom?: unknown) => {
136
+ entity.optimizationChoice = choice;
137
+
138
+ const normalizedCustom =
139
+ choice === 'custom' ? normalizeCustomSettings(custom, globalDefaults) : undefined;
140
+
141
+ if (normalizedCustom) {
142
+ entity.optimizationCustom = normalizedCustom;
143
+ } else {
144
+ delete entity.optimizationCustom;
145
+ }
146
+
147
+ if (assetId) {
148
+ entity.optimizerAssetId = assetId;
149
+ }
150
+
151
+ stashUploadPreference(fileName, assetId, { choice, custom: normalizedCustom });
152
+ };
153
+
154
+ if (preference) {
155
+ applyPreference(preference.choice, preference.custom);
156
+ return;
157
+ }
158
+
159
+ if (fileInfo.optimizationChoice && isValidChoice(fileInfo.optimizationChoice)) {
160
+ applyPreference(fileInfo.optimizationChoice, fileInfo.optimizationCustom);
161
+ }
162
+ };
163
+
164
+ const enqueueVideoJobs = async (
165
+ strapi: Core.Strapi,
166
+ files: Array<Record<string, unknown>>,
167
+ globalDefaults: OptimizationSettings
168
+ ) => {
169
+ const preferenceService = strapi.plugin(PLUGIN_ID).service('preference');
170
+ const jobQueue = strapi.plugin(PLUGIN_ID).service('job-queue');
171
+
172
+ try {
173
+ for (const file of files) {
174
+ if (!isVideoMime(file.mime)) {
175
+ continue;
176
+ }
177
+
178
+ const fileName = String(file.name ?? '');
179
+ const assetId =
180
+ typeof file.optimizerAssetId === 'string' ? file.optimizerAssetId : undefined;
181
+
182
+ const batchPreference = resolveUploadBatchPreference(fileName, assetId);
183
+ const pending = takePendingPreference(fileName, assetId);
184
+ const optimizationChoice =
185
+ batchPreference?.choice ??
186
+ pending?.choice ??
187
+ (isValidChoice(file.optimizationChoice) ? file.optimizationChoice : undefined);
188
+ const optimizationCustom =
189
+ (batchPreference?.choice === 'custom'
190
+ ? normalizeCustomSettings(batchPreference.custom, globalDefaults)
191
+ : undefined) ??
192
+ pending?.custom ??
193
+ (file.optimizationCustom as Partial<OptimizationSettings> | undefined);
194
+
195
+ const resolved = await preferenceService.resolveOptimization({
196
+ optimizationChoice,
197
+ optimizationCustom,
198
+ mime: file.mime as string,
199
+ });
200
+
201
+ if (resolved.skip || !resolved.settings || !file.id) {
202
+ strapi.log.debug(
203
+ `[video-optimizer] Skipping file ${file.id} (${fileName}): choice=${optimizationChoice ?? 'default'}, skip=${resolved.skip}`
204
+ );
205
+ continue;
206
+ }
207
+
208
+ strapi.log.info(
209
+ `[video-optimizer] Queueing optimization for file ${file.id} (${fileName}) choice=${optimizationChoice ?? 'default'} → ${resolved.settings.defaultFormat}`
210
+ );
211
+
212
+ await jobQueue.enqueue({
213
+ fileId: Number(file.id),
214
+ settings: resolved.settings,
215
+ choice: optimizationChoice,
216
+ });
217
+ }
218
+ } finally {
219
+ clearUploadBatchPreferences();
220
+ }
221
+ };
222
+
223
+ const wrapUploadController = (
224
+ strapi: Core.Strapi,
225
+ handler: (ctx: { request: { body?: Record<string, unknown> } }) => Promise<unknown>
226
+ ) => {
227
+ return async (ctx: { request: { body?: Record<string, unknown> } }) => {
228
+ const preferences = parseUploadPreferences(ctx.request.body);
229
+
230
+ if (preferences.length === 0) {
231
+ return handler(ctx);
232
+ }
233
+
234
+ registerUploadBatchPreferences(preferences);
235
+
236
+ strapi.log.info(
237
+ `[video-optimizer] Controller preferences (${preferences.length}, ${SERVER_BUILD_MARKER}): ${preferences
238
+ .map((entry) => {
239
+ const format =
240
+ entry.preference.choice === 'custom'
241
+ ? entry.preference.custom?.defaultFormat ?? '?'
242
+ : entry.preference.choice;
243
+ return `${entry.fileName}=${format}`;
244
+ })
245
+ .join(', ')}`
246
+ );
247
+
248
+ return optimizerUploadContext.run(createUploadContext(preferences), () => handler(ctx));
249
+ };
250
+ };
251
+
252
+ export default async ({ strapi }: { strapi: Core.Strapi }) => {
253
+ const uploadService = strapi.plugin('upload').service('upload');
254
+ const globalDefaults = configToSettings(strapi.plugin(PLUGIN_ID).config as ReturnType<
255
+ typeof import('./config').default.default
256
+ >);
257
+
258
+ strapi.log.info(`[video-optimizer] Server bootstrap loaded (${SERVER_BUILD_MARKER})`);
259
+
260
+ const adminUploadController = strapi.plugin('upload').controllers['admin-upload'] as
261
+ | Record<string, (ctx: { request: { body?: Record<string, unknown> } }) => Promise<unknown>>
262
+ | undefined;
263
+
264
+ if (adminUploadController?.uploadFiles) {
265
+ const originalUploadFiles = adminUploadController.uploadFiles.bind(adminUploadController);
266
+ adminUploadController.uploadFiles = wrapUploadController(strapi, originalUploadFiles);
267
+ }
268
+
269
+ if (adminUploadController?.unstable_uploadFile) {
270
+ const originalUnstableUpload = adminUploadController.unstable_uploadFile.bind(adminUploadController);
271
+ adminUploadController.unstable_uploadFile = wrapUploadController(strapi, originalUnstableUpload);
272
+ }
273
+
274
+ strapi.server.use(async (ctx, next) => {
275
+ if (!isUploadRoute(ctx.method, ctx.path)) {
276
+ return next();
277
+ }
278
+
279
+ const preferences = parseUploadPreferences(ctx.request.body as Record<string, unknown>);
280
+
281
+ if (preferences.length === 0) {
282
+ return next();
283
+ }
284
+
285
+ registerUploadBatchPreferences(preferences);
286
+
287
+ strapi.log.debug(
288
+ `[video-optimizer] Upload preferences received (${preferences.length} file(s)) on ${ctx.path}: ${preferences
289
+ .map((entry) => {
290
+ const format =
291
+ entry.preference.choice === 'custom'
292
+ ? entry.preference.custom?.defaultFormat ?? '?'
293
+ : entry.preference.choice;
294
+ return `${entry.fileName}=${format}`;
295
+ })
296
+ .join(', ')}`
297
+ );
298
+
299
+ setFallbackUploadPreferences(preferences);
300
+
301
+ try {
302
+ return await optimizerUploadContext.run(createUploadContext(preferences), () => next());
303
+ } finally {
304
+ clearFallbackUploadPreferences();
305
+ }
306
+ });
307
+
308
+ const originalFormatFileInfo = uploadService.formatFileInfo.bind(uploadService);
309
+ uploadService.formatFileInfo = async (
310
+ fileProps: { filename: string; type: string; size: number },
311
+ fileInfo: Record<string, unknown> = {},
312
+ metas: Record<string, unknown> = {}
313
+ ) => {
314
+ const entity = await originalFormatFileInfo(fileProps, fileInfo, metas);
315
+ applyPreferenceToEntity(entity as Record<string, unknown>, fileInfo, fileProps.type, globalDefaults);
316
+ return entity;
317
+ };
318
+
319
+ const originalUpload = uploadService.upload.bind(uploadService);
320
+ uploadService.upload = async (args, opts) => {
321
+ const result = await runWithUser(opts?.user?.id, () => originalUpload(args, opts));
322
+ const files = Array.isArray(result) ? result : [result];
323
+
324
+ if (files.length) {
325
+ setImmediate(() => {
326
+ void enqueueVideoJobs(strapi, files as Array<Record<string, unknown>>, globalDefaults).catch(
327
+ (error: unknown) => {
328
+ const message = error instanceof Error ? error.message : String(error);
329
+ strapi.log.error(`[video-optimizer] Failed to enqueue upload jobs: ${message}`);
330
+ }
331
+ );
332
+ });
333
+ }
334
+
335
+ return result;
336
+ };
337
+
338
+ const originalReplace = uploadService.replace.bind(uploadService);
339
+ uploadService.replace = async (id, args, opts) => {
340
+ const result = await runWithUser(opts?.user?.id, () => originalReplace(id, args, opts));
341
+ const files = Array.isArray(result) ? result : [result];
342
+
343
+ if (files.length) {
344
+ setImmediate(() => {
345
+ void enqueueVideoJobs(strapi, files as Array<Record<string, unknown>>, globalDefaults).catch(
346
+ (error: unknown) => {
347
+ const message = error instanceof Error ? error.message : String(error);
348
+ strapi.log.error(`[video-optimizer] Failed to enqueue replace jobs: ${message}`);
349
+ }
350
+ );
351
+ });
352
+ }
353
+
354
+ return result;
355
+ };
356
+
357
+ const jobQueue = strapi.plugin(PLUGIN_ID).service('job-queue');
358
+
359
+ const originalRemove = uploadService.remove.bind(uploadService);
360
+ uploadService.remove = async (file: { id: number | string }) => {
361
+ await jobQueue.cancelJobsForFile(Number(file.id));
362
+ return originalRemove(file);
363
+ };
364
+
365
+ strapi.eventHub.on('media.delete', ({ media }: { media?: { id?: number | string } }) => {
366
+ const fileId = Number(media?.id);
367
+
368
+ if (!Number.isFinite(fileId) || fileId <= 0) {
369
+ return;
370
+ }
371
+
372
+ void jobQueue.cancelJobsForFile(fileId);
373
+ });
374
+
375
+ await strapi.plugin(PLUGIN_ID).service('preference').ensureGlobalSettingsDefaults();
376
+ await jobQueue.clearJobsOnStartup();
377
+ };
@@ -0,0 +1 @@
1
+ export const SERVER_BUILD_MARKER = 'test-8';
@@ -0,0 +1,91 @@
1
+ import {
2
+ AUDIO_MODES,
3
+ FFMPEG_PRESETS,
4
+ OPTIMIZATION_CHOICES,
5
+ VIDEO_CODECS,
6
+ VIDEO_FORMATS,
7
+ clampMaxConcurrentJobs,
8
+ clampMaxFfmpegThreads,
9
+ codecForFormat,
10
+ type AudioMode,
11
+ type FfmpegPreset,
12
+ type OptimizationChoice,
13
+ type VideoCodec,
14
+ type VideoFormat,
15
+ } from '../constants';
16
+ import type { PluginConfig } from './index';
17
+
18
+ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
19
+ defaultChoice: 'original',
20
+ defaultFormat: 'mp4',
21
+ videoCodec: 'h264',
22
+ crf: 23,
23
+ preset: 'medium',
24
+ maxWidth: 1920,
25
+ maxHeight: 1080,
26
+ audioMode: 'compress',
27
+ audioBitrate: '128k',
28
+ maxConcurrentJobs: 1,
29
+ maxFfmpegThreads: 2,
30
+ };
31
+
32
+ const isValidChoice = (choice: unknown): choice is OptimizationChoice =>
33
+ typeof choice === 'string' && OPTIMIZATION_CHOICES.includes(choice as OptimizationChoice);
34
+
35
+ const isValidFormat = (format: unknown): format is VideoFormat =>
36
+ typeof format === 'string' && VIDEO_FORMATS.includes(format as VideoFormat);
37
+
38
+ const isValidCodec = (codec: unknown): codec is VideoCodec =>
39
+ typeof codec === 'string' && VIDEO_CODECS.includes(codec as VideoCodec);
40
+
41
+ const isValidAudioMode = (mode: unknown): mode is AudioMode =>
42
+ typeof mode === 'string' && AUDIO_MODES.includes(mode as AudioMode);
43
+
44
+ const isValidPreset = (preset: unknown): preset is FfmpegPreset =>
45
+ typeof preset === 'string' && FFMPEG_PRESETS.includes(preset as FfmpegPreset);
46
+
47
+ export const normalizePluginConfig = (
48
+ ...sources: Array<Partial<PluginConfig> | undefined>
49
+ ): PluginConfig => {
50
+ const merged = Object.assign({}, DEFAULT_PLUGIN_CONFIG, ...sources.filter(Boolean));
51
+
52
+ const defaultFormat = isValidFormat(merged.defaultFormat)
53
+ ? merged.defaultFormat
54
+ : DEFAULT_PLUGIN_CONFIG.defaultFormat;
55
+
56
+ return {
57
+ defaultChoice: isValidChoice(merged.defaultChoice)
58
+ ? merged.defaultChoice
59
+ : DEFAULT_PLUGIN_CONFIG.defaultChoice,
60
+ defaultFormat,
61
+ videoCodec: isValidCodec(merged.videoCodec)
62
+ ? merged.videoCodec
63
+ : codecForFormat(defaultFormat),
64
+ crf:
65
+ typeof merged.crf === 'number' && merged.crf >= 0 && merged.crf <= 51
66
+ ? merged.crf
67
+ : DEFAULT_PLUGIN_CONFIG.crf,
68
+ preset: isValidPreset(merged.preset) ? merged.preset : DEFAULT_PLUGIN_CONFIG.preset,
69
+ maxWidth:
70
+ typeof merged.maxWidth === 'number' && merged.maxWidth >= 1
71
+ ? merged.maxWidth
72
+ : DEFAULT_PLUGIN_CONFIG.maxWidth,
73
+ maxHeight:
74
+ typeof merged.maxHeight === 'number' && merged.maxHeight >= 1
75
+ ? merged.maxHeight
76
+ : DEFAULT_PLUGIN_CONFIG.maxHeight,
77
+ audioMode: isValidAudioMode(merged.audioMode)
78
+ ? merged.audioMode
79
+ : DEFAULT_PLUGIN_CONFIG.audioMode,
80
+ audioBitrate:
81
+ typeof merged.audioBitrate === 'string' && merged.audioBitrate.trim()
82
+ ? merged.audioBitrate
83
+ : DEFAULT_PLUGIN_CONFIG.audioBitrate,
84
+ maxConcurrentJobs: clampMaxConcurrentJobs(
85
+ merged.maxConcurrentJobs ?? DEFAULT_PLUGIN_CONFIG.maxConcurrentJobs
86
+ ),
87
+ maxFfmpegThreads: clampMaxFfmpegThreads(
88
+ merged.maxFfmpegThreads ?? DEFAULT_PLUGIN_CONFIG.maxFfmpegThreads
89
+ ),
90
+ };
91
+ };
@@ -0,0 +1,51 @@
1
+ import type {
2
+ AudioMode,
3
+ FfmpegPreset,
4
+ OptimizationChoice,
5
+ VideoCodec,
6
+ VideoFormat,
7
+ } from '../constants';
8
+ import { MAX_CONCURRENT_JOBS_LIMIT, MAX_FFMPEG_THREADS_LIMIT, clampMaxConcurrentJobs, clampMaxFfmpegThreads } from '../constants';
9
+
10
+ import { DEFAULT_PLUGIN_CONFIG } from './defaults';
11
+
12
+ export interface PluginConfig {
13
+ defaultChoice: OptimizationChoice;
14
+ defaultFormat: VideoFormat;
15
+ videoCodec: VideoCodec;
16
+ crf: number;
17
+ preset: FfmpegPreset;
18
+ maxWidth: number;
19
+ maxHeight: number;
20
+ audioMode: AudioMode;
21
+ audioBitrate: string;
22
+ maxConcurrentJobs: number;
23
+ maxFfmpegThreads: number;
24
+ }
25
+
26
+ export default {
27
+ default: (): PluginConfig => ({ ...DEFAULT_PLUGIN_CONFIG }),
28
+ validator(config: Partial<PluginConfig>) {
29
+ if (config.crf !== undefined && (config.crf < 0 || config.crf > 51)) {
30
+ throw new Error('crf must be between 0 and 51');
31
+ }
32
+ if (config.maxWidth !== undefined && config.maxWidth < 1) {
33
+ throw new Error('maxWidth must be at least 1');
34
+ }
35
+ if (config.maxHeight !== undefined && config.maxHeight < 1) {
36
+ throw new Error('maxHeight must be at least 1');
37
+ }
38
+ if (
39
+ config.maxConcurrentJobs !== undefined &&
40
+ clampMaxConcurrentJobs(config.maxConcurrentJobs) !== config.maxConcurrentJobs
41
+ ) {
42
+ throw new Error(`maxConcurrentJobs must be between 1 and ${MAX_CONCURRENT_JOBS_LIMIT}`);
43
+ }
44
+ if (
45
+ config.maxFfmpegThreads !== undefined &&
46
+ clampMaxFfmpegThreads(config.maxFfmpegThreads) !== config.maxFfmpegThreads
47
+ ) {
48
+ throw new Error(`maxFfmpegThreads must be between 1 and ${MAX_FFMPEG_THREADS_LIMIT}`);
49
+ }
50
+ },
51
+ };