Move to @/media
This commit is contained in:
parent
5c04864b0e
commit
72fa6c653f
12 changed files with 176 additions and 159 deletions
|
@ -1,7 +1,7 @@
|
|||
import log from "@/next/log";
|
||||
import { wait } from "@/utils/promise";
|
||||
import { boxSealOpen, toB64 } from "@ente/shared/crypto/internal/libsodium";
|
||||
import castGateway from "@ente/shared/network/cast";
|
||||
import { wait } from "@ente/shared/utils";
|
||||
import _sodium from "libsodium-wrappers";
|
||||
|
||||
export interface Registration {
|
||||
|
|
|
@ -5,10 +5,10 @@ import { nameAndExtension } from "@/next/file";
|
|||
import log from "@/next/log";
|
||||
import { shuffled } from "@/utils/array";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { wait } from "@/utils/promise";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getCastFileURL, getEndpoint } from "@ente/shared/network/api";
|
||||
import { wait } from "@ente/shared/utils";
|
||||
import type { CastData } from "services/cast-data";
|
||||
import { detectMediaMIMEType } from "services/detect-type";
|
||||
import {
|
||||
|
|
|
@ -3,12 +3,12 @@ import { decodeLivePhoto } from "@/media/live-photo";
|
|||
import type { Metadata } from "@/media/types/file";
|
||||
import { ensureElectron } from "@/next/electron";
|
||||
import log from "@/next/log";
|
||||
import { wait } from "@/utils/promise";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { Events, eventBus } from "@ente/shared/events";
|
||||
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
||||
import { formatDateTimeShort } from "@ente/shared/time/format";
|
||||
import { User } from "@ente/shared/user/types";
|
||||
import { wait } from "@ente/shared/utils";
|
||||
import QueueProcessor, {
|
||||
CancellationStatus,
|
||||
RequestCanceller,
|
||||
|
|
|
@ -3,9 +3,9 @@ import { decodeLivePhoto } from "@/media/live-photo";
|
|||
import { ensureElectron } from "@/next/electron";
|
||||
import { nameAndExtension } from "@/next/file";
|
||||
import log from "@/next/log";
|
||||
import { wait } from "@/utils/promise";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
import { User } from "@ente/shared/user/types";
|
||||
import { wait } from "@ente/shared/utils";
|
||||
import { getLocalCollections } from "services/collectionService";
|
||||
import downloadManager from "services/download";
|
||||
import { getAllLocalFiles } from "services/fileService";
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type";
|
||||
import {
|
||||
generateImageThumbnailUsingCanvas,
|
||||
generateVideoThumbnailUsingCanvas,
|
||||
} from "@/media/image";
|
||||
import log from "@/next/log";
|
||||
import { type Electron } from "@/next/types/ipc";
|
||||
import { withTimeout } from "@ente/shared/utils";
|
||||
import * as ffmpeg from "services/ffmpeg";
|
||||
import { heicToJPEG } from "services/heic-convert";
|
||||
import { toDataOrPathOrZipEntry, type DesktopUploadItem } from "./types";
|
||||
|
@ -30,10 +33,10 @@ export const generateThumbnailWeb = async (
|
|||
fileTypeInfo: FileTypeInfo,
|
||||
): Promise<Uint8Array> =>
|
||||
fileTypeInfo.fileType === FILE_TYPE.IMAGE
|
||||
? await generateImageThumbnailUsingCanvas(blob, fileTypeInfo)
|
||||
? await generateImageThumbnailWeb(blob, fileTypeInfo)
|
||||
: await generateVideoThumbnailWeb(blob);
|
||||
|
||||
const generateImageThumbnailUsingCanvas = async (
|
||||
const generateImageThumbnailWeb = async (
|
||||
blob: Blob,
|
||||
{ extension }: FileTypeInfo,
|
||||
) => {
|
||||
|
@ -42,35 +45,7 @@ const generateImageThumbnailUsingCanvas = async (
|
|||
blob = await heicToJPEG(blob);
|
||||
}
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvasCtx = canvas.getContext("2d");
|
||||
|
||||
const imageURL = URL.createObjectURL(blob);
|
||||
await withTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.setAttribute("src", imageURL);
|
||||
image.onload = () => {
|
||||
try {
|
||||
URL.revokeObjectURL(imageURL);
|
||||
const { width, height } = scaledThumbnailDimensions(
|
||||
image.width,
|
||||
image.height,
|
||||
maxThumbnailDimension,
|
||||
);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvasCtx.drawImage(image, 0, 0, width, height);
|
||||
resolve(undefined);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}),
|
||||
30 * 1000,
|
||||
);
|
||||
|
||||
return await compressedJPEGData(canvas);
|
||||
return generateImageThumbnailUsingCanvas(blob);
|
||||
};
|
||||
|
||||
const generateVideoThumbnailWeb = async (blob: Blob) => {
|
||||
|
@ -85,92 +60,6 @@ const generateVideoThumbnailWeb = async (blob: Blob) => {
|
|||
}
|
||||
};
|
||||
|
||||
const generateVideoThumbnailUsingCanvas = async (blob: Blob) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvasCtx = canvas.getContext("2d");
|
||||
|
||||
const videoURL = URL.createObjectURL(blob);
|
||||
await withTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
const video = document.createElement("video");
|
||||
video.preload = "metadata";
|
||||
video.src = videoURL;
|
||||
video.addEventListener("loadeddata", () => {
|
||||
try {
|
||||
URL.revokeObjectURL(videoURL);
|
||||
const { width, height } = scaledThumbnailDimensions(
|
||||
video.videoWidth,
|
||||
video.videoHeight,
|
||||
maxThumbnailDimension,
|
||||
);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvasCtx.drawImage(video, 0, 0, width, height);
|
||||
resolve(undefined);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}),
|
||||
30 * 1000,
|
||||
);
|
||||
|
||||
return await compressedJPEGData(canvas);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the size of the thumbnail to create for an image with the given
|
||||
* {@link width} and {@link height}.
|
||||
*
|
||||
* This function calculates a new size of an image for limiting it to maximum
|
||||
* width and height (both specified by {@link maxDimension}), while maintaining
|
||||
* aspect ratio.
|
||||
*
|
||||
* It returns `{0, 0}` for invalid inputs.
|
||||
*/
|
||||
const scaledThumbnailDimensions = (
|
||||
width: number,
|
||||
height: number,
|
||||
maxDimension: number,
|
||||
): { width: number; height: number } => {
|
||||
if (width === 0 || height === 0) return { width: 0, height: 0 };
|
||||
const widthScaleFactor = maxDimension / width;
|
||||
const heightScaleFactor = maxDimension / height;
|
||||
const scaleFactor = Math.min(widthScaleFactor, heightScaleFactor);
|
||||
const thumbnailDimensions = {
|
||||
width: Math.round(width * scaleFactor),
|
||||
height: Math.round(height * scaleFactor),
|
||||
};
|
||||
if (thumbnailDimensions.width === 0 || thumbnailDimensions.height === 0)
|
||||
return { width: 0, height: 0 };
|
||||
return thumbnailDimensions;
|
||||
};
|
||||
|
||||
const compressedJPEGData = async (canvas: HTMLCanvasElement) => {
|
||||
let blob: Blob;
|
||||
let prevSize = Number.MAX_SAFE_INTEGER;
|
||||
let quality = 0.7;
|
||||
|
||||
do {
|
||||
if (blob) prevSize = blob.size;
|
||||
blob = await new Promise((resolve) => {
|
||||
canvas.toBlob((blob) => resolve(blob), "image/jpeg", quality);
|
||||
});
|
||||
quality -= 0.1;
|
||||
} while (
|
||||
quality >= 0.5 &&
|
||||
blob.size > maxThumbnailSize &&
|
||||
percentageSizeDiff(blob.size, prevSize) >= 10
|
||||
);
|
||||
|
||||
return new Uint8Array(await blob.arrayBuffer());
|
||||
};
|
||||
|
||||
const percentageSizeDiff = (
|
||||
newThumbnailSize: number,
|
||||
oldThumbnailSize: number,
|
||||
) => ((oldThumbnailSize - newThumbnailSize) * 100) / oldThumbnailSize;
|
||||
|
||||
/**
|
||||
* Generate a JPEG thumbnail for the given file or path using native tools.
|
||||
*
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import log from "@/next/log";
|
||||
import { wait } from "@/utils/promise";
|
||||
import { CustomError, handleUploadError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint, getUploadEndpoint } from "@ente/shared/network/api";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { wait } from "@ente/shared/utils";
|
||||
import { EnteFile } from "types/file";
|
||||
import { MultipartUploadURLs, UploadFile, UploadURL } from "./uploadService";
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ import log from "@/next/log";
|
|||
import type { Electron } from "@/next/types/ipc";
|
||||
import { ComlinkWorker } from "@/next/worker/comlink-worker";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { wait } from "@/utils/promise";
|
||||
import { getDedicatedCryptoWorker } from "@ente/shared/crypto";
|
||||
import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { Events, eventBus } from "@ente/shared/events";
|
||||
import { wait } from "@ente/shared/utils";
|
||||
import { Canceler } from "axios";
|
||||
import { Remote } from "comlink";
|
||||
import {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { wait } from "@/utils/promise";
|
||||
import { changeEmail, sendOTTForEmailChange } from "@ente/accounts/api/user";
|
||||
import { APP_HOMES } from "@ente/shared/apps/constants";
|
||||
import { PageProps } from "@ente/shared/apps/types";
|
||||
|
@ -6,7 +7,6 @@ import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer";
|
|||
import LinkButton from "@ente/shared/components/LinkButton";
|
||||
import SubmitButton from "@ente/shared/components/SubmitButton";
|
||||
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
||||
import { wait } from "@ente/shared/utils";
|
||||
import { Alert, Box, TextField } from "@mui/material";
|
||||
import { Formik, FormikHelpers } from "formik";
|
||||
import { t } from "i18next";
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { Formik, FormikHelpers } from "formik";
|
||||
import { t } from "i18next";
|
||||
import { useRef, useState } from "react";
|
||||
import OtpInput from "react-otp-input";
|
||||
|
||||
import { wait } from "@/utils/promise";
|
||||
import InvalidInputMessage from "@ente/accounts/components/two-factor/InvalidInputMessage";
|
||||
import {
|
||||
CenteredFlex,
|
||||
VerticallyCentered,
|
||||
} from "@ente/shared/components/Container";
|
||||
import SubmitButton from "@ente/shared/components/SubmitButton";
|
||||
import { wait } from "@ente/shared/utils";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Formik, FormikHelpers } from "formik";
|
||||
import { t } from "i18next";
|
||||
import { useRef, useState } from "react";
|
||||
import OtpInput from "react-otp-input";
|
||||
|
||||
interface formValues {
|
||||
otp: string;
|
||||
|
|
128
web/packages/media/image.ts
Normal file
128
web/packages/media/image.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import { ensure } from "@/utils/ensure";
|
||||
import { withTimeout } from "@/utils/promise";
|
||||
|
||||
/** Maximum width or height of the generated thumbnail */
|
||||
const maxThumbnailDimension = 720;
|
||||
/** Maximum size (in bytes) of the generated thumbnail */
|
||||
const maxThumbnailSize = 100 * 1024; // 100 KB
|
||||
|
||||
export const generateImageThumbnailUsingCanvas = async (blob: Blob) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvasCtx = ensure(canvas.getContext("2d"));
|
||||
|
||||
const imageURL = URL.createObjectURL(blob);
|
||||
await withTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.setAttribute("src", imageURL);
|
||||
image.onload = () => {
|
||||
try {
|
||||
URL.revokeObjectURL(imageURL);
|
||||
const { width, height } = scaledThumbnailDimensions(
|
||||
image.width,
|
||||
image.height,
|
||||
maxThumbnailDimension,
|
||||
);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvasCtx.drawImage(image, 0, 0, width, height);
|
||||
resolve(undefined);
|
||||
} catch (e: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}),
|
||||
30 * 1000,
|
||||
);
|
||||
|
||||
return await compressedJPEGData(canvas);
|
||||
};
|
||||
|
||||
export const generateVideoThumbnailUsingCanvas = async (blob: Blob) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvasCtx = ensure(canvas.getContext("2d"));
|
||||
|
||||
const videoURL = URL.createObjectURL(blob);
|
||||
await withTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
const video = document.createElement("video");
|
||||
video.preload = "metadata";
|
||||
video.src = videoURL;
|
||||
video.addEventListener("loadeddata", () => {
|
||||
try {
|
||||
URL.revokeObjectURL(videoURL);
|
||||
const { width, height } = scaledThumbnailDimensions(
|
||||
video.videoWidth,
|
||||
video.videoHeight,
|
||||
maxThumbnailDimension,
|
||||
);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvasCtx.drawImage(video, 0, 0, width, height);
|
||||
resolve(undefined);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}),
|
||||
30 * 1000,
|
||||
);
|
||||
|
||||
return await compressedJPEGData(canvas);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the size of the thumbnail to create for an image with the given
|
||||
* {@link width} and {@link height}.
|
||||
*
|
||||
* This function calculates a new size of an image for limiting it to maximum
|
||||
* width and height (both specified by {@link maxDimension}), while maintaining
|
||||
* aspect ratio.
|
||||
*
|
||||
* It returns `{0, 0}` for invalid inputs.
|
||||
*/
|
||||
const scaledThumbnailDimensions = (
|
||||
width: number,
|
||||
height: number,
|
||||
maxDimension: number,
|
||||
): { width: number; height: number } => {
|
||||
if (width === 0 || height === 0) return { width: 0, height: 0 };
|
||||
const widthScaleFactor = maxDimension / width;
|
||||
const heightScaleFactor = maxDimension / height;
|
||||
const scaleFactor = Math.min(widthScaleFactor, heightScaleFactor);
|
||||
const thumbnailDimensions = {
|
||||
width: Math.round(width * scaleFactor),
|
||||
height: Math.round(height * scaleFactor),
|
||||
};
|
||||
if (thumbnailDimensions.width === 0 || thumbnailDimensions.height === 0)
|
||||
return { width: 0, height: 0 };
|
||||
return thumbnailDimensions;
|
||||
};
|
||||
|
||||
const compressedJPEGData = async (canvas: HTMLCanvasElement) => {
|
||||
let blob: Blob | undefined | null;
|
||||
let prevSize = Number.MAX_SAFE_INTEGER;
|
||||
let quality = 0.7;
|
||||
|
||||
do {
|
||||
if (blob) prevSize = blob.size;
|
||||
blob = await new Promise((resolve) => {
|
||||
canvas.toBlob((blob) => resolve(blob), "image/jpeg", quality);
|
||||
});
|
||||
quality -= 0.1;
|
||||
} while (
|
||||
quality >= 0.5 &&
|
||||
blob &&
|
||||
blob.size > maxThumbnailSize &&
|
||||
percentageSizeDiff(blob.size, prevSize) >= 10
|
||||
);
|
||||
|
||||
return new Uint8Array(await ensure(blob).arrayBuffer());
|
||||
};
|
||||
|
||||
const percentageSizeDiff = (
|
||||
newThumbnailSize: number,
|
||||
oldThumbnailSize: number,
|
||||
) => ((oldThumbnailSize - newThumbnailSize) * 100) / oldThumbnailSize;
|
|
@ -1,11 +1,4 @@
|
|||
/**
|
||||
* Wait for {@link ms} milliseconds
|
||||
*
|
||||
* This function is a promisified `setTimeout`. It returns a promise that
|
||||
* resolves after {@link ms} milliseconds.
|
||||
*/
|
||||
export const wait = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
import { wait } from "@/utils/promise";
|
||||
|
||||
export function downloadAsFile(filename: string, content: string) {
|
||||
const file = new Blob([content], {
|
||||
|
@ -52,23 +45,3 @@ export async function retryAsyncFunction<T>(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Await the given {@link promise} for {@link timeoutMS} milliseconds. If it
|
||||
* does not resolve within {@link timeoutMS}, then reject with a timeout error.
|
||||
*/
|
||||
export const withTimeout = async <T>(promise: Promise<T>, ms: number) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
const rejectOnTimeout = new Promise<T>((_, reject) => {
|
||||
timeoutId = setTimeout(
|
||||
() => reject(new Error("Operation timed out")),
|
||||
ms,
|
||||
);
|
||||
});
|
||||
const promiseAndCancelTimeout = async () => {
|
||||
const result = await promise;
|
||||
clearTimeout(timeoutId);
|
||||
return result;
|
||||
};
|
||||
return Promise.race([promiseAndCancelTimeout(), rejectOnTimeout]);
|
||||
};
|
||||
|
|
28
web/packages/utils/promise.ts
Normal file
28
web/packages/utils/promise.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Wait for {@link ms} milliseconds
|
||||
*
|
||||
* This function is a promisified `setTimeout`. It returns a promise that
|
||||
* resolves after {@link ms} milliseconds.
|
||||
*/
|
||||
export const wait = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Await the given {@link promise} for {@link timeoutMS} milliseconds. If it
|
||||
* does not resolve within {@link timeoutMS}, then reject with a timeout error.
|
||||
*/
|
||||
export const withTimeout = async <T>(promise: Promise<T>, ms: number) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
const rejectOnTimeout = new Promise<T>((_, reject) => {
|
||||
timeoutId = setTimeout(
|
||||
() => reject(new Error("Operation timed out")),
|
||||
ms,
|
||||
);
|
||||
});
|
||||
const promiseAndCancelTimeout = async () => {
|
||||
const result = await promise;
|
||||
clearTimeout(timeoutId);
|
||||
return result;
|
||||
};
|
||||
return Promise.race([promiseAndCancelTimeout(), rejectOnTimeout]);
|
||||
};
|
Loading…
Add table
Reference in a new issue