Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.

Commit f83d0bb

Browse files
feat: video compression with ffmpeg (#1830)
* feat: video compression with ffmpeg * update alert * feat: remove retry fallback from video compression and use different compression params depending on input file size. Also handle failures & errors with toast messages. * chore: refine video conversion input file size params * feat: cancel button for individual video compressions * fix: video compression cancel non-unique id bug, and change 'Cancel' underline from red to green * fix: format --------- Co-authored-by: Flavio Moceri <moceri.flavio@gmail.com>
1 parent 5e479d0 commit f83d0bb

File tree

9 files changed

+550
-188
lines changed

9 files changed

+550
-188
lines changed

package-lock.json

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"dependencies": {
2020
"@emoji-mart/data": "^1.2.1",
2121
"@emoji-mart/react": "^1.1.1",
22+
"@ffmpeg/ffmpeg": "^0.12.15",
23+
"@ffmpeg/util": "^0.12.2",
2224
"@noble/hashes": "^1.5.0",
2325
"@swc/helpers": "~0.5.15",
2426
"@synonymdev/pubky": "^0.5.0-rc.2",

src/components/CreateContent/Section/_FooterArea.tsx

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export default function FooterArea({
7373
setFilesBeingCompressed
7474
// charCountArticle
7575
}: FooterAreaProps) {
76-
const { addAlert, removeAlert } = useAlertContext();
76+
const { addAlert, removeAlert, updateAlert, updateAlertCancelState, removeAllCompressionAlerts } = useAlertContext();
7777
const { openModal } = useModal();
7878
const [addTagInput, setAddTagInput] = useState<boolean>(false);
7979
const [tagInput, setTagInput] = useState('');
@@ -127,8 +127,10 @@ export default function FooterArea({
127127

128128
const processFiles = async () => {
129129
const validFiles: File[] = [];
130+
const filesArray = Array.from(files);
130131

131-
for (const file of Array.from(files)) {
132+
for (let i = 0; i < filesArray.length; i++) {
133+
const file = filesArray[i];
132134
// Check if we already have 4 files - include files being compressed
133135
if (validFiles.length >= 4 - (selectedFiles?.length || 0) - (filesBeingCompressed || 0)) {
134136
break;
@@ -151,10 +153,7 @@ export default function FooterArea({
151153

152154
if (isImage && file.size > maxImageSizeInBytes) {
153155
try {
154-
const loadingAlertId = addAlert(
155-
`Compressing image ${validFiles.length + 1}/${files.length}...`,
156-
'loading'
157-
);
156+
const loadingAlertId = addAlert(`Compressing image ${i + 1}/${filesArray.length}...`, 'loading');
158157
setIsCompressing(true);
159158
setFilesBeingCompressed && setFilesBeingCompressed((prev) => prev + 1);
160159
const resizedFile = await Utils.resizeImageFile(file, maxImageSizeInBytes);
@@ -176,18 +175,59 @@ export default function FooterArea({
176175
addAlert('The maximum allowed size for videos compression is 100 MB', 'warning');
177176
continue;
178177
}
178+
let loadingAlertId: number | undefined;
179+
let abortController: AbortController | undefined;
180+
const currentFileIndex = i + 1; // Capture the current file index
179181
try {
180-
const loadingAlertId = addAlert(
181-
`Compressing video ${validFiles.length + 1}/${files.length}...`,
182-
'loading'
182+
abortController = new AbortController();
183+
184+
loadingAlertId = addAlert(
185+
`Compressing video ${currentFileIndex}/${filesArray.length}...`,
186+
'loading',
187+
() => {
188+
// Cancel compression when cancel button is clicked
189+
abortController?.abort();
190+
},
191+
true // Start with cancel disabled
183192
);
184193
setIsCompressing(true);
185194
setFilesBeingCompressed && setFilesBeingCompressed((prev) => prev + 1);
186-
const resizedFile = await Utils.resizeVideoFile(file, maxOtherSizeInBytes);
195+
196+
const resizedFile = await Utils.resizeVideoFile(
197+
file,
198+
maxOtherSizeInBytes,
199+
(progress) => {
200+
// Update the alert with current progress
201+
updateAlert(
202+
loadingAlertId!,
203+
`Compressing video ${currentFileIndex}/${filesArray.length}... ${progress}%`
204+
);
205+
206+
// Enable cancel button on first progress
207+
if (progress > 0) {
208+
updateAlertCancelState(loadingAlertId!, false);
209+
}
210+
},
211+
abortController.signal
212+
);
213+
187214
removeAlert(loadingAlertId);
188215
validFiles.push(resizedFile);
189216
} catch (error) {
190-
addAlert('The maximum allowed size for videos is 20 MB', 'warning');
217+
// Remove the loading alert first
218+
if (loadingAlertId) {
219+
removeAlert(loadingAlertId);
220+
}
221+
222+
// Check if it was cancelled
223+
if (error instanceof Error && error.message === 'Compression cancelled') {
224+
// Don't show error for cancellation
225+
continue;
226+
}
227+
228+
// Show the error message from compression
229+
const errorMessage = error instanceof Error ? error.message : String(error);
230+
addAlert(errorMessage, 'warning');
191231
continue;
192232
} finally {
193233
setFilesBeingCompressed && setFilesBeingCompressed((prev) => Math.max(0, prev - 1));

src/components/CreateContent/Section/_InputArea.tsx

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default function InputArea({
6565
setFilesBeingCompressed
6666
}: InputAreaProps) {
6767
const [isDragging, setIsDragging] = useState(false);
68-
const { addAlert, removeAlert } = useAlertContext();
68+
const { addAlert, removeAlert, updateAlert, updateAlertCancelState, removeAllCompressionAlerts } = useAlertContext();
6969

7070
const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
7171
event.preventDefault();
@@ -111,8 +111,10 @@ export default function InputArea({
111111

112112
const processFiles = async () => {
113113
const validFiles: File[] = [];
114+
const filesArray = Array.from(files);
114115

115-
for (const file of Array.from(files)) {
116+
for (let i = 0; i < filesArray.length; i++) {
117+
const file = filesArray[i];
116118
// Check if we already have 4 files - include files being compressed
117119
if (validFiles.length >= 4 - (selectedFiles?.length || 0) - (filesBeingCompressed || 0)) {
118120
break;
@@ -160,18 +162,59 @@ export default function InputArea({
160162
addAlert('The maximum allowed size for videos compression is 100 MB', 'warning');
161163
continue;
162164
}
165+
let loadingAlertId: number | undefined;
166+
let abortController: AbortController | undefined;
167+
const currentFileIndex = i + 1; // Capture the current file index
163168
try {
164-
const loadingAlertId = addAlert(
165-
`Compressing video ${validFiles.length + 1}/${files.length}...`,
166-
'loading'
169+
abortController = new AbortController();
170+
171+
loadingAlertId = addAlert(
172+
`Compressing video ${currentFileIndex}/${filesArray.length}...`,
173+
'loading',
174+
() => {
175+
// Cancel compression when cancel button is clicked
176+
abortController?.abort();
177+
},
178+
true // Start with cancel disabled
167179
);
168180
setIsCompressing(true);
169181
setFilesBeingCompressed && setFilesBeingCompressed((prev) => prev + 1);
170-
const resizedFile = await Utils.resizeVideoFile(file, maxOtherSizeInBytes);
182+
183+
const resizedFile = await Utils.resizeVideoFile(
184+
file,
185+
maxOtherSizeInBytes,
186+
(progress) => {
187+
// Update the alert with current progress
188+
updateAlert(
189+
loadingAlertId!,
190+
`Compressing video ${currentFileIndex}/${filesArray.length}... ${progress}%`
191+
);
192+
193+
// Enable cancel button on first progress
194+
if (progress > 0) {
195+
updateAlertCancelState(loadingAlertId!, false);
196+
}
197+
},
198+
abortController.signal
199+
);
200+
171201
removeAlert(loadingAlertId);
172202
validFiles.push(resizedFile);
173203
} catch (error) {
174-
addAlert('The maximum allowed size for videos is 20 MB', 'warning');
204+
// Remove the loading alert first
205+
if (loadingAlertId) {
206+
removeAlert(loadingAlertId);
207+
}
208+
209+
// Check if it was cancelled
210+
if (error instanceof Error && error.message === 'Compression cancelled') {
211+
// Don't show error for cancellation
212+
continue;
213+
}
214+
215+
// Show the error message from compression
216+
const errorMessage = error instanceof Error ? error.message : String(error);
217+
addAlert(errorMessage, 'warning');
175218
continue;
176219
} finally {
177220
setFilesBeingCompressed && setFilesBeingCompressed((prev) => Math.max(0, prev - 1));

src/components/CreateContent/index.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export default function CreateContent({
8181
setIsCompressing
8282
}: CreateContentProps) {
8383
const { profile, pubky } = usePubkyClientContext();
84-
const { addAlert, removeAlert } = useAlertContext();
84+
const { addAlert, removeAlert, updateAlert, updateAlertCancelState, removeAllCompressionAlerts } = useAlertContext();
8585
const [showEmojis, setShowEmojis] = useState(false);
8686
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
8787
const wrapperRef = useRef<HTMLDivElement>(null);
@@ -320,15 +320,55 @@ export default function CreateContent({
320320
continue;
321321
}
322322

323+
let loadingAlertId: number | undefined;
324+
let abortController: AbortController | undefined;
323325
try {
324-
const loadingAlertId = addAlert(`Compressing video ${i + 1}/${filesToProcess.length}...`, 'loading');
326+
abortController = new AbortController();
327+
328+
loadingAlertId = addAlert(
329+
`Compressing video ${i + 1}/${filesToProcess.length}...`,
330+
'loading',
331+
() => {
332+
// Cancel compression when cancel button is clicked
333+
abortController?.abort();
334+
},
335+
true
336+
); // Start with cancel disabled
325337
setIsCompressing(true);
326338
setFilesBeingCompressed((prev) => prev + 1);
327-
const resizedFile = await Utils.resizeVideoFile(file, maxOtherSizeInBytes);
339+
340+
const resizedFile = await Utils.resizeVideoFile(
341+
file,
342+
maxOtherSizeInBytes,
343+
(progress) => {
344+
// Update the alert with current progress
345+
updateAlert(loadingAlertId!, `Compressing video ${i + 1}/${filesToProcess.length}... ${progress}%`);
346+
347+
// Enable cancel button on first progress
348+
if (progress > 0) {
349+
updateAlertCancelState(loadingAlertId!, false);
350+
}
351+
},
352+
abortController.signal
353+
);
354+
328355
removeAlert(loadingAlertId);
329356
validFiles.push(resizedFile);
330357
} catch (error) {
331-
addAlert('The maximum allowed size for videos is 20 MB', 'warning');
358+
// Remove the loading alert first
359+
if (loadingAlertId) {
360+
removeAlert(loadingAlertId);
361+
}
362+
363+
// Check if it was cancelled
364+
if (error instanceof Error && error.message === 'Compression cancelled') {
365+
// Don't show error for cancellation
366+
continue;
367+
}
368+
369+
// Show the error message from compression
370+
const errorMessage = error instanceof Error ? error.message : String(error);
371+
addAlert(errorMessage, 'warning');
332372
continue;
333373
} finally {
334374
setFilesBeingCompressed((prev) => Math.max(0, prev - 1));

src/components/Modal/_CreateArticle/_Content.tsx

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,13 +255,33 @@ export default function ContentCreateArticle({
255255
addAlert('The maximum allowed size for videos compression is 100 MB', 'warning');
256256
return;
257257
}
258+
let loadingAlertId: number | undefined;
259+
let abortController: AbortController | undefined;
258260
try {
259-
const loadingAlertId = addAlert('Compressing video...', 'loading');
261+
abortController = new AbortController();
262+
263+
loadingAlertId = addAlert('Compressing video...', 'loading', () => {
264+
// Cancel compression when cancel button is clicked
265+
abortController?.abort();
266+
});
260267
setIsCompressing(true);
261-
processedFile = await Utils.resizeVideoFile(file, maxOtherSizeInBytes);
268+
processedFile = await Utils.resizeVideoFile(file, maxOtherSizeInBytes, undefined, abortController.signal);
262269
removeAlert(loadingAlertId);
263270
} catch (error) {
264-
setErrorFile('The maximum allowed size for videos is 20 MB');
271+
// Remove the loading alert first
272+
if (loadingAlertId) {
273+
removeAlert(loadingAlertId);
274+
}
275+
276+
// Check if it was cancelled
277+
if (error instanceof Error && error.message === 'Compression cancelled') {
278+
// Don't show error for cancellation
279+
return;
280+
}
281+
282+
// Show the error message from compression
283+
const errorMessage = error instanceof Error ? error.message : String(error);
284+
setErrorFile(errorMessage);
265285
return;
266286
} finally {
267287
setIsCompressing(false);
@@ -364,13 +384,33 @@ export default function ContentCreateArticle({
364384
addAlert('The maximum allowed size for videos compression is 100 MB', 'warning');
365385
return;
366386
}
387+
let loadingAlertId: number | undefined;
388+
let abortController: AbortController | undefined;
367389
try {
368-
const loadingAlertId = addAlert('Compressing video...', 'loading');
390+
abortController = new AbortController();
391+
392+
loadingAlertId = addAlert('Compressing video...', 'loading', () => {
393+
// Cancel compression when cancel button is clicked
394+
abortController?.abort();
395+
});
369396
setIsCompressing(true);
370-
processedFile = await Utils.resizeVideoFile(file, maxOtherSizeInBytes);
397+
processedFile = await Utils.resizeVideoFile(file, maxOtherSizeInBytes, undefined, abortController.signal);
371398
removeAlert(loadingAlertId);
372399
} catch (error) {
373-
addAlert('The maximum allowed size for videos is 20 MB', 'warning');
400+
// Remove the loading alert first
401+
if (loadingAlertId) {
402+
removeAlert(loadingAlertId);
403+
}
404+
405+
// Check if it was cancelled
406+
if (error instanceof Error && error.message === 'Compression cancelled') {
407+
// Don't show error for cancellation
408+
return;
409+
}
410+
411+
// Show the error message from compression
412+
const errorMessage = error instanceof Error ? error.message : String(error);
413+
addAlert(errorMessage, 'warning');
374414
return;
375415
} finally {
376416
setIsCompressing(false);

0 commit comments

Comments
 (0)