This commit is contained in:
Manav Rathi 2024-04-20 11:49:45 +05:30
parent 13542c1511
commit 6337ffc203
No known key found for this signature in database
5 changed files with 188 additions and 185 deletions

View file

@ -279,21 +279,6 @@ const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
*
* ---
*
* [Note: Custom errors across Electron/Renderer boundary]
*
* If we need to identify errors thrown by the main process when invoked from
* the renderer process, we can only use the `message` field because:
*
* > Errors thrown throw `handle` in the main process are not transparent as
* > they are serialized and only the `message` property from the original error
* > is provided to the renderer process.
* >
* > - https://www.electronjs.org/docs/latest/tutorial/ipc
* >
* > Ref: https://github.com/electron/electron/issues/24427
*
* ---
*
* [Note: Transferring large amount of data over IPC]
*
* Electron's IPC implementation uses the HTML standard Structured Clone

View file

@ -32,11 +32,13 @@ export interface PendingUploads {
}
/**
* Errors that have special semantics on the web side.
* See: [Note: Custom errors across Electron/Renderer boundary]
*
* Note: this is not a type, and cannot be used in preload.js; it is only meant
* for use in the main process code.
*/
export const CustomErrors = {
WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
"Windows native image processing is not supported",
export const CustomErrorMessage = {
NotAvailable: "This feature in not available on the current OS/arch",
};
/**

View file

@ -1,3 +1,4 @@
import { decodeLivePhoto } from "@/media/live-photo";
import { openCache, type BlobCache } from "@/next/blob-cache";
import log from "@/next/log";
import { APPS } from "@ente/shared/apps/constants";
@ -5,13 +6,13 @@ import ComlinkCryptoWorker 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 { isPlaybackPossible } from "@ente/shared/media/video-playback";
import { Remote } from "comlink";
import { FILE_TYPE } from "constants/file";
import isElectron from "is-electron";
import * as ffmpegService from "services/ffmpeg/ffmpegService";
import { EnteFile } from "types/file";
import {
generateStreamFromArrayBuffer,
getRenderableFileURL,
} from "utils/file";
import { generateStreamFromArrayBuffer, getRenderableImage } from "utils/file";
import { PhotosDownloadClient } from "./clients/photos";
import { PublicAlbumsDownloadClient } from "./clients/publicAlbums";
@ -467,3 +468,159 @@ function createDownloadClient(
return new PhotosDownloadClient(token, timeout);
}
}
async function getRenderableFileURL(
file: EnteFile,
fileBlob: Blob,
originalFileURL: string,
forceConvert: boolean,
): Promise<SourceURLs> {
let srcURLs: SourceURLs["url"];
switch (file.metadata.fileType) {
case FILE_TYPE.IMAGE: {
const convertedBlob = await getRenderableImage(
file.metadata.title,
fileBlob,
);
const convertedURL = getFileObjectURL(
originalFileURL,
fileBlob,
convertedBlob,
);
srcURLs = convertedURL;
break;
}
case FILE_TYPE.LIVE_PHOTO: {
srcURLs = await getRenderableLivePhotoURL(
file,
fileBlob,
forceConvert,
);
break;
}
case FILE_TYPE.VIDEO: {
const convertedBlob = await getPlayableVideo(
file.metadata.title,
fileBlob,
forceConvert,
);
const convertedURL = getFileObjectURL(
originalFileURL,
fileBlob,
convertedBlob,
);
srcURLs = convertedURL;
break;
}
default: {
srcURLs = originalFileURL;
break;
}
}
let isOriginal: boolean;
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
isOriginal = false;
} else {
isOriginal = (srcURLs as string) === (originalFileURL as string);
}
return {
url: srcURLs,
isOriginal,
isRenderable:
file.metadata.fileType !== FILE_TYPE.LIVE_PHOTO && !!srcURLs,
type:
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
? "livePhoto"
: "normal",
};
}
const getFileObjectURL = (
originalFileURL: string,
originalBlob: Blob,
convertedBlob: Blob,
) => {
const convertedURL = convertedBlob
? convertedBlob === originalBlob
? originalFileURL
: URL.createObjectURL(convertedBlob)
: null;
return convertedURL;
};
async function getRenderableLivePhotoURL(
file: EnteFile,
fileBlob: Blob,
forceConvert: boolean,
): Promise<LivePhotoSourceURL> {
const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob);
const getRenderableLivePhotoImageURL = async () => {
try {
const imageBlob = new Blob([livePhoto.imageData]);
const convertedImageBlob = await getRenderableImage(
livePhoto.imageFileName,
imageBlob,
);
return URL.createObjectURL(convertedImageBlob);
} catch (e) {
//ignore and return null
return null;
}
};
const getRenderableLivePhotoVideoURL = async () => {
try {
const videoBlob = new Blob([livePhoto.videoData]);
const convertedVideoBlob = await getPlayableVideo(
livePhoto.videoFileName,
videoBlob,
forceConvert,
true,
);
return URL.createObjectURL(convertedVideoBlob);
} catch (e) {
//ignore and return null
return null;
}
};
return {
image: getRenderableLivePhotoImageURL,
video: getRenderableLivePhotoVideoURL,
};
}
async function getPlayableVideo(
videoNameTitle: string,
videoBlob: Blob,
forceConvert = false,
runOnWeb = false,
) {
try {
const isPlayable = await isPlaybackPossible(
URL.createObjectURL(videoBlob),
);
if (isPlayable && !forceConvert) {
return videoBlob;
} else {
if (!forceConvert && !runOnWeb && !isElectron()) {
return null;
}
log.info(
`video format not supported, converting it name: ${videoNameTitle}`,
);
const mp4ConvertedVideo = await ffmpegService.convertToMP4(
new File([videoBlob], videoNameTitle),
);
log.info(`video successfully converted ${videoNameTitle}`);
return new Blob([await mp4ConvertedVideo.arrayBuffer()]);
}
} catch (e) {
log.error("video conversion failed", e);
return null;
}
}

View file

@ -5,7 +5,6 @@ import type { Electron } from "@/next/types/ipc";
import { workerBridge } from "@/next/worker/worker-bridge";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { CustomError } from "@ente/shared/error";
import { isPlaybackPossible } from "@ente/shared/media/video-playback";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { User } from "@ente/shared/user/types";
import { downloadUsingAnchor } from "@ente/shared/utils";
@ -21,11 +20,7 @@ import {
import { t } from "i18next";
import isElectron from "is-electron";
import { moveToHiddenCollection } from "services/collectionService";
import DownloadManager, {
LivePhotoSourceURL,
SourceURLs,
} from "services/download";
import * as ffmpegService from "services/ffmpeg/ffmpegService";
import DownloadManager from "services/download";
import {
deleteFromTrash,
trashFiles,
@ -271,149 +266,6 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) {
});
}
export async function getRenderableFileURL(
file: EnteFile,
fileBlob: Blob,
originalFileURL: string,
forceConvert: boolean,
): Promise<SourceURLs> {
let srcURLs: SourceURLs["url"];
switch (file.metadata.fileType) {
case FILE_TYPE.IMAGE: {
const convertedBlob = await getRenderableImage(
file.metadata.title,
fileBlob,
);
const convertedURL = getFileObjectURL(
originalFileURL,
fileBlob,
convertedBlob,
);
srcURLs = convertedURL;
break;
}
case FILE_TYPE.LIVE_PHOTO: {
srcURLs = await getRenderableLivePhotoURL(
file,
fileBlob,
forceConvert,
);
break;
}
case FILE_TYPE.VIDEO: {
const convertedBlob = await getPlayableVideo(
file.metadata.title,
fileBlob,
forceConvert,
);
const convertedURL = getFileObjectURL(
originalFileURL,
fileBlob,
convertedBlob,
);
srcURLs = convertedURL;
break;
}
default: {
srcURLs = originalFileURL;
break;
}
}
let isOriginal: boolean;
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
isOriginal = false;
} else {
isOriginal = (srcURLs as string) === (originalFileURL as string);
}
return {
url: srcURLs,
isOriginal,
isRenderable:
file.metadata.fileType !== FILE_TYPE.LIVE_PHOTO && !!srcURLs,
type:
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
? "livePhoto"
: "normal",
};
}
async function getRenderableLivePhotoURL(
file: EnteFile,
fileBlob: Blob,
forceConvert: boolean,
): Promise<LivePhotoSourceURL> {
const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob);
const getRenderableLivePhotoImageURL = async () => {
try {
const imageBlob = new Blob([livePhoto.imageData]);
const convertedImageBlob = await getRenderableImage(
livePhoto.imageFileName,
imageBlob,
);
return URL.createObjectURL(convertedImageBlob);
} catch (e) {
//ignore and return null
return null;
}
};
const getRenderableLivePhotoVideoURL = async () => {
try {
const videoBlob = new Blob([livePhoto.videoData]);
const convertedVideoBlob = await getPlayableVideo(
livePhoto.videoFileName,
videoBlob,
forceConvert,
true,
);
return URL.createObjectURL(convertedVideoBlob);
} catch (e) {
//ignore and return null
return null;
}
};
return {
image: getRenderableLivePhotoImageURL,
video: getRenderableLivePhotoVideoURL,
};
}
export async function getPlayableVideo(
videoNameTitle: string,
videoBlob: Blob,
forceConvert = false,
runOnWeb = false,
) {
try {
const isPlayable = await isPlaybackPossible(
URL.createObjectURL(videoBlob),
);
if (isPlayable && !forceConvert) {
return videoBlob;
} else {
if (!forceConvert && !runOnWeb && !isElectron()) {
return null;
}
log.info(
`video format not supported, converting it name: ${videoNameTitle}`,
);
const mp4ConvertedVideo = await ffmpegService.convertToMP4(
new File([videoBlob], videoNameTitle),
);
log.info(`video successfully converted ${videoNameTitle}`);
return new Blob([await mp4ConvertedVideo.arrayBuffer()]);
}
} catch (e) {
log.error("video conversion failed", e);
return null;
}
}
export async function getRenderableImage(fileName: string, imageBlob: Blob) {
let fileTypeInfo: FileTypeInfo;
try {
@ -1061,16 +913,3 @@ const fixTimeHelper = async (
) => {
setFixCreationTimeAttributes({ files: selectedFiles });
};
const getFileObjectURL = (
originalFileURL: string,
originalBlob: Blob,
convertedBlob: Blob,
) => {
const convertedURL = convertedBlob
? convertedBlob === originalBlob
? originalFileURL
: URL.createObjectURL(convertedBlob)
: null;
return convertedURL;
};

View file

@ -447,6 +447,26 @@ export interface Electron {
getDirFiles: (dirPath: string) => Promise<ElectronFile[]>;
}
/**
* Errors that have special semantics on the web side.
*
* [Note: Custom errors across Electron/Renderer boundary]
*
* If we need to identify errors thrown by the main process when invoked from
* the renderer process, we can only use the `message` field because:
*
* > Errors thrown throw `handle` in the main process are not transparent as
* > they are serialized and only the `message` property from the original error
* > is provided to the renderer process.
* >
* > - https://www.electronjs.org/docs/latest/tutorial/ipc
* >
* > Ref: https://github.com/electron/electron/issues/24427
*/
export const CustomErrorMessage = {
NotAvailable: "This feature in not available on the current OS/arch",
};
/**
* Data passed across the IPC bridge when an app update is available.
*/