forked from mirrors/misskey
feat(frontend): Video compression (#16574)
* wip * Update CHANGELOG.md * wip * wip * wip * wip * Update use-uploader.ts * Update use-uploader.ts
This commit is contained in:
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user