ente/src/services/upload/thumbnailService.ts
2021-08-13 09:08:42 +05:30

176 lines
5.3 KiB
TypeScript

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) {
const err = Error(
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
);
logError(err);
reject(err);
}
});
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
);
});
return canvas;
}
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;
}