@frkntmbs/strapi-plugin-video-optimizer 1.0.1 → 1.0.2

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 (28) hide show
  1. package/README.md +8 -0
  2. package/admin/src/components/BridgeProviders.tsx +3 -0
  3. package/admin/src/components/MediaLibraryCardActionsBridge.tsx +16 -0
  4. package/admin/src/components/OptimizationBenefitWarning.tsx +52 -0
  5. package/admin/src/components/UploadEnhancerBridge.tsx +13 -0
  6. package/admin/src/components/upload/PendingAssetStep.tsx +14 -0
  7. package/admin/src/translations/en.json +2 -0
  8. package/admin/src/translations/tr.json +2 -0
  9. package/admin/src/utils/evaluateOptimizationBenefit.ts +113 -0
  10. package/admin/src/utils/initMediaLibraryCardActions.ts +8 -2
  11. package/admin/src/utils/initUploadEnhancer.ts +9 -4
  12. package/admin/src/utils/mediaLibraryCardStore.ts +8 -2
  13. package/admin/src/utils/mediaLibraryQueryBridge.ts +4 -0
  14. package/admin/src/utils/probeVideoDimensions.ts +22 -3
  15. package/admin/src/utils/uploadAssetStore.ts +48 -7
  16. package/dist/admin/{SettingsPage-D6e536P0.mjs → SettingsPage-CiTCB9pJ.mjs} +1 -1
  17. package/dist/admin/{SettingsPage-CN2fR83m.js → SettingsPage-CzelpJtC.js} +1 -1
  18. package/dist/admin/{en-CsHicGzL.mjs → en-Cv305Xlb.mjs} +2 -0
  19. package/dist/admin/{en-CqM903j3.js → en-YfXcNqYi.js} +2 -0
  20. package/dist/admin/{index-rAmxCQz6.mjs → index-B_Qy06R1.mjs} +190 -17
  21. package/dist/admin/{index-DOuHOS2G.js → index-CCnMe2zb.js} +190 -17
  22. package/dist/admin/{index-BjWoS0YU.js → index-CcmEXX7b.js} +1 -1
  23. package/dist/admin/{index-Cs_uiChW.mjs → index-ClPTEo43.mjs} +2 -2
  24. package/dist/admin/index.js +1 -1
  25. package/dist/admin/index.mjs +1 -1
  26. package/dist/admin/{tr-Y0-ANilh.mjs → tr-CEXm27JX.mjs} +2 -0
  27. package/dist/admin/{tr-muzHkdC4.js → tr-Dos6G22t.js} +2 -0
  28. package/package.json +1 -1
package/README.md CHANGED
@@ -251,6 +251,14 @@ Configure settings for a single video:
251
251
 
252
252
  While a video is queued or encoding, hover the card and click the **stop** button to cancel the job. If the file was deleted during encoding, the job is cancelled automatically.
253
253
 
254
+ ## Test results
255
+
256
+ Real-world encode runs on a Strapi 5 project with default plugin settings (`maxConcurrentJobs: 1`, `maxFfmpegThreads: 2`). Results vary by source file, output format, and server CPU.
257
+
258
+ > **Note:** Optimization controls encoding — it does **not** guarantee a smaller file. Re-encoding an already compressed source at the same resolution may increase size. For size reduction, raise CRF, lower resolution, or stay on H.264 instead of switching to WebM.
259
+
260
+ _Test results will be added here after benchmark runs (e.g. 4K → 1920 scaling)._
261
+
254
262
  ## Permissions
255
263
 
256
264
  Global settings are protected by admin permissions:
@@ -28,6 +28,9 @@ const enMessages: Record<string, string> = {
28
28
  [`${PLUGIN_ID}.choice.custom`]: 'Custom',
29
29
  [`${PLUGIN_ID}.choice.custom.description`]:
30
30
  'Configure format and quality settings specifically for this video.',
31
+ [`${PLUGIN_ID}.optimization.warning.title`]: 'This video may already be well optimized',
32
+ [`${PLUGIN_ID}.optimization.warning.description`]:
33
+ 'Re-encoding may not reduce file size and can even make it larger. Consider Keep original, or raise CRF / lower resolution if you need a smaller output.',
31
34
  [`${PLUGIN_ID}.settings.global.defaultFormat`]: 'Output format',
32
35
  [`${PLUGIN_ID}.settings.global.videoCodec`]: 'Video codec',
33
36
  [`${PLUGIN_ID}.settings.global.crf`]: 'CRF (quality)',
@@ -9,6 +9,7 @@ import {
9
9
  } from '@strapi/design-system';
10
10
  import { Cross, Sparkle, Stop } from '@strapi/icons';
11
11
  import { useIntl } from 'react-intl';
12
+ import { OptimizationBenefitWarning } from './OptimizationBenefitWarning';
12
13
  import { OptimizationChoicePicker } from './OptimizationChoicePicker';
13
14
  import { OptimizationCustomForm } from './OptimizationVideoFields';
14
15
  import { getTranslationKey } from '../pluginId';
@@ -21,6 +22,7 @@ import {
21
22
  closeMediaLibraryEditor,
22
23
  createCustomForMediaLibraryFile,
23
24
  getEditingMediaLibraryDimensions,
25
+ getEditingMediaLibrarySizeBytes,
24
26
  getEditingMediaLibraryFileId,
25
27
  getEditingMediaLibraryFileName,
26
28
  getMediaLibraryCards,
@@ -49,6 +51,10 @@ export const MediaLibraryCardActionsBridge = () => {
49
51
  subscribeMediaLibraryCards,
50
52
  getEditingMediaLibraryDimensions
51
53
  );
54
+ const editingSizeBytes = useSyncExternalStore(
55
+ subscribeMediaLibraryCards,
56
+ getEditingMediaLibrarySizeBytes
57
+ );
52
58
  const draftPreference = useSyncExternalStore(
53
59
  subscribeMediaLibraryCards,
54
60
  getMediaLibraryDraftPreference
@@ -142,6 +148,15 @@ export const MediaLibraryCardActionsBridge = () => {
142
148
  {editingFileName}
143
149
  </Typography>
144
150
  )}
151
+ <OptimizationBenefitWarning
152
+ choice={draftPreference.choice}
153
+ metadata={{
154
+ width: editingDimensions?.width,
155
+ height: editingDimensions?.height,
156
+ sizeBytes: editingSizeBytes,
157
+ }}
158
+ customSettings={resolvedCustom ?? undefined}
159
+ />
145
160
  <OptimizationChoicePicker
146
161
  value={draftPreference.choice}
147
162
  onChange={setMediaLibraryDraftChoice}
@@ -212,6 +227,7 @@ export const MediaLibraryCardActionsBridge = () => {
212
227
  openMediaLibraryEditor(card.fileId, card.fileName, {
213
228
  width: card.width,
214
229
  height: card.height,
230
+ sizeBytes: card.sizeBytes,
215
231
  });
216
232
  }}
217
233
  >
@@ -0,0 +1,52 @@
1
+ import { Box, Typography } from '@strapi/design-system';
2
+ import { useIntl } from 'react-intl';
3
+ import { getTranslationKey, type OptimizationChoice, type OptimizationSettings } from '../pluginId';
4
+ import {
5
+ evaluateOptimizationBenefit,
6
+ type VideoSourceMetadata,
7
+ } from '../utils/evaluateOptimizationBenefit';
8
+ import { getGlobalSettings } from '../utils/uploadAssetStore';
9
+
10
+ interface OptimizationBenefitWarningProps {
11
+ choice: OptimizationChoice;
12
+ metadata: VideoSourceMetadata;
13
+ customSettings?: OptimizationSettings;
14
+ }
15
+
16
+ export const OptimizationBenefitWarning = ({
17
+ choice,
18
+ metadata,
19
+ customSettings,
20
+ }: OptimizationBenefitWarningProps) => {
21
+ const { formatMessage } = useIntl();
22
+ const showWarning = evaluateOptimizationBenefit(
23
+ choice,
24
+ metadata,
25
+ getGlobalSettings(),
26
+ customSettings
27
+ );
28
+
29
+ if (!showWarning) {
30
+ return null;
31
+ }
32
+
33
+ return (
34
+ <Box
35
+ padding={4}
36
+ hasRadius
37
+ background="warning100"
38
+ borderColor="warning200"
39
+ borderStyle="solid"
40
+ borderWidth="1px"
41
+ >
42
+ <Typography variant="omega" fontWeight="bold" textColor="warning700">
43
+ {formatMessage({ id: getTranslationKey('optimization.warning.title') })}
44
+ </Typography>
45
+ <Box paddingTop={1}>
46
+ <Typography variant="pi" textColor="warning700">
47
+ {formatMessage({ id: getTranslationKey('optimization.warning.description') })}
48
+ </Typography>
49
+ </Box>
50
+ </Box>
51
+ );
52
+ };
@@ -10,6 +10,7 @@ import {
10
10
  import { Cross, Sparkle } from '@strapi/icons';
11
11
  import { useIntl } from 'react-intl';
12
12
  import { AssetOptimizationLabel } from './AssetOptimizationLabel';
13
+ import { OptimizationBenefitWarning } from './OptimizationBenefitWarning';
13
14
  import { OptimizationChoicePicker } from './OptimizationChoicePicker';
14
15
  import { OptimizationCustomForm } from './OptimizationVideoFields';
15
16
  import { getTranslationKey } from '../pluginId';
@@ -20,6 +21,7 @@ import {
20
21
  getDraftPreference,
21
22
  getEditingAssetId,
22
23
  getSourceDimensionsForAsset,
24
+ getSourceMetadataForAsset,
23
25
  getUploadAssetCards,
24
26
  getUploadDialogElement,
25
27
  openAssetEditor,
@@ -43,6 +45,7 @@ export const UploadEnhancerBridge = () => {
43
45
 
44
46
  const editingCard = cards.find((card) => card.assetId === editingAssetId);
45
47
  const sourceDimensions = editingAssetId ? getSourceDimensionsForAsset(editingAssetId) : undefined;
48
+ const sourceMetadata = editingAssetId ? getSourceMetadataForAsset(editingAssetId) : {};
46
49
  const resolvedCustom =
47
50
  editingAssetId && draftPreference.choice === 'custom'
48
51
  ? resolveCustomSettingsForAsset(editingAssetId, draftPreference.custom)
@@ -132,6 +135,16 @@ export const UploadEnhancerBridge = () => {
132
135
  {editingCard.assetName}
133
136
  </Typography>
134
137
  )}
138
+ <OptimizationBenefitWarning
139
+ choice={draftPreference.choice}
140
+ metadata={{
141
+ width: editingCard?.width ?? sourceMetadata.width ?? sourceDimensions?.width,
142
+ height: editingCard?.height ?? sourceMetadata.height ?? sourceDimensions?.height,
143
+ sizeBytes: editingCard?.sizeBytes ?? sourceMetadata.sizeBytes,
144
+ durationSeconds: editingCard?.durationSeconds ?? sourceMetadata.durationSeconds,
145
+ }}
146
+ customSettings={resolvedCustom ?? undefined}
147
+ />
135
148
  <OptimizationChoicePicker
136
149
  value={draftPreference.choice}
137
150
  onChange={setDraftChoice}
@@ -3,12 +3,14 @@ import { Box, Flex, Typography } from '@strapi/design-system';
3
3
  import { Sparkle } from '@strapi/icons';
4
4
  import { useIntl } from 'react-intl';
5
5
  import { AssetOptimizationLabel } from '../AssetOptimizationLabel';
6
+ import { OptimizationBenefitWarning } from '../OptimizationBenefitWarning';
6
7
  import { OptimizationChoicePicker } from '../OptimizationChoicePicker';
7
8
  import { OptimizationCustomForm } from '../OptimizationVideoFields';
8
9
  import {
9
10
  createCustomForAsset,
10
11
  getAssetPreference,
11
12
  getSourceDimensionsForAsset,
13
+ getSourceMetadataForAsset,
12
14
  resolveCustomSettingsForAsset,
13
15
  type UploadAssetEntry,
14
16
  } from '../../utils/uploadAssetStore';
@@ -28,6 +30,7 @@ export const PendingAssetStep = ({
28
30
  const { formatMessage } = useIntl();
29
31
 
30
32
  const sourceDimensions = getSourceDimensionsForAsset(asset.assetId);
33
+ const sourceMetadata = getSourceMetadataForAsset(asset.assetId);
31
34
  const resolvedCustom = resolveCustomSettingsForAsset(asset.assetId, preference.custom);
32
35
 
33
36
  const handleChoiceChange = (choice: AssetOptimizationPreference['choice']) => {
@@ -54,6 +57,17 @@ export const PendingAssetStep = ({
54
57
  {asset.assetName}
55
58
  </Typography>
56
59
 
60
+ <OptimizationBenefitWarning
61
+ choice={preference.choice}
62
+ metadata={{
63
+ width: asset.width ?? sourceMetadata.width ?? sourceDimensions?.width,
64
+ height: asset.height ?? sourceMetadata.height ?? sourceDimensions?.height,
65
+ sizeBytes: asset.sizeBytes ?? sourceMetadata.sizeBytes,
66
+ durationSeconds: asset.durationSeconds ?? sourceMetadata.durationSeconds,
67
+ }}
68
+ customSettings={preference.choice === 'custom' ? resolvedCustom : undefined}
69
+ />
70
+
57
71
  <OptimizationChoicePicker value={preference.choice} onChange={handleChoiceChange} />
58
72
 
59
73
  {preference.choice === 'custom' && (
@@ -45,6 +45,8 @@
45
45
  "choice.global.description": "Uses the global optimization profile configured in Settings.",
46
46
  "choice.custom": "Custom",
47
47
  "choice.custom.description": "Configure format and quality settings specifically for this video.",
48
+ "optimization.warning.title": "This video may already be well optimized",
49
+ "optimization.warning.description": "Re-encoding may not reduce file size and can even make it larger. Consider Keep original, or raise CRF / lower resolution if you need a smaller output.",
48
50
  "upload.optimization.label": "Optimization",
49
51
  "upload.button.label": "Optimization settings",
50
52
  "upload.modal.title": "Video optimization",
@@ -45,6 +45,8 @@
45
45
  "choice.global.description": "Settings altındaki global optimizasyon profilini kullanır.",
46
46
  "choice.custom": "Özel",
47
47
  "choice.custom.description": "Bu video için format ve kalite ayarlarını ayrıca yapılandırın.",
48
+ "optimization.warning.title": "Bu video zaten iyi optimize edilmiş olabilir",
49
+ "optimization.warning.description": "Yeniden encode dosya boyutunu düşürmeyebilir, hatta büyütebilir. Olduğu gibi bırakmayı düşünün; daha küçük çıktı için CRF değerini yükseltin veya çözünürlüğü düşürün.",
48
50
  "upload.optimization.label": "Optimizasyon",
49
51
  "upload.button.label": "Optimizasyon ayarları",
50
52
  "upload.modal.title": "Video optimizasyonu",
@@ -0,0 +1,113 @@
1
+ import type {
2
+ GlobalOptimizationSettings,
3
+ OptimizationChoice,
4
+ OptimizationSettings,
5
+ } from '../pluginId';
6
+
7
+ export interface VideoSourceMetadata {
8
+ width?: number;
9
+ height?: number;
10
+ sizeBytes?: number;
11
+ durationSeconds?: number;
12
+ }
13
+
14
+ const willResize = (
15
+ sourceWidth: number,
16
+ sourceHeight: number,
17
+ targetWidth: number,
18
+ targetHeight: number,
19
+ resizeMode: 'exact' | 'fit-within'
20
+ ) => {
21
+ if (targetWidth === sourceWidth && targetHeight === sourceHeight) {
22
+ return false;
23
+ }
24
+
25
+ if (resizeMode === 'fit-within') {
26
+ return sourceWidth > targetWidth || sourceHeight > targetHeight;
27
+ }
28
+
29
+ return true;
30
+ };
31
+
32
+ const bitrateMbps = (sizeBytes: number, durationSeconds: number) =>
33
+ (sizeBytes * 8) / durationSeconds / 1_000_000;
34
+
35
+ const maxBitrateMbpsForResolution = (maxDimension: number) => {
36
+ if (maxDimension <= 720) {
37
+ return 1.5;
38
+ }
39
+
40
+ if (maxDimension <= 1080) {
41
+ return 2.5;
42
+ }
43
+
44
+ if (maxDimension <= 1440) {
45
+ return 5;
46
+ }
47
+
48
+ return 8;
49
+ };
50
+
51
+ const isBitrateAlreadyEfficient = (metadata: VideoSourceMetadata) => {
52
+ const { width, height, sizeBytes, durationSeconds } = metadata;
53
+
54
+ if (!width || !height || !sizeBytes || !durationSeconds || durationSeconds <= 0) {
55
+ return false;
56
+ }
57
+
58
+ const maxDimension = Math.max(width, height);
59
+ const bitrate = bitrateMbps(sizeBytes, durationSeconds);
60
+
61
+ return bitrate <= maxBitrateMbpsForResolution(maxDimension);
62
+ };
63
+
64
+ export const evaluateOptimizationBenefit = (
65
+ choice: OptimizationChoice,
66
+ metadata: VideoSourceMetadata,
67
+ globalSettings: GlobalOptimizationSettings,
68
+ customSettings?: OptimizationSettings
69
+ ): boolean => {
70
+ if (choice === 'original') {
71
+ return false;
72
+ }
73
+
74
+ const { width, height } = metadata;
75
+
76
+ if (!width || !height) {
77
+ return false;
78
+ }
79
+
80
+ if (choice === 'global') {
81
+ const wouldResize = willResize(
82
+ width,
83
+ height,
84
+ globalSettings.maxWidth,
85
+ globalSettings.maxHeight,
86
+ 'fit-within'
87
+ );
88
+
89
+ if (wouldResize) {
90
+ return false;
91
+ }
92
+
93
+ return isBitrateAlreadyEfficient(metadata) || Boolean(metadata.sizeBytes);
94
+ }
95
+
96
+ if (choice === 'custom' && customSettings) {
97
+ const wouldResizeCustom = willResize(
98
+ width,
99
+ height,
100
+ customSettings.maxWidth,
101
+ customSettings.maxHeight,
102
+ 'exact'
103
+ );
104
+
105
+ if (wouldResizeCustom) {
106
+ return false;
107
+ }
108
+
109
+ return isBitrateAlreadyEfficient(metadata) || Boolean(metadata.sizeBytes);
110
+ }
111
+
112
+ return false;
113
+ };
@@ -200,8 +200,14 @@ const collectCardActions = (uploadAssets: UploadAssetRecord[]): MediaLibraryCard
200
200
  entries.push({
201
201
  fileId,
202
202
  fileName: asset?.name ?? card.querySelector('[id$="-title"]')?.textContent?.trim() ?? '',
203
- width: dimensions?.width,
204
- height: dimensions?.height,
203
+ width: dimensions?.width ?? asset?.width,
204
+ height: dimensions?.height ?? asset?.height,
205
+ sizeBytes:
206
+ typeof asset?.sizeInBytes === 'number'
207
+ ? asset.sizeInBytes
208
+ : typeof asset?.size === 'number'
209
+ ? Math.round(asset.size * 1024)
210
+ : undefined,
205
211
  optimizeHost,
206
212
  cancelHost,
207
213
  });
@@ -11,7 +11,7 @@ import {
11
11
  setGlobalSettings,
12
12
  setUploadAssetCards,
13
13
  setUploadDialogElement,
14
- updateAssetDimensions,
14
+ updateAssetMetadata,
15
15
  type UploadAssetEntry,
16
16
  } from './uploadAssetStore';
17
17
  import { isVideoFileName } from '../pluginId';
@@ -20,7 +20,7 @@ import {
20
20
  ensureVideoElementDimensions,
21
21
  findUploadFilesInDialog,
22
22
  matchUploadFile,
23
- probeVideoFileDimensions,
23
+ probeVideoFileMetadata,
24
24
  } from './probeVideoDimensions';
25
25
 
26
26
  const pendingDimensionProbes = new Set<string>();
@@ -312,12 +312,17 @@ const queueDimensionProbe = (
312
312
  const file = matchUploadFile(files, assetName);
313
313
 
314
314
  if (file) {
315
- dimensions = await probeVideoFileDimensions(file);
315
+ const metadata = await probeVideoFileMetadata(file);
316
+
317
+ if (metadata) {
318
+ updateAssetMetadata(assetId, metadata);
319
+ return;
320
+ }
316
321
  }
317
322
  }
318
323
 
319
324
  if (dimensions) {
320
- updateAssetDimensions(assetId, dimensions);
325
+ updateAssetMetadata(assetId, dimensions);
321
326
  }
322
327
  } finally {
323
328
  pendingDimensionProbes.delete(assetId);
@@ -12,6 +12,7 @@ export interface MediaLibraryCardEntry {
12
12
  fileName: string;
13
13
  width?: number;
14
14
  height?: number;
15
+ sizeBytes?: number;
15
16
  optimizeHost: HTMLElement;
16
17
  cancelHost: HTMLElement;
17
18
  }
@@ -23,6 +24,7 @@ const listeners = new Set<() => void>();
23
24
  let editingFileId: number | null = null;
24
25
  let editingFileName: string | null = null;
25
26
  let editingDimensions: { width?: number; height?: number } | null = null;
27
+ let editingSizeBytes: number | undefined;
26
28
  let draftPreference: AssetOptimizationPreference | null = null;
27
29
  let enqueueInFlight = false;
28
30
  let cancelInFlight = new Set<number>();
@@ -96,6 +98,8 @@ export const getEditingMediaLibraryFileName = () => editingFileName;
96
98
 
97
99
  export const getEditingMediaLibraryDimensions = () => editingDimensions;
98
100
 
101
+ export const getEditingMediaLibrarySizeBytes = () => editingSizeBytes;
102
+
99
103
  export const getMediaLibraryDraftPreference = (): AssetOptimizationPreference => {
100
104
  return draftPreference ?? STABLE_EMPTY_DRAFT;
101
105
  };
@@ -107,11 +111,12 @@ export const isMediaLibraryCancelInFlight = (fileId: number) => cancelInFlight.h
107
111
  export const openMediaLibraryEditor = (
108
112
  fileId: number,
109
113
  fileName: string,
110
- dimensions?: { width?: number; height?: number }
114
+ metadata?: { width?: number; height?: number; sizeBytes?: number }
111
115
  ) => {
112
116
  editingFileId = fileId;
113
117
  editingFileName = fileName;
114
- editingDimensions = dimensions ?? null;
118
+ editingDimensions = metadata ? { width: metadata.width, height: metadata.height } : null;
119
+ editingSizeBytes = metadata?.sizeBytes;
115
120
  draftPreference = createDefaultPreference();
116
121
 
117
122
  if (draftPreference.choice === 'custom') {
@@ -125,6 +130,7 @@ export const closeMediaLibraryEditor = () => {
125
130
  editingFileId = null;
126
131
  editingFileName = null;
127
132
  editingDimensions = null;
133
+ editingSizeBytes = undefined;
128
134
  draftPreference = null;
129
135
  notify();
130
136
  };
@@ -10,6 +10,10 @@ export interface UploadAssetRecord {
10
10
  url?: string;
11
11
  mime?: string;
12
12
  ext?: string;
13
+ width?: number;
14
+ height?: number;
15
+ size?: number;
16
+ sizeInBytes?: number;
13
17
  }
14
18
 
15
19
  let queryClientRef: QueryClient | null = null;
@@ -1,6 +1,17 @@
1
1
  export const probeVideoFileDimensions = (
2
2
  file: File
3
3
  ): Promise<{ width: number; height: number } | undefined> =>
4
+ probeVideoFileMetadata(file).then((metadata) =>
5
+ metadata?.width && metadata?.height
6
+ ? { width: metadata.width, height: metadata.height }
7
+ : undefined
8
+ );
9
+
10
+ export const probeVideoFileMetadata = (
11
+ file: File
12
+ ): Promise<
13
+ { width: number; height: number; durationSeconds?: number; sizeBytes: number } | undefined
14
+ > =>
4
15
  new Promise((resolve) => {
5
16
  const url = URL.createObjectURL(file);
6
17
  const video = document.createElement('video');
@@ -13,12 +24,20 @@ export const probeVideoFileDimensions = (
13
24
  };
14
25
 
15
26
  video.onloadedmetadata = () => {
16
- const dimensions =
27
+ const metadata =
17
28
  video.videoWidth > 0 && video.videoHeight > 0
18
- ? { width: video.videoWidth, height: video.videoHeight }
29
+ ? {
30
+ width: video.videoWidth,
31
+ height: video.videoHeight,
32
+ durationSeconds:
33
+ Number.isFinite(video.duration) && video.duration > 0
34
+ ? video.duration
35
+ : undefined,
36
+ sizeBytes: file.size,
37
+ }
19
38
  : undefined;
20
39
  cleanup();
21
- resolve(dimensions);
40
+ resolve(metadata);
22
41
  };
23
42
 
24
43
  video.onerror = () => {
@@ -8,12 +8,15 @@ import type {
8
8
  import { isVideoFileName } from '../pluginId';
9
9
  import { PLUGIN_BUILD_MARKER } from '../buildVersion';
10
10
  import { wakeJobPoller } from './initJobPoller';
11
+ import type { VideoSourceMetadata } from './evaluateOptimizationBenefit';
11
12
 
12
13
  export interface UploadAssetEntry {
13
14
  assetId: string;
14
15
  assetName: string;
15
16
  width?: number;
16
17
  height?: number;
18
+ sizeBytes?: number;
19
+ durationSeconds?: number;
17
20
  actionsContainer: HTMLElement;
18
21
  footerHost?: HTMLElement;
19
22
  }
@@ -22,6 +25,7 @@ let globalSettings: GlobalOptimizationSettings = { ...DEFAULT_GLOBAL_SETTINGS };
22
25
  const assetPreferencesById = new Map<string, AssetOptimizationPreference>();
23
26
  const assetNamesById = new Map<string, string>();
24
27
  const assetDimensionsById = new Map<string, { width: number; height: number }>();
28
+ const assetMetadataById = new Map<string, VideoSourceMetadata>();
25
29
  const assetPreferencesByFileKey = new Map<string, AssetOptimizationPreference>();
26
30
  const committedPreferencesByAssetId = new Map<string, AssetOptimizationPreference>();
27
31
  const committedPreferencesByName = new Map<string, AssetOptimizationPreference>();
@@ -101,19 +105,48 @@ export const resolveCustomSettingsForAsset = (
101
105
 
102
106
  export const getSourceDimensionsForAsset = (assetId: string) => assetDimensionsById.get(assetId);
103
107
 
104
- export const updateAssetDimensions = (
105
- assetId: string,
106
- dimensions: { width: number; height: number }
107
- ) => {
108
- assetDimensionsById.set(assetId, dimensions);
108
+ export const getSourceMetadataForAsset = (assetId: string): VideoSourceMetadata => {
109
+ const stored = assetMetadataById.get(assetId);
110
+
111
+ if (stored) {
112
+ return stored;
113
+ }
114
+
115
+ const dimensions = assetDimensionsById.get(assetId);
116
+ const card = cardsSnapshot.find((entry) => entry.assetId === assetId);
117
+
118
+ return {
119
+ width: dimensions?.width ?? card?.width,
120
+ height: dimensions?.height ?? card?.height,
121
+ sizeBytes: card?.sizeBytes,
122
+ durationSeconds: card?.durationSeconds,
123
+ };
124
+ };
125
+
126
+ export const updateAssetMetadata = (assetId: string, metadata: VideoSourceMetadata) => {
127
+ const nextMetadata = {
128
+ ...assetMetadataById.get(assetId),
129
+ ...metadata,
130
+ };
131
+
132
+ assetMetadataById.set(assetId, nextMetadata);
133
+
134
+ if (nextMetadata.width && nextMetadata.height) {
135
+ assetDimensionsById.set(assetId, {
136
+ width: nextMetadata.width,
137
+ height: nextMetadata.height,
138
+ });
139
+ }
109
140
 
110
141
  const index = cards.findIndex((entry) => entry.assetId === assetId);
111
142
 
112
143
  if (index >= 0) {
113
144
  cards[index] = {
114
145
  ...cards[index],
115
- width: dimensions.width,
116
- height: dimensions.height,
146
+ width: nextMetadata.width ?? cards[index].width,
147
+ height: nextMetadata.height ?? cards[index].height,
148
+ sizeBytes: nextMetadata.sizeBytes ?? cards[index].sizeBytes,
149
+ durationSeconds: nextMetadata.durationSeconds ?? cards[index].durationSeconds,
117
150
  };
118
151
  cardsSnapshot = cards.slice();
119
152
  }
@@ -121,6 +154,13 @@ export const updateAssetDimensions = (
121
154
  notify();
122
155
  };
123
156
 
157
+ export const updateAssetDimensions = (
158
+ assetId: string,
159
+ dimensions: { width: number; height: number }
160
+ ) => {
161
+ updateAssetMetadata(assetId, dimensions);
162
+ };
163
+
124
164
  export const getAssetDimensions = (assetId: string) => assetDimensionsById.get(assetId);
125
165
 
126
166
  export const createDefaultPreference = (): AssetOptimizationPreference => {
@@ -260,6 +300,7 @@ export const clearUploadSession = () => {
260
300
  assetPreferencesById.clear();
261
301
  assetNamesById.clear();
262
302
  assetDimensionsById.clear();
303
+ assetMetadataById.clear();
263
304
  cards = [];
264
305
  cardsSnapshot = [];
265
306
  editingAssetId = null;
@@ -3,7 +3,7 @@ import { useState, useEffect } from "react";
3
3
  import { Typography, Button, Flex, Box, Grid, Field, SingleSelect, SingleSelectOption, TextInput } from "@strapi/design-system";
4
4
  import { Check } from "@strapi/icons";
5
5
  import { useFetchClient, useNotification, useRBAC, Page, Layouts } from "@strapi/strapi/admin";
6
- import { u as useIntl, D as DEFAULT_GLOBAL_SETTINGS, g as getTranslationKey, O as OptimizationVideoFields, a as OptimizationResizeFields, M as MAX_CONCURRENT_JOBS_LIMIT, c as clampMaxConcurrentJobs, b as MAX_FFMPEG_THREADS_LIMIT, d as clampMaxFfmpegThreads, P as PLUGIN_ID, m as mergeGlobalSettings } from "./index-rAmxCQz6.mjs";
6
+ import { u as useIntl, D as DEFAULT_GLOBAL_SETTINGS, g as getTranslationKey, O as OptimizationVideoFields, a as OptimizationResizeFields, M as MAX_CONCURRENT_JOBS_LIMIT, c as clampMaxConcurrentJobs, b as MAX_FFMPEG_THREADS_LIMIT, d as clampMaxFfmpegThreads, P as PLUGIN_ID, m as mergeGlobalSettings } from "./index-B_Qy06R1.mjs";
7
7
  const SETTINGS_READ = [{ action: "plugin::video-optimizer.settings.read", subject: null }];
8
8
  const SETTINGS_UPDATE = [{ action: "plugin::video-optimizer.settings.update", subject: null }];
9
9
  const CHOICES = ["original", "global", "custom"];
@@ -5,7 +5,7 @@ const React = require("react");
5
5
  const designSystem = require("@strapi/design-system");
6
6
  const icons = require("@strapi/icons");
7
7
  const admin = require("@strapi/strapi/admin");
8
- const index = require("./index-DOuHOS2G.js");
8
+ const index = require("./index-CCnMe2zb.js");
9
9
  const SETTINGS_READ = [{ action: "plugin::video-optimizer.settings.read", subject: null }];
10
10
  const SETTINGS_UPDATE = [{ action: "plugin::video-optimizer.settings.update", subject: null }];
11
11
  const CHOICES = ["original", "global", "custom"];
@@ -45,6 +45,8 @@ const en = {
45
45
  "choice.global.description": "Uses the global optimization profile configured in Settings.",
46
46
  "choice.custom": "Custom",
47
47
  "choice.custom.description": "Configure format and quality settings specifically for this video.",
48
+ "optimization.warning.title": "This video may already be well optimized",
49
+ "optimization.warning.description": "Re-encoding may not reduce file size and can even make it larger. Consider Keep original, or raise CRF / lower resolution if you need a smaller output.",
48
50
  "upload.optimization.label": "Optimization",
49
51
  "upload.button.label": "Optimization settings",
50
52
  "upload.modal.title": "Video optimization",
@@ -47,6 +47,8 @@ const en = {
47
47
  "choice.global.description": "Uses the global optimization profile configured in Settings.",
48
48
  "choice.custom": "Custom",
49
49
  "choice.custom.description": "Configure format and quality settings specifically for this video.",
50
+ "optimization.warning.title": "This video may already be well optimized",
51
+ "optimization.warning.description": "Re-encoding may not reduce file size and can even make it larger. Consider Keep original, or raise CRF / lower resolution if you need a smaller output.",
50
52
  "upload.optimization.label": "Optimization",
51
53
  "upload.button.label": "Optimization settings",
52
54
  "upload.modal.title": "Video optimization",