ente/src/services/upload/thumbnailService.ts

170 lines
5 KiB
TypeScript
Raw Normal View History

import { FILE_TYPE } from 'pages/gallery';
import { CustomError } from 'utils/common/errorUtil';
import { fileIsHEIC, convertHEIC2JPEG } from 'utils/file';
import { logError } from 'utils/sentry';
import { getUint8ArrayView } from './readFileService';
export const TYPE_IMAGE = 'image';
const THUMBNAIL_HEIGHT = 720;
const MAX_ATTEMPTS = 3;
const MIN_THUMBNAIL_SIZE = 50000;
const WAIT_TIME_THUMBNAIL_GENERATION = 10 * 1000;
export async function generateThumbnail(
reader: FileReader,
file: globalThis.File,
fileType:FILE_TYPE,
): Promise<{ thumbnail: Uint8Array, hasStaticThumbnail: boolean }> {
try {
let hasStaticThumbnail = false;
let canvas = null;
try {
if (fileType===FILE_TYPE.IMAGE) {
canvas=await generateImageThumbnail(file);
} else {
canvas=await generateVideoThumbnail(file);
}
} catch (e) {
logError(e);
// ignore and set staticThumbnail
hasStaticThumbnail = true;
}
const thumbnailBlob=await thumbnailCanvasToBlob(canvas);
const thumbnail = await getUint8ArrayView(
reader,
thumbnailBlob,
);
return { thumbnail, hasStaticThumbnail };
} catch (e) {
logError(e, 'Error generating thumbnail');
throw e;
}
}
export async function generateImageThumbnail(file:globalThis.File) {
const canvas = document.createElement('canvas');
const canvasCTX = canvas.getContext('2d');
let imageURL=null;
let timeout = null;
if (fileIsHEIC(file.name)) {
file = new globalThis.File(
[await convertHEIC2JPEG(file)],
null,
null,
);
}
let image = new Image();
imageURL = URL.createObjectURL(file);
image.setAttribute('src', imageURL);
await new Promise((resolve, reject) => {
image.onload = () => {
try {
const thumbnailWidth =
(image.width * THUMBNAIL_HEIGHT) / image.height;
canvas.width = thumbnailWidth;
canvas.height = THUMBNAIL_HEIGHT;
canvasCTX.drawImage(
image,
0,
0,
thumbnailWidth,
THUMBNAIL_HEIGHT,
);
image = null;
clearTimeout(timeout);
resolve(null);
} catch (e) {
reject(e);
logError(e);
reject(Error(`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`));
}
};
timeout = setTimeout(
() =>
reject(
Error(`wait time exceeded for format ${file.name.split('.').slice(-1)[0]}`),
),
WAIT_TIME_THUMBNAIL_GENERATION,
);
});
return canvas;
}
export async function generateVideoThumbnail(file:globalThis.File) {
const canvas = document.createElement('canvas');
const canvasCTX = canvas.getContext('2d');
let videoURL=null;
let timeout=null;
await new Promise((resolve, reject) => {
let video = document.createElement('video');
videoURL = URL.createObjectURL(file);
video.addEventListener('timeupdate', function () {
try {
if (!video) {
return;
}
const thumbnailWidth =
(video.videoWidth * THUMBNAIL_HEIGHT) /
video.videoHeight;
canvas.width = thumbnailWidth;
canvas.height = THUMBNAIL_HEIGHT;
canvasCTX.drawImage(
video,
0,
0,
thumbnailWidth,
THUMBNAIL_HEIGHT,
);
video = null;
clearTimeout(timeout);
resolve(null);
} catch (e) {
reject(e);
logError(e);
reject(Error(`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`));
}
});
video.preload = 'metadata';
video.src = videoURL;
video.currentTime = 3;
timeout=setTimeout(
() =>
reject(Error(`wait time exceeded for format ${file.name.split('.').slice(-1)[0]}`)),
WAIT_TIME_THUMBNAIL_GENERATION,
);
});
}
export async function thumbnailCanvasToBlob(canvas:HTMLCanvasElement) {
let thumbnailBlob = null;
let attempts = 0;
let quality = 1;
do {
attempts++;
quality /= 2;
thumbnailBlob = await new Promise((resolve) => {
canvas.toBlob(
function (blob) {
resolve(blob);
},
'image/jpeg',
quality,
);
});
thumbnailBlob = thumbnailBlob ?? new Blob([]);
} while (
thumbnailBlob.size > MIN_THUMBNAIL_SIZE &&
attempts <= MAX_ATTEMPTS
);
return thumbnailBlob;
}