diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index a9909f626..4c96295f5 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -279,21 +279,6 @@ const getDirFiles = (dirPath: string): Promise => * * --- * - * [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 diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index 7bb2f1fab..3fa375eab 100644 --- a/desktop/src/types/ipc.ts +++ b/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", }; /** diff --git a/web/apps/photos/src/services/download/index.ts b/web/apps/photos/src/services/download/index.ts index 41af5c055..81aae1bf5 100644 --- a/web/apps/photos/src/services/download/index.ts +++ b/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 { + 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 { + 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; + } +} diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index cc3ddc5e1..17068c449 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/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 { - 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 { - 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; -}; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 7e51f407b..80ea174a5 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -447,6 +447,26 @@ export interface Electron { getDirFiles: (dirPath: string) => Promise; } +/** + * 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. */