Inline
This commit is contained in:
parent
13542c1511
commit
6337ffc203
5 changed files with 188 additions and 185 deletions
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue