[cast] Fix live photo handling, precache (#1465)

- Fix live photo cast (previously these were not being rendered because
of an incorrect conditional)
- Precache

There is still an initial delay where the missing thumbnail is shown on
switching to the next image on first load. But anyways, these are
improvements, can tackle that later.
This commit is contained in:
Manav Rathi 2024-04-16 21:06:30 +05:30 committed by GitHub
commit 486044ad51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 72 additions and 320 deletions

View file

@ -1,50 +1,24 @@
import { SlideshowContext } from "pages/slideshow";
import { useContext, useEffect, useState } from "react";
import { useEffect } from "react";
export default function PhotoAuditorium({
url,
nextSlideUrl,
}: {
interface PhotoAuditoriumProps {
url: string;
nextSlideUrl: string;
}) {
const { showNextSlide } = useContext(SlideshowContext);
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
showNextSlide: () => void;
}
export const PhotoAuditorium: React.FC<PhotoAuditoriumProps> = ({
url,
nextSlideUrl,
showNextSlide,
}) => {
useEffect(() => {
let timeout: NodeJS.Timeout;
let timeout2: NodeJS.Timeout;
if (nextSlidePrerendered) {
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
const delayTime = Math.max(10000 - elapsedTime, 0);
if (elapsedTime >= 10000) {
setShowPreloadedNextSlide(true);
} else {
timeout = setTimeout(() => {
setShowPreloadedNextSlide(true);
}, delayTime);
}
if (showNextSlide) {
timeout2 = setTimeout(() => {
showNextSlide();
setNextSlidePrerendered(false);
setPrerenderTime(null);
setShowPreloadedNextSlide(false);
}, delayTime);
}
}
const timeoutId = window.setTimeout(() => {
showNextSlide();
}, 10000);
return () => {
if (timeout) clearTimeout(timeout);
if (timeout2) clearTimeout(timeout2);
if (timeoutId) clearTimeout(timeoutId);
};
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
}, [showNextSlide]);
return (
<div
@ -69,27 +43,22 @@ export default function PhotoAuditorium({
backdropFilter: "blur(10px)",
}}
>
<img
src={url}
style={{
maxWidth: "100%",
maxHeight: "100%",
display: showPreloadedNextSlide ? "none" : "block",
}}
/>
<img
src={nextSlideUrl}
style={{
maxWidth: "100%",
maxHeight: "100%",
display: showPreloadedNextSlide ? "block" : "none",
display: "none",
}}
onLoad={() => {
setNextSlidePrerendered(true);
setPrerenderTime(Date.now());
/>
<img
src={url}
style={{
maxWidth: "100%",
maxHeight: "100%",
}}
/>
</div>
</div>
);
}
};

View file

@ -1,95 +0,0 @@
import { SlideshowContext } from "pages/slideshow";
import { useContext, useEffect, useState } from "react";
export default function PhotoAuditorium({
url,
nextSlideUrl,
}: {
url: string;
nextSlideUrl: string;
}) {
const { showNextSlide } = useContext(SlideshowContext);
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
useEffect(() => {
let timeout: NodeJS.Timeout;
let timeout2: NodeJS.Timeout;
if (nextSlidePrerendered) {
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
const delayTime = Math.max(10000 - elapsedTime, 0);
if (elapsedTime >= 10000) {
setShowPreloadedNextSlide(true);
} else {
timeout = setTimeout(() => {
setShowPreloadedNextSlide(true);
}, delayTime);
}
if (showNextSlide) {
timeout2 = setTimeout(() => {
showNextSlide();
setNextSlidePrerendered(false);
setPrerenderTime(null);
setShowPreloadedNextSlide(false);
}, delayTime);
}
}
return () => {
if (timeout) clearTimeout(timeout);
if (timeout2) clearTimeout(timeout2);
};
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
return (
<div
style={{
width: "100vw",
height: "100vh",
backgroundImage: `url(${url})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundBlendMode: "multiply",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
>
<div
style={{
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backdropFilter: "blur(10px)",
}}
>
<img
src={url}
style={{
maxWidth: "100%",
maxHeight: "100%",
display: showPreloadedNextSlide ? "none" : "block",
}}
/>
<img
src={nextSlideUrl}
style={{
maxWidth: "100%",
maxHeight: "100%",
display: showPreloadedNextSlide ? "block" : "none",
}}
onLoad={() => {
setNextSlidePrerendered(true);
setPrerenderTime(Date.now());
}}
/>
</div>
</div>
);
}

View file

@ -1,55 +0,0 @@
import mime from "mime-types";
import { SlideshowContext } from "pages/slideshow";
import { useContext, useEffect, useRef } from "react";
export default function VideoAuditorium({
name,
url,
}: {
name: string;
url: string;
}) {
const { showNextSlide } = useContext(SlideshowContext);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
attemptPlay();
}, [url, videoRef]);
const attemptPlay = async () => {
if (videoRef.current) {
try {
await videoRef.current.play();
} catch {
showNextSlide();
}
}
};
return (
<div
style={{
width: "100vw",
height: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<video
ref={videoRef}
autoPlay
controls
style={{
maxWidth: "100vw",
maxHeight: "100vh",
}}
onError={showNextSlide}
onEnded={showNextSlide}
>
<source src={url} type={mime.lookup(name)} />
</video>
</div>
);
}

View file

@ -1,30 +0,0 @@
import { FILE_TYPE } from "constants/file";
import PhotoAuditorium from "./PhotoAuditorium";
// import VideoAuditorium from './VideoAuditorium';
interface fileProp {
fileName: string;
fileURL: string;
type: FILE_TYPE;
}
interface IProps {
file1: fileProp;
file2: fileProp;
}
export default function Theatre(props: IProps) {
switch (props.file1.type && props.file2.type) {
case FILE_TYPE.IMAGE:
return (
<PhotoAuditorium
url={props.file1.fileURL}
nextSlideUrl={props.file2.fileURL}
/>
);
// case FILE_TYPE.VIDEO:
// return (
// <VideoAuditorium name={props.fileName} url={props.fileURL} />
// );
}
}

View file

@ -1,9 +1,9 @@
import log from "@/next/log";
import PairedSuccessfullyOverlay from "components/PairedSuccessfullyOverlay";
import Theatre from "components/Theatre";
import { PhotoAuditorium } from "components/PhotoAuditorium";
import { FILE_TYPE } from "constants/file";
import { useRouter } from "next/router";
import { createContext, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import {
getCastCollection,
getLocalFiles,
@ -13,25 +13,20 @@ import { Collection } from "types/collection";
import { EnteFile } from "types/file";
import { getPreviewableImage, isRawFileFromFileName } from "utils/file";
export const SlideshowContext = createContext<{
showNextSlide: () => void;
}>(null);
const renderableFileURLCache = new Map<number, string>();
export default function Slideshow() {
const [collectionFiles, setCollectionFiles] = useState<EnteFile[]>([]);
const [currentFile, setCurrentFile] = useState<EnteFile | undefined>(
undefined,
);
const [nextFile, setNextFile] = useState<EnteFile | undefined>(undefined);
const [loading, setLoading] = useState(true);
const [castToken, setCastToken] = useState<string>("");
const [castCollection, setCastCollection] = useState<
Collection | undefined
>(undefined);
>();
const [collectionFiles, setCollectionFiles] = useState<EnteFile[]>([]);
const [currentFileId, setCurrentFileId] = useState<number | undefined>();
const [currentFileURL, setCurrentFileURL] = useState<string | undefined>();
const [nextFileURL, setNextFileURL] = useState<string | undefined>();
const router = useRouter();
const syncCastFiles = async (token: string) => {
try {
@ -72,29 +67,16 @@ export default function Slideshow() {
const isFileEligibleForCast = (file: EnteFile) => {
const fileType = file.metadata.fileType;
if (fileType !== FILE_TYPE.IMAGE && fileType !== FILE_TYPE.LIVE_PHOTO) {
if (fileType !== FILE_TYPE.IMAGE && fileType !== FILE_TYPE.LIVE_PHOTO)
return false;
}
const fileSizeLimit = 100 * 1024 * 1024;
if (file.info.fileSize > 100 * 1024 * 1024) return false;
if (file.info.fileSize > fileSizeLimit) {
return false;
}
const name = file.metadata.title;
if (fileType === FILE_TYPE.IMAGE) {
if (isRawFileFromFileName(name)) {
return false;
}
}
if (isRawFileFromFileName(file.metadata.title)) return false;
return true;
};
const router = useRouter();
useEffect(() => {
try {
const castToken = window.localStorage.getItem("castToken");
@ -117,9 +99,9 @@ export default function Slideshow() {
showNextSlide();
}, [collectionFiles]);
const showNextSlide = () => {
const showNextSlide = async () => {
const currentIndex = collectionFiles.findIndex(
(file) => file.id === currentFile?.id,
(file) => file.id === currentFileId,
);
const nextIndex = (currentIndex + 1) % collectionFiles.length;
@ -128,63 +110,44 @@ export default function Slideshow() {
const nextFile = collectionFiles[nextIndex];
const nextNextFile = collectionFiles[nextNextIndex];
setCurrentFile(nextFile);
setNextFile(nextNextFile);
let nextURL = renderableFileURLCache.get(nextFile.id);
let nextNextURL = renderableFileURLCache.get(nextNextFile.id);
if (!nextURL) {
try {
const blob = await getPreviewableImage(nextFile, castToken);
const url = URL.createObjectURL(blob);
renderableFileURLCache.set(nextFile.id, url);
nextURL = url;
} catch (e) {
return;
}
}
if (!nextNextURL) {
try {
const blob = await getPreviewableImage(nextNextFile, castToken);
const url = URL.createObjectURL(blob);
renderableFileURLCache.set(nextNextFile.id, url);
nextNextURL = url;
} catch (e) {
return;
}
}
setLoading(false);
setCurrentFileId(nextFile.id);
setCurrentFileURL(nextURL);
setNextFileURL(nextNextURL);
};
const [renderableFileURL, setRenderableFileURL] = useState<string>("");
const getRenderableFileURL = async () => {
if (!currentFile) return;
const cacheValue = renderableFileURLCache.get(currentFile.id);
if (cacheValue) {
setRenderableFileURL(cacheValue);
setLoading(false);
return;
}
try {
const blob = await getPreviewableImage(
currentFile as EnteFile,
castToken,
);
const url = URL.createObjectURL(blob);
renderableFileURLCache.set(currentFile?.id, url);
setRenderableFileURL(url);
} catch (e) {
return;
} finally {
setLoading(false);
}
};
useEffect(() => {
if (currentFile) {
getRenderableFileURL();
}
}, [currentFile]);
if (loading) return <PairedSuccessfullyOverlay />;
return (
<>
<SlideshowContext.Provider value={{ showNextSlide }}>
<Theatre
file1={{
fileName: currentFile?.metadata.title,
fileURL: renderableFileURL,
type: currentFile?.metadata.fileType,
}}
file2={{
fileName: nextFile?.metadata.title,
fileURL: renderableFileURL,
type: nextFile?.metadata.fileType,
}}
/>
</SlideshowContext.Provider>
{loading && <PairedSuccessfullyOverlay />}
</>
<PhotoAuditorium
url={currentFileURL}
nextSlideUrl={nextFileURL}
showNextSlide={showNextSlide}
/>
);
}

View file

@ -50,7 +50,7 @@ export default function AlbumCastDialog(props: Props) {
setFieldError,
) => {
try {
await doCast(value);
await doCast(value.trim());
props.onHide();
} catch (e) {
const error = e as Error;