@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.
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/admin/custom.d.ts +8 -0
- package/admin/src/buildVersion.ts +3 -0
- package/admin/src/components/AssetOptimizationLabel.tsx +61 -0
- package/admin/src/components/BridgeProviders.tsx +123 -0
- package/admin/src/components/MediaLibraryCacheBridge.tsx +24 -0
- package/admin/src/components/MediaLibraryCardActionsBridge.tsx +249 -0
- package/admin/src/components/MediaLibraryJobWatcher.tsx +136 -0
- package/admin/src/components/MediaLibraryProgressBridge.tsx +97 -0
- package/admin/src/components/OptimizationChoicePicker.tsx +65 -0
- package/admin/src/components/OptimizationResizeFields.tsx +120 -0
- package/admin/src/components/OptimizationVideoFields.tsx +217 -0
- package/admin/src/components/UploadEnhancerBridge.tsx +205 -0
- package/admin/src/components/upload/PendingAssetStep.tsx +97 -0
- package/admin/src/defaultGlobalSettings.ts +32 -0
- package/admin/src/hooks/useDefaultOptimizationMode.ts +24 -0
- package/admin/src/hooks/useUploadWithOptimizer.ts +45 -0
- package/admin/src/index.ts +84 -0
- package/admin/src/pages/SettingsPage.tsx +208 -0
- package/admin/src/pluginId.ts +79 -0
- package/admin/src/translations/en.json +74 -0
- package/admin/src/translations/tr.json +74 -0
- package/admin/src/utils/adminFetch.ts +57 -0
- package/admin/src/utils/captureQueryClient.ts +34 -0
- package/admin/src/utils/debugMediaLibraryProgress.ts +70 -0
- package/admin/src/utils/extractAssetDimensions.ts +22 -0
- package/admin/src/utils/initJobPoller.ts +173 -0
- package/admin/src/utils/initMediaLibraryCardActions.ts +308 -0
- package/admin/src/utils/initMediaLibraryProgress.ts +219 -0
- package/admin/src/utils/initUploadEnhancer.ts +447 -0
- package/admin/src/utils/invalidateMediaLibrary.ts +203 -0
- package/admin/src/utils/jobProgressStore.ts +113 -0
- package/admin/src/utils/mediaLibraryCardMatch.ts +414 -0
- package/admin/src/utils/mediaLibraryCardStore.ts +223 -0
- package/admin/src/utils/mediaLibraryQueryBridge.ts +113 -0
- package/admin/src/utils/mediaLibraryRoute.ts +9 -0
- package/admin/src/utils/optimizationFields.ts +17 -0
- package/admin/src/utils/probeVideoDimensions.ts +94 -0
- package/admin/src/utils/uploadAssetStore.ts +670 -0
- package/admin/tsconfig.json +8 -0
- package/dist/admin/SettingsPage-CN2fR83m.js +150 -0
- package/dist/admin/SettingsPage-D6e536P0.mjs +150 -0
- package/dist/admin/en-CqM903j3.js +77 -0
- package/dist/admin/en-CsHicGzL.mjs +77 -0
- package/dist/admin/index-BjWoS0YU.js +2542 -0
- package/dist/admin/index-Cs_uiChW.mjs +2541 -0
- package/dist/admin/index-DOuHOS2G.js +8799 -0
- package/dist/admin/index-rAmxCQz6.mjs +8781 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/tr-Y0-ANilh.mjs +77 -0
- package/dist/admin/tr-muzHkdC4.js +77 -0
- package/dist/server/index.js +1538 -0
- package/dist/server/index.mjs +1533 -0
- package/package.json +100 -0
- package/server/index.js +1 -0
- package/server/src/bootstrap.ts +377 -0
- package/server/src/buildVersion.ts +1 -0
- package/server/src/config/defaults.ts +91 -0
- package/server/src/config/index.ts +51 -0
- package/server/src/constants.ts +83 -0
- package/server/src/controllers/index.ts +7 -0
- package/server/src/controllers/job.ts +102 -0
- package/server/src/controllers/preference.ts +206 -0
- package/server/src/index.ts +15 -0
- package/server/src/register.ts +19 -0
- package/server/src/routes/index.ts +103 -0
- package/server/src/services/index.ts +9 -0
- package/server/src/services/job-queue.ts +663 -0
- package/server/src/services/optimizer.ts +284 -0
- package/server/src/services/preference.ts +172 -0
- package/server/src/utils/request-context.ts +7 -0
- package/server/src/utils/upload-preferences-context.ts +202 -0
- package/server/tsconfig.json +8 -0
- package/strapi-admin.js +7 -0
- package/strapi-server.js +7 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { AssetOptimizationPreference, OptimizationSettings, VideoOptimizerJob } from '../pluginId';
|
|
2
|
+
import { adminPost } from './adminFetch';
|
|
3
|
+
import { wakeJobPoller } from './initJobPoller';
|
|
4
|
+
import {
|
|
5
|
+
createCustomFromGlobal,
|
|
6
|
+
createDefaultPreference,
|
|
7
|
+
getGlobalSettings,
|
|
8
|
+
} from './uploadAssetStore';
|
|
9
|
+
|
|
10
|
+
export interface MediaLibraryCardEntry {
|
|
11
|
+
fileId: number;
|
|
12
|
+
fileName: string;
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
optimizeHost: HTMLElement;
|
|
16
|
+
cancelHost: HTMLElement;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let cards: MediaLibraryCardEntry[] = [];
|
|
20
|
+
let cardsSnapshot: MediaLibraryCardEntry[] = [];
|
|
21
|
+
const listeners = new Set<() => void>();
|
|
22
|
+
|
|
23
|
+
let editingFileId: number | null = null;
|
|
24
|
+
let editingFileName: string | null = null;
|
|
25
|
+
let editingDimensions: { width?: number; height?: number } | null = null;
|
|
26
|
+
let draftPreference: AssetOptimizationPreference | null = null;
|
|
27
|
+
let enqueueInFlight = false;
|
|
28
|
+
let cancelInFlight = new Set<number>();
|
|
29
|
+
let storeRevision = 0;
|
|
30
|
+
|
|
31
|
+
const STABLE_EMPTY_DRAFT: AssetOptimizationPreference = Object.freeze({
|
|
32
|
+
choice: 'original',
|
|
33
|
+
custom: undefined,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const notify = () => {
|
|
37
|
+
storeRevision += 1;
|
|
38
|
+
listeners.forEach((listener) => listener());
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const getMediaLibraryStoreRevision = () => storeRevision;
|
|
42
|
+
|
|
43
|
+
export const subscribeMediaLibraryCards = (listener: () => void) => {
|
|
44
|
+
listeners.add(listener);
|
|
45
|
+
return () => listeners.delete(listener);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getMediaLibraryCards = () => cardsSnapshot;
|
|
49
|
+
|
|
50
|
+
const refreshEntryHosts = (entry: MediaLibraryCardEntry): MediaLibraryCardEntry | null => {
|
|
51
|
+
const optimizeHost = document.querySelector(
|
|
52
|
+
`[data-video-optimizer-ml-optimize-host="${entry.fileId}"]`
|
|
53
|
+
) as HTMLElement | null;
|
|
54
|
+
const cancelHost = document.querySelector(
|
|
55
|
+
`[data-video-optimizer-ml-cancel-host="${entry.fileId}"]`
|
|
56
|
+
) as HTMLElement | null;
|
|
57
|
+
|
|
58
|
+
if (!optimizeHost?.isConnected || !cancelHost?.isConnected) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...entry,
|
|
64
|
+
optimizeHost,
|
|
65
|
+
cancelHost,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const setMediaLibraryCards = (nextCards: MediaLibraryCardEntry[]) => {
|
|
70
|
+
const resolved = nextCards
|
|
71
|
+
.map((entry) => refreshEntryHosts(entry))
|
|
72
|
+
.filter((entry): entry is MediaLibraryCardEntry => entry !== null);
|
|
73
|
+
|
|
74
|
+
const sameLength = resolved.length === cards.length;
|
|
75
|
+
const sameEntries =
|
|
76
|
+
sameLength &&
|
|
77
|
+
resolved.every(
|
|
78
|
+
(entry, index) =>
|
|
79
|
+
entry.fileId === cards[index]?.fileId &&
|
|
80
|
+
entry.optimizeHost === cards[index]?.optimizeHost &&
|
|
81
|
+
entry.cancelHost === cards[index]?.cancelHost
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (sameEntries) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
cards = resolved;
|
|
89
|
+
cardsSnapshot = resolved.slice();
|
|
90
|
+
notify();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const getEditingMediaLibraryFileId = () => editingFileId;
|
|
94
|
+
|
|
95
|
+
export const getEditingMediaLibraryFileName = () => editingFileName;
|
|
96
|
+
|
|
97
|
+
export const getEditingMediaLibraryDimensions = () => editingDimensions;
|
|
98
|
+
|
|
99
|
+
export const getMediaLibraryDraftPreference = (): AssetOptimizationPreference => {
|
|
100
|
+
return draftPreference ?? STABLE_EMPTY_DRAFT;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const isMediaLibraryEnqueueInFlight = () => enqueueInFlight;
|
|
104
|
+
|
|
105
|
+
export const isMediaLibraryCancelInFlight = (fileId: number) => cancelInFlight.has(fileId);
|
|
106
|
+
|
|
107
|
+
export const openMediaLibraryEditor = (
|
|
108
|
+
fileId: number,
|
|
109
|
+
fileName: string,
|
|
110
|
+
dimensions?: { width?: number; height?: number }
|
|
111
|
+
) => {
|
|
112
|
+
editingFileId = fileId;
|
|
113
|
+
editingFileName = fileName;
|
|
114
|
+
editingDimensions = dimensions ?? null;
|
|
115
|
+
draftPreference = createDefaultPreference();
|
|
116
|
+
|
|
117
|
+
if (draftPreference.choice === 'custom') {
|
|
118
|
+
draftPreference.custom = createCustomForMediaLibraryFile();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
notify();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const closeMediaLibraryEditor = () => {
|
|
125
|
+
editingFileId = null;
|
|
126
|
+
editingFileName = null;
|
|
127
|
+
editingDimensions = null;
|
|
128
|
+
draftPreference = null;
|
|
129
|
+
notify();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const setMediaLibraryDraftChoice = (choice: AssetOptimizationPreference['choice']) => {
|
|
133
|
+
if (!draftPreference) {
|
|
134
|
+
draftPreference = createDefaultPreference();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
draftPreference = {
|
|
138
|
+
choice,
|
|
139
|
+
custom:
|
|
140
|
+
choice === 'custom'
|
|
141
|
+
? draftPreference.custom ?? createCustomForMediaLibraryFile()
|
|
142
|
+
: undefined,
|
|
143
|
+
};
|
|
144
|
+
notify();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const setMediaLibraryDraftCustom = (custom: OptimizationSettings) => {
|
|
148
|
+
if (!draftPreference) {
|
|
149
|
+
draftPreference = createDefaultPreference();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
draftPreference = {
|
|
153
|
+
choice: 'custom',
|
|
154
|
+
custom,
|
|
155
|
+
};
|
|
156
|
+
notify();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const saveMediaLibraryEditor = async () => {
|
|
160
|
+
if (!editingFileId || !draftPreference || draftPreference.choice === 'original') {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
enqueueInFlight = true;
|
|
165
|
+
notify();
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await adminPost<{ job?: VideoOptimizerJob }>('/video-optimizer/jobs/enqueue', {
|
|
169
|
+
fileId: editingFileId,
|
|
170
|
+
optimizationChoice: draftPreference.choice,
|
|
171
|
+
optimizationCustom:
|
|
172
|
+
draftPreference.choice === 'custom' ? draftPreference.custom : undefined,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!result?.job) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
closeMediaLibraryEditor();
|
|
180
|
+
wakeJobPoller();
|
|
181
|
+
return true;
|
|
182
|
+
} finally {
|
|
183
|
+
enqueueInFlight = false;
|
|
184
|
+
notify();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const cancelMediaLibraryJob = async (fileId: number) => {
|
|
189
|
+
if (cancelInFlight.has(fileId)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
cancelInFlight.add(fileId);
|
|
194
|
+
notify();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const result = await adminPost<{ ok?: boolean }>('/video-optimizer/jobs/cancel', { fileId });
|
|
198
|
+
|
|
199
|
+
if (!result?.ok) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
wakeJobPoller();
|
|
204
|
+
return true;
|
|
205
|
+
} finally {
|
|
206
|
+
cancelInFlight.delete(fileId);
|
|
207
|
+
notify();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const createCustomForMediaLibraryFile = (): OptimizationSettings => {
|
|
212
|
+
const base = createCustomFromGlobal();
|
|
213
|
+
const width = editingDimensions?.width;
|
|
214
|
+
const height = editingDimensions?.height;
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
...base,
|
|
218
|
+
maxWidth: width ?? base.maxWidth,
|
|
219
|
+
maxHeight: height ?? base.maxHeight,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const getMediaLibraryDefaultChoiceLabel = () => getGlobalSettings().defaultChoice;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { QueryClient } from 'react-query';
|
|
2
|
+
import { adminGet } from './adminFetch';
|
|
3
|
+
|
|
4
|
+
const UPLOAD_PLUGIN_ID = 'upload';
|
|
5
|
+
|
|
6
|
+
export interface UploadAssetRecord {
|
|
7
|
+
id: number;
|
|
8
|
+
name?: string;
|
|
9
|
+
hash?: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
mime?: string;
|
|
12
|
+
ext?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let queryClientRef: QueryClient | null = null;
|
|
16
|
+
let fetchedAssets: UploadAssetRecord[] = [];
|
|
17
|
+
let fetchPromise: Promise<UploadAssetRecord[]> | null = null;
|
|
18
|
+
let uploadAssetsFetchSettled = false;
|
|
19
|
+
|
|
20
|
+
export const registerMediaLibraryQueryClientBridge = (queryClient: QueryClient | null) => {
|
|
21
|
+
queryClientRef = queryClient;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getMediaLibraryQueryClient = () => queryClientRef;
|
|
25
|
+
|
|
26
|
+
const readAssetsFromQueryCache = (): UploadAssetRecord[] | null => {
|
|
27
|
+
if (!queryClientRef) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const query of queryClientRef.getQueryCache().findAll([UPLOAD_PLUGIN_ID, 'assets'])) {
|
|
32
|
+
if (query.state.status !== 'success') {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const data = query.state.data as UploadAssetRecord[] | { results?: UploadAssetRecord[] } | undefined;
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(data)) {
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (data && typeof data === 'object' && 'results' in data) {
|
|
43
|
+
return Array.isArray(data.results) ? data.results : [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const readAssetsFromUploadApi = async (): Promise<UploadAssetRecord[]> => {
|
|
51
|
+
const params = new URLSearchParams(window.location.search);
|
|
52
|
+
const page = params.get('page') ?? '1';
|
|
53
|
+
const pageSize = params.get('pageSize') ?? '10';
|
|
54
|
+
const sort = params.get('sort') ?? 'createdAt:DESC';
|
|
55
|
+
|
|
56
|
+
const data = await adminGet<UploadAssetRecord[] | { results?: UploadAssetRecord[] }>(
|
|
57
|
+
`/upload/files?sort=${encodeURIComponent(sort)}&page=${page}&pageSize=${pageSize}`
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(data)) {
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return data?.results ?? [];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const getUploadAssetsFromCache = (): UploadAssetRecord[] => {
|
|
68
|
+
const fromQuery = readAssetsFromQueryCache();
|
|
69
|
+
|
|
70
|
+
if (fromQuery !== null) {
|
|
71
|
+
return fromQuery;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return fetchedAssets;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const ensureUploadAssets = async (): Promise<UploadAssetRecord[]> => {
|
|
78
|
+
const fromQuery = readAssetsFromQueryCache();
|
|
79
|
+
|
|
80
|
+
if (fromQuery !== null) {
|
|
81
|
+
fetchedAssets = fromQuery;
|
|
82
|
+
uploadAssetsFetchSettled = true;
|
|
83
|
+
return fromQuery;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (uploadAssetsFetchSettled) {
|
|
87
|
+
return fetchedAssets;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (fetchPromise) {
|
|
91
|
+
return fetchPromise;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fetchPromise = (async () => {
|
|
95
|
+
try {
|
|
96
|
+
fetchedAssets = await readAssetsFromUploadApi();
|
|
97
|
+
uploadAssetsFetchSettled = true;
|
|
98
|
+
return fetchedAssets;
|
|
99
|
+
} catch {
|
|
100
|
+
return fetchedAssets;
|
|
101
|
+
} finally {
|
|
102
|
+
fetchPromise = null;
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
|
|
106
|
+
return fetchPromise;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const invalidateFetchedUploadAssets = () => {
|
|
110
|
+
fetchedAssets = [];
|
|
111
|
+
fetchPromise = null;
|
|
112
|
+
uploadAssetsFetchSettled = false;
|
|
113
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { VideoFormat } from '../pluginId';
|
|
2
|
+
|
|
3
|
+
export const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi', 'mkv', 'm4v', 'ogv', 'wmv', 'flv', '3gp'];
|
|
4
|
+
|
|
5
|
+
export const getVideoFormatFromName = (name?: string): VideoFormat | 'other' => {
|
|
6
|
+
const ext = name?.split('.').pop()?.toLowerCase();
|
|
7
|
+
|
|
8
|
+
if (ext === 'webm') {
|
|
9
|
+
return 'webm';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (ext === 'mp4' || ext === 'mov' || ext === 'm4v') {
|
|
13
|
+
return 'mp4';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return 'other';
|
|
17
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export const probeVideoFileDimensions = (
|
|
2
|
+
file: File
|
|
3
|
+
): Promise<{ width: number; height: number } | undefined> =>
|
|
4
|
+
new Promise((resolve) => {
|
|
5
|
+
const url = URL.createObjectURL(file);
|
|
6
|
+
const video = document.createElement('video');
|
|
7
|
+
video.preload = 'metadata';
|
|
8
|
+
|
|
9
|
+
const cleanup = () => {
|
|
10
|
+
URL.revokeObjectURL(url);
|
|
11
|
+
video.removeAttribute('src');
|
|
12
|
+
video.load();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
video.onloadedmetadata = () => {
|
|
16
|
+
const dimensions =
|
|
17
|
+
video.videoWidth > 0 && video.videoHeight > 0
|
|
18
|
+
? { width: video.videoWidth, height: video.videoHeight }
|
|
19
|
+
: undefined;
|
|
20
|
+
cleanup();
|
|
21
|
+
resolve(dimensions);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
video.onerror = () => {
|
|
25
|
+
cleanup();
|
|
26
|
+
resolve(undefined);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
video.src = url;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const findUploadFilesInDialog = (dialog: Element): File[] => {
|
|
33
|
+
const files: File[] = [];
|
|
34
|
+
|
|
35
|
+
dialog.querySelectorAll('input[type="file"]').forEach((input) => {
|
|
36
|
+
if (input instanceof HTMLInputElement && input.files?.length) {
|
|
37
|
+
files.push(...Array.from(input.files));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return files;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const matchUploadFile = (files: File[], assetName: string): File | undefined => {
|
|
45
|
+
const target = assetName.trim().toLowerCase();
|
|
46
|
+
|
|
47
|
+
return files.find((file) => {
|
|
48
|
+
const name = file.name.trim().toLowerCase();
|
|
49
|
+
return name === target || target.endsWith(name) || name.endsWith(target);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const ensureVideoElementDimensions = (
|
|
54
|
+
card: Element
|
|
55
|
+
): Promise<{ width: number; height: number } | undefined> => {
|
|
56
|
+
const video = card.querySelector('video');
|
|
57
|
+
|
|
58
|
+
if (!(video instanceof HTMLVideoElement)) {
|
|
59
|
+
return Promise.resolve(undefined);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (video.videoWidth > 0 && video.videoHeight > 0) {
|
|
63
|
+
return Promise.resolve({ width: video.videoWidth, height: video.videoHeight });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let settled = false;
|
|
68
|
+
|
|
69
|
+
const finish = (dimensions?: { width: number; height: number }) => {
|
|
70
|
+
if (settled) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
settled = true;
|
|
75
|
+
resolve(dimensions);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
video.addEventListener(
|
|
79
|
+
'loadedmetadata',
|
|
80
|
+
() => {
|
|
81
|
+
if (video.videoWidth > 0 && video.videoHeight > 0) {
|
|
82
|
+
finish({ width: video.videoWidth, height: video.videoHeight });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
finish(undefined);
|
|
87
|
+
},
|
|
88
|
+
{ once: true }
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
video.load();
|
|
92
|
+
window.setTimeout(() => finish(undefined), 2500);
|
|
93
|
+
});
|
|
94
|
+
};
|