diff --git a/web/packages/gallery/services/ffmpeg/index.ts b/web/packages/gallery/services/ffmpeg/index.ts index 26dddb7f4b..5be1be642e 100644 --- a/web/packages/gallery/services/ffmpeg/index.ts +++ b/web/packages/gallery/services/ffmpeg/index.ts @@ -280,7 +280,7 @@ const convertToMP4Native = async (electron: Electron, blob: Blob) => { * * @param blob The input video blob. * - * @returns The data of the generated preview variant. + * @returns The output video blob containing the generated preview variant. */ export const generateVideoPreviewVariantWeb = async (blob: Blob) => ffmpegExecWeb(generateVideoPreviewMetadataCommand, blob, "mp4"); @@ -290,38 +290,29 @@ export const generateVideoPreviewVariantWeb = async (blob: Blob) => * * Current parameters * + * - H264 * - 720p width * - 2000kbps bitrate * - 30fps frame rate - * - * See: [Note: Preview variant of videos] - * - * Options: - * - * - `-vf` creates a filter graph for the video stream - * - * - `-vf scale=720:-1` scales the video to 720p width, keeping aspect ratio. - * - * - `-r` sets the frame rate. - * - * - `-c:v libx264` sets the codec for the video stream to H264. - * - * - `-b:v 2000k` sets the bitrate for the video stream. - * - * - `-c:a aac -b:a 128k` converts the audio stream to 128k bit AAC. */ const generateVideoPreviewMetadataCommand = [ ffmpegPathPlaceholder, "-i", inputPathPlaceholder, + // `-vf` creates a filter graph for the video stream. "-vf", + // `-vf scale=720:-1` scales the video to 720p width, keeping aspect ratio. "scale=720:-1", + // `-r 30` sets the frame rate to 30 fps. "-r", "30", + // `-c:v libx264` sets the codec for the video stream to H264. "-c:v", "libx264", + // `-b:v 2000k` sets the bitrate for the video stream. "-b:v", "2000k", + // `-c:a aac -b:a 128k` converts the audio stream to 128k bit AAC. "-c:a", "aac", "-b:a", diff --git a/web/packages/gallery/services/preview.ts b/web/packages/gallery/services/preview.ts deleted file mode 100644 index 1b95507b52..0000000000 --- a/web/packages/gallery/services/preview.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { EnteFile } from "ente-media/file"; -import { FileType } from "ente-media/file-type"; -import { downloadManager } from "./download"; -import { generateVideoPreviewVariantWeb } from "./ffmpeg"; - -// TODO(HLS): Move me to video.ts -/** - * Create a preview variant of the given video {@link file}. - * - * [Note: Preview variant of videos] - * - * A preview variant of a video is created by transcoding it into a smaller, - * streamable and more standard format. - * - * The video is transcoded into a format that is both smaller but is also using - * a much more widely supported codec etc so that it can be played back readily - * across browsers and OSes independent of the codec used by the source video. - * - * We also use a format that can be streamed back by the client instead of - * needing to download it all at once. - * - * @param file The {@link EnteFile} of type video for which we want to create a - * preview variant. - */ -export const createVideoPreviewVariant = async (file: EnteFile) => { - if (file.metadata.fileType != FileType.video) - throw new Error("Preview variant can only be created for video files"); - - const fileBlob = await downloadManager.fileBlob(file); - const previewFileData = await generateVideoPreviewVariantWeb(fileBlob); - // Unrevoked currently. - const previewFileURL = URL.createObjectURL(new Blob([previewFileData])); - return previewFileURL; -}; diff --git a/web/packages/gallery/services/video.ts b/web/packages/gallery/services/video.ts index 29b4c62e77..d1a8b04607 100644 --- a/web/packages/gallery/services/video.ts +++ b/web/packages/gallery/services/video.ts @@ -9,6 +9,8 @@ import { settingsSnapshot } from "ente-new/photos/services/settings"; import { gunzip } from "ente-new/photos/utils/gzip"; import { ensurePrecondition } from "ente-utils/ensure"; import { z } from "zod"; +import { downloadManager } from "./download"; +import { generateVideoPreviewVariantWeb } from "./ffmpeg"; import { fetchFileData, fetchFilePreviewData } from "./file-data"; import type { UploadItem } from "./upload"; @@ -277,10 +279,10 @@ export const processVideoNewUpload = ( return; } - // Enqueue. + // Enqueue the item. _state.videoProcessingQueue.push({ file, uploadItem }); - // Tickle. + // Tickle the processor if it isn't already running. _state.queueProcessor ??= processQueue(); }; @@ -305,11 +307,88 @@ const processQueue = async () => { _state.queueProcessor = undefined; }; +/** + * Generate and upload a streamable variant of the given {@link EnteFile}. + * + * [Note: Preview variant of videos] + * + * A preview variant of a video is created by transcoding it into a smaller, + * streamable, and (more) widely supported format. + * + * 1. The video is transcoded into a format that is both smaller but is also + * using a much more widely supported codec so that it can be played back + * readily across browsers and OSes independent of the codec used by the + * source video. + * + * 2. We use a format that can be streamed back by the client instead of needing + * to download it all at once, and also generate an HLS playlist that refers + * to the offsets in the generated video file. + * + * 3. Both the generated video and the HLS playlist are then uploaded, E2EE. + */ const processQueueItem = async ({ file, uploadItem, }: VideoProcessingQueueItem) => { log.debug(() => ["gen-hls", { file, uploadItem }]); + const fileBlob = await fetchOriginalVideoBlob(file, uploadItem); + const previewFileData = await generateVideoPreviewVariantWeb(fileBlob); + + console.log(previewFileData); + await Promise.resolve(0); }; + +/** + * Return a blob containing the contents of the given video file. + * + * The blob is either constructed using the given {@link uploadItem} if present, + * otherwise it is downloaded from remote. + * + * @param file An {@link EnteFile} of type {@link FileType.video}. + * + * @param uploadItem If we're called during the upload process, then this will + * be set to the {@link UploadItem} that was uploaded. This way, we can directly + * use the on-disk file instead of needing to download the original from remote. + */ +const fetchOriginalVideoBlob = async ( + file: EnteFile, + uploadItem: UploadItem | undefined, +): Promise => + uploadItem + ? fetchOriginalVideoUploadItemBlob(file, uploadItem) + : await downloadManager.fileBlob(file); + +const fetchOriginalVideoUploadItemBlob = ( + _: EnteFile, + uploadItem: UploadItem, +) => { + // TODO(HLS): Commented below is the implementation that the eventual + // desktop only conversion would need to handle - the conversion logic would + // need to move to the desktop side to allow it to handle large videos. + // + // Meanwhile during development, we assume we're on the happy web-only cases + // (dragging and dropping a file). All this code is behind a development + // feature flag, so it is not going to impact end users. + + if (typeof uploadItem == "string" || Array.isArray(uploadItem)) { + throw new Error("Not implemented"); + // const { response, lastModifiedMs } = await readStream( + // ensureElectron(), + // uploadItem, + // ); + // const path = typeof uploadItem == "string" ? uploadItem : uploadItem[1]; + // // This function will not be called for videos, and for images + // // it is reasonable to read the entire stream into memory here. + // return new File([await response.arrayBuffer()], basename(path), { + // lastModified: lastModifiedMs, + // }); + } else { + if (uploadItem instanceof File) { + return uploadItem; + } else { + return uploadItem.file; + } + } +};