Manav Rathi 1 year ago
parent
commit
6337ffc203

+ 0 - 15
desktop/src/preload.ts

@@ -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

+ 6 - 4
desktop/src/types/ipc.ts

@@ -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",
 };
 
 /**

+ 161 - 4
web/apps/photos/src/services/download/index.ts

@@ -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;
+    }
+}

+ 1 - 162
web/apps/photos/src/utils/file/index.ts

@@ -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;
-};

+ 20 - 0
web/packages/next/types/ipc.ts

@@ -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.
  */