summaryrefslogtreecommitdiff
path: root/packages/frontend/src/composables/use-uploader.ts
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-09-24 09:01:06 +0900
committerGitHub <noreply@github.com>2025-09-24 09:01:06 +0900
commit0f8c068e84c8b7961f0f9c50c565a9f778a0cbf6 (patch)
tree7928f1c2198522fd906b1b80915e7595a81f7cdb /packages/frontend/src/composables/use-uploader.ts
parentBump version to 2025.9.1-alpha.1 (diff)
downloadmisskey-0f8c068e84c8b7961f0f9c50c565a9f778a0cbf6.tar.gz
misskey-0f8c068e84c8b7961f0f9c50c565a9f778a0cbf6.tar.bz2
misskey-0f8c068e84c8b7961f0f9c50c565a9f778a0cbf6.zip
feat(frontend): Video compression (#16574)
* wip * Update CHANGELOG.md * wip * wip * wip * wip * Update use-uploader.ts * Update use-uploader.ts
Diffstat (limited to 'packages/frontend/src/composables/use-uploader.ts')
-rw-r--r--packages/frontend/src/composables/use-uploader.ts123
1 files changed, 115 insertions, 8 deletions
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<void> {
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<void> {
@@ -564,10 +606,74 @@ export function useUploader(options: {
item.preprocessedFile = markRaw(preprocessedFile);
}
- onUnmounted(() => {
+ async function preprocessForVideo(item: UploaderItem): Promise<void> {
+ 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)),