From 0f8c068e84c8b7961f0f9c50c565a9f778a0cbf6 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:01:06 +0900 Subject: feat(frontend): Video compression (#16574) * wip * Update CHANGELOG.md * wip * wip * wip * wip * Update use-uploader.ts * Update use-uploader.ts --- packages/frontend/src/composables/use-uploader.ts | 123 ++++++++++++++++++++-- 1 file changed, 115 insertions(+), 8 deletions(-) (limited to 'packages/frontend/src/composables/use-uploader.ts') diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts index 826d8c5203..12b6e85940 100644 --- a/packages/frontend/src/composables/use-uploader.ts +++ b/packages/frontend/src/composables/use-uploader.ts @@ -43,6 +43,12 @@ const IMAGE_EDITING_SUPPORTED_TYPES = [ 'image/webp', ]; +const VIDEO_COMPRESSION_SUPPORTED_TYPES = [ // TODO + 'video/mp4', + 'video/quicktime', + 'video/x-matroska', +]; + const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES; const IMAGE_PREPROCESS_NEEDED_TYPES = [ @@ -51,6 +57,10 @@ const IMAGE_PREPROCESS_NEEDED_TYPES = [ ...IMAGE_EDITING_SUPPORTED_TYPES, ]; +const VIDEO_PREPROCESS_NEEDED_TYPES = [ + ...VIDEO_COMPRESSION_SUPPORTED_TYPES, +]; + const mimeTypeMap = { 'image/webp': 'webp', 'image/jpeg': 'jpg', @@ -64,6 +74,7 @@ export type UploaderItem = { progress: { max: number; value: number } | null; thumbnail: string | null; preprocessing: boolean; + preprocessProgress: number | null; uploading: boolean; uploaded: Misskey.entities.DriveFile | null; uploadFailed: boolean; @@ -76,6 +87,7 @@ export type UploaderItem = { isSensitive?: boolean; caption?: string | null; abort?: (() => void) | null; + abortPreprocess?: (() => void) | null; }; function getCompressionSettings(level: 0 | 1 | 2 | 3) { @@ -129,11 +141,12 @@ export function useUploader(options: { progress: null, thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? window.URL.createObjectURL(file) : null, preprocessing: false, + preprocessProgress: null, uploading: false, aborted: false, uploaded: null, uploadFailed: false, - compressionLevel: prefer.s.defaultImageCompressionLevel, + compressionLevel: IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(file.type) ? prefer.s.defaultImageCompressionLevel : VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(file.type) ? prefer.s.defaultVideoCompressionLevel : 0, watermarkPresetId: uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null, file: markRaw(file), }); @@ -318,7 +331,7 @@ export function useUploader(options: { } if ( - IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) && + (IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) || VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type)) && !item.preprocessing && !item.uploading && !item.uploaded @@ -391,6 +404,19 @@ export function useUploader(options: { removeItem(item); }, }); + } else if (item.preprocessing && item.abortPreprocess != null) { + menu.push({ + type: 'divider', + }, { + icon: 'ti ti-player-stop', + text: i18n.ts.abort, + danger: true, + action: () => { + if (item.abortPreprocess != null) { + item.abortPreprocess(); + } + }, + }); } else if (item.uploading) { menu.push({ type: 'divider', @@ -474,6 +500,10 @@ export function useUploader(options: { continue; } + if (item.abortPreprocess != null) { + item.abortPreprocess(); + } + if (item.abort != null) { item.abort(); } @@ -484,18 +514,30 @@ export function useUploader(options: { async function preprocess(item: UploaderItem): Promise { item.preprocessing = true; + item.preprocessProgress = null; - try { - if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) { + if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) { + try { await preprocessForImage(item); - } - } catch (err) { - console.error('Failed to preprocess image', err); + } catch (err) { + console.error('Failed to preprocess image', err); // nop + } + } + + if (VIDEO_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) { + try { + await preprocessForVideo(item); + } catch (err) { + console.error('Failed to preprocess video', err); + + // nop + } } item.preprocessing = false; + item.preprocessProgress = null; } async function preprocessForImage(item: UploaderItem): Promise { @@ -564,10 +606,74 @@ export function useUploader(options: { item.preprocessedFile = markRaw(preprocessedFile); } - onUnmounted(() => { + async function preprocessForVideo(item: UploaderItem): Promise { + let preprocessedFile: Blob | File = item.file; + + const needsCompress = item.compressionLevel !== 0 && VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type); + + if (needsCompress) { + const mediabunny = await import('mediabunny'); + + const source = new mediabunny.BlobSource(preprocessedFile); + + const input = new mediabunny.Input({ + source, + formats: mediabunny.ALL_FORMATS, + }); + + const output = new mediabunny.Output({ + target: new mediabunny.BufferTarget(), + format: new mediabunny.Mp4OutputFormat(), + }); + + const currentConversion = await mediabunny.Conversion.init({ + input, + output, + video: { + //width: 320, // Height will be deduced automatically to retain aspect ratio + bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW, + }, + audio: { + bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW, + }, + }); + + currentConversion.onProgress = newProgress => item.preprocessProgress = newProgress; + + item.abortPreprocess = () => { + item.abortPreprocess = null; + currentConversion.cancel(); + item.preprocessing = false; + item.preprocessProgress = null; + }; + + await currentConversion.execute(); + + item.abortPreprocess = null; + + preprocessedFile = new Blob([output.target.buffer!], { type: output.format.mimeType }); + item.compressedSize = output.target.buffer!.byteLength; + item.uploadName = `${item.name}.mp4`; + } else { + item.compressedSize = null; + item.uploadName = item.name; + } + + if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); + item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null; + item.preprocessedFile = markRaw(preprocessedFile); + } + + function dispose() { for (const item of items.value) { if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); } + + abortAll(); + } + + onUnmounted(() => { + dispose(); }); return { @@ -575,6 +681,7 @@ export function useUploader(options: { addFiles, removeItem, abortAll, + dispose, upload, getMenu, uploading: computed(() => items.value.some(item => item.uploading)), -- cgit v1.2.3-freya