@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.
- package/README.md +8 -0
- package/admin/src/components/BridgeProviders.tsx +3 -0
- package/admin/src/components/MediaLibraryCardActionsBridge.tsx +16 -0
- package/admin/src/components/OptimizationBenefitWarning.tsx +52 -0
- package/admin/src/components/UploadEnhancerBridge.tsx +13 -0
- package/admin/src/components/upload/PendingAssetStep.tsx +14 -0
- package/admin/src/translations/en.json +2 -0
- package/admin/src/translations/tr.json +2 -0
- package/admin/src/utils/evaluateOptimizationBenefit.ts +113 -0
- package/admin/src/utils/initMediaLibraryCardActions.ts +8 -2
- package/admin/src/utils/initUploadEnhancer.ts +9 -4
- package/admin/src/utils/mediaLibraryCardStore.ts +8 -2
- package/admin/src/utils/mediaLibraryQueryBridge.ts +4 -0
- package/admin/src/utils/probeVideoDimensions.ts +22 -3
- package/admin/src/utils/uploadAssetStore.ts +48 -7
- package/dist/admin/{SettingsPage-D6e536P0.mjs → SettingsPage-CiTCB9pJ.mjs} +1 -1
- package/dist/admin/{SettingsPage-CN2fR83m.js → SettingsPage-CzelpJtC.js} +1 -1
- package/dist/admin/{en-CsHicGzL.mjs → en-Cv305Xlb.mjs} +2 -0
- package/dist/admin/{en-CqM903j3.js → en-YfXcNqYi.js} +2 -0
- package/dist/admin/{index-rAmxCQz6.mjs → index-B_Qy06R1.mjs} +190 -17
- package/dist/admin/{index-DOuHOS2G.js → index-CCnMe2zb.js} +190 -17
- package/dist/admin/{index-BjWoS0YU.js → index-CcmEXX7b.js} +1 -1
- package/dist/admin/{index-Cs_uiChW.mjs → index-ClPTEo43.mjs} +2 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/{tr-Y0-ANilh.mjs → tr-CEXm27JX.mjs} +2 -0
- package/dist/admin/{tr-muzHkdC4.js → tr-Dos6G22t.js} +2 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
metadata?: { width?: number; height?: number; sizeBytes?: number }
|
|
111
115
|
) => {
|
|
112
116
|
editingFileId = fileId;
|
|
113
117
|
editingFileName = fileName;
|
|
114
|
-
editingDimensions =
|
|
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
|
};
|
|
@@ -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
|
|
27
|
+
const metadata =
|
|
17
28
|
video.videoWidth > 0 && video.videoHeight > 0
|
|
18
|
-
? {
|
|
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(
|
|
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
|
|
105
|
-
assetId
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
|
|
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:
|
|
116
|
-
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-
|
|
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-
|
|
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",
|