소스 검색

Different variations

Manav Rathi 1 년 전
부모
커밋
72c99de344

+ 0 - 55
web/apps/cast/src/components/Slide.tsx

@@ -1,55 +0,0 @@
-import { styled } from "@mui/material";
-
-interface SlideViewProps {
-    /** The URL of the image to show. */
-    url: string;
-}
-
-/**
- * Show the image at {@link url} in a full screen view.
- *
- * Also show {@link nextURL} in an hidden image view to prepare the browser for
- * an imminent transition to it.
- */
-export const SlideView: React.FC<SlideViewProps> = ({ url }) => {
-    return (
-        <Container style={{ backgroundImage: `url(${url})` }}>
-            <img src={url} decoding="sync" alt="" />
-        </Container>
-    );
-};
-
-const Container = styled("div")`
-    width: 100%;
-    height: 100%;
-
-    background-size: cover;
-    background-position: center;
-    background-repeat: no-repeat;
-    background-blend-mode: multiply;
-    background-color: rgba(0, 0, 0, 0.5);
-    opacity: 0.2;
-
-    /* Smooth out the transition a bit.
-     *
-     * For the img itself, we set decoding="sync" to have it switch seamlessly.
-     * But there does not seem to be a way of setting decoding sync for the
-     * background image, and for large (multi-MB) images the background image
-     * switch is still visually non-atomic.
-     *
-     * As a workaround, add a long transition so that the background image
-     * transitions in a more "fade-to" manner. This effect might or might not be
-     * visually the best though.
-     *
-     * Does not work in Firefox, but that's fine, this is only a slight tweak,
-     * not a functional requirement.
-     */
-    /* transition: all 2s; */
-
-    img {
-        width: 100%;
-        height: 100%;
-        /* backdrop-filter: blur(10px); */
-        object-fit: contain;
-    }
-`;

+ 1 - 1
web/apps/cast/src/pages/index.tsx

@@ -6,7 +6,7 @@ import { useRouter } from "next/router";
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
 import { readCastData, storeCastData } from "services/cast-data";
 import { readCastData, storeCastData } from "services/cast-data";
 import { getCastData, register } from "services/pair";
 import { getCastData, register } from "services/pair";
-import { advertiseOnChromecast } from "../services/cast-receiver";
+import { advertiseOnChromecast } from "../services/chromecast";
 
 
 export default function Index() {
 export default function Index() {
     const [publicKeyB64, setPublicKeyB64] = useState<string | undefined>();
     const [publicKeyB64, setPublicKeyB64] = useState<string | undefined>();

+ 100 - 2
web/apps/cast/src/pages/slideshow.tsx

@@ -2,10 +2,10 @@ import log from "@/next/log";
 import { ensure } from "@/utils/ensure";
 import { ensure } from "@/utils/ensure";
 import { styled } from "@mui/material";
 import { styled } from "@mui/material";
 import { FilledCircleCheck } from "components/FilledCircleCheck";
 import { FilledCircleCheck } from "components/FilledCircleCheck";
-import { SlideView } from "components/Slide";
 import { useRouter } from "next/router";
 import { useRouter } from "next/router";
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
 import { readCastData } from "services/cast-data";
 import { readCastData } from "services/cast-data";
+import { isChromecast } from "services/chromecast";
 import { imageURLGenerator } from "services/render";
 import { imageURLGenerator } from "services/render";
 
 
 export default function Slideshow() {
 export default function Slideshow() {
@@ -55,7 +55,11 @@ export default function Slideshow() {
     if (loading) return <PairingComplete />;
     if (loading) return <PairingComplete />;
     if (isEmpty) return <NoItems />;
     if (isEmpty) return <NoItems />;
 
 
-    return <SlideView url={imageURL} />;
+    return isChromecast() ? (
+        <SlideViewChromecast url={imageURL} />
+    ) : (
+        <SlideView url={imageURL} />
+    );
 }
 }
 
 
 const PairingComplete: React.FC = () => {
 const PairingComplete: React.FC = () => {
@@ -97,3 +101,97 @@ const NoItems: React.FC = () => {
         </Message>
         </Message>
     );
     );
 };
 };
+
+interface SlideViewProps {
+    /** The URL of the image to show. */
+    url: string;
+}
+
+const SlideView: React.FC<SlideViewProps> = ({ url }) => {
+    return (
+        <SlideView_ style={{ backgroundImage: `url(${url})` }}>
+            <img src={url} decoding="sync" alt="" />
+        </SlideView_>
+    );
+};
+
+const SlideView_ = styled("div")`
+    width: 100%;
+    height: 100%;
+
+    background-size: cover;
+    background-position: center;
+    background-repeat: no-repeat;
+    background-blend-mode: multiply;
+    background-color: rgba(0, 0, 0, 0.5);
+
+    /* Smooth out the transition a bit.
+     *
+     * For the img itself, we set decoding="sync" to have it switch seamlessly.
+     * But there does not seem to be a way of setting decoding sync for the
+     * background image, and for large (multi-MB) images the background image
+     * switch is still visually non-atomic.
+     *
+     * As a workaround, add a long transition so that the background image
+     * transitions in a more "fade-to" manner. This effect might or might not be
+     * visually the best though.
+     *
+     * Does not work in Firefox, but that's fine, this is only a slight tweak,
+     * not a functional requirement.
+     */
+    transition: all 2s;
+
+    img {
+        width: 100%;
+        height: 100%;
+        backdrop-filter: blur(10px);
+        object-fit: contain;
+    }
+`;
+
+/**
+ * Variant of {@link SlideView} for use when we're running on Chromecast.
+ *
+ * Chromecast devices have trouble with
+ *
+ *     backdrop-filter: blur(10px);
+ *
+ * So emulate a cheaper approximation for use on Chromecast.
+ */
+const SlideViewChromecast: React.FC<SlideViewProps> = ({ url }) => {
+    return (
+        <SlideViewChromecast_>
+            <img className="svc-bg" src={url} alt="" />
+            <img className="svc-content" src={url} decoding="sync" alt="" />
+        </SlideViewChromecast_>
+    );
+};
+
+const SlideViewChromecast_ = styled("div")`
+    width: 100%;
+    height: 100%;
+
+    /* We can't set opacity of background-image, so use a wrapper */
+    position: relative;
+    overflow: hidden;
+
+    img.svg-bg {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+
+        opacity: 0.2;
+        background-blend-mode: multiply;
+        background-color: rgba(0, 0, 0, 0.5);
+    }
+
+    img.svc-content {
+        position: relative;
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+    }
+`;

+ 24 - 0
web/apps/cast/src/services/cast-receiver.tsx → web/apps/cast/src/services/chromecast.tsx

@@ -31,6 +31,10 @@ class CastReceiver {
      * always happens in response to a message handler.
      * always happens in response to a message handler.
      */
      */
     haveStarted = false;
     haveStarted = false;
+    /**
+     * Cached result of the isChromecast test.
+     */
+    isChromecast: boolean | undefined;
     /**
     /**
      * A callback to invoke to get the pairing code when we get a new incoming
      * A callback to invoke to get the pairing code when we get a new incoming
      * pairing request.
      * pairing request.
@@ -201,3 +205,23 @@ const advertiseCode = (cast: Cast) => {
     // Start listening for Chromecast connections.
     // Start listening for Chromecast connections.
     context.start(options);
     context.start(options);
 };
 };
+
+/**
+ * Return true if we're running on a Chromecast device.
+ *
+ * This allows changing our app's behaviour when we're running on Chromecast.
+ * Such checks are needed because during our testing we found that in practice,
+ * some processing is too heavy for Chromecast hardware (we tested with a 2nd
+ * gen device, this might not be true for newer variants).
+ *
+ * This variable is lazily updated when we enter {@link renderableImageURLs}. It
+ * is kept at the top level to avoid passing it around.
+ */
+export const isChromecast = () => {
+    let result = castReceiver.isChromecast;
+    if (result === undefined) {
+        result = window.navigator.userAgent.includes("CrKey");
+        castReceiver.isChromecast = result;
+    }
+    return result;
+};

+ 11 - 40
web/apps/cast/src/services/render.ts

@@ -27,31 +27,7 @@ import {
     FileMagicMetadata,
     FileMagicMetadata,
     FilePublicMagicMetadata,
     FilePublicMagicMetadata,
 } from "types/file";
 } from "types/file";
-
-/**
- * Change the behaviour when we're running on Chromecast.
- *
- * The Chromecast device fails to load the images if we give it too large
- * images. The documentation states:
- *
- * > Images have a display size limitation of 720p (1280x720). Images should be
- * > optimized to 1280x720 or less to avoid scaling down on the receiver device.
- * >
- * > https://developers.google.com/cast/docs/media
- *
- * When testing with Chromecast device (2nd gen, this might not be true for
- * newer variants), in practice we found that even this is iffy, likely because
- * in our case we also need to decrypt the E2EE data.
- *
- * So we have different codepaths when running on a Chromecast hardware.
- *
- * Also, to detect if we're running on a Chromecast, a user-agent check is the
- * only way. See: https://issuetracker.google.com/issues/36189456.
- *
- * This variable is lazily updated when we enter {@link renderableImageURLs}. It
- * is kept at the top level to avoid passing it around.
- */
-// let isChromecast = false;
+import { isChromecast } from "./chromecast";
 
 
 /**
 /**
  * If we're using HEIC conversion, then this variable caches the comlink web
  * If we're using HEIC conversion, then this variable caches the comlink web
@@ -119,8 +95,6 @@ export const imageURLGenerator = async function* (castData: CastData) {
      */
      */
     let consecutiveFailures = 0;
     let consecutiveFailures = 0;
 
 
-    // isChromecast = window.navigator.userAgent.includes("CrKey");
-
     while (true) {
     while (true) {
         const encryptedFiles = shuffled(
         const encryptedFiles = shuffled(
             await getEncryptedCollectionFiles(castToken),
             await getEncryptedCollectionFiles(castToken),
@@ -166,8 +140,8 @@ export const imageURLGenerator = async function* (castData: CastData) {
             //
             //
             // The last to last element is the one that was shown prior to that,
             // The last to last element is the one that was shown prior to that,
             // and now can be safely revoked.
             // and now can be safely revoked.
-            // if (previousURLs.length > 1)
-                // URL.revokeObjectURL(previousURLs.shift());
+            if (previousURLs.length > 1)
+                URL.revokeObjectURL(previousURLs.shift());
 
 
             previousURLs.push(url);
             previousURLs.push(url);
 
 
@@ -318,7 +292,7 @@ export const heicToJPEG = async (heicBlob: Blob) => {
  */
  */
 const createRenderableURL = async (castToken: string, file: EnteFile) => {
 const createRenderableURL = async (castToken: string, file: EnteFile) => {
     const imageBlob = await renderableImageBlob(castToken, file);
     const imageBlob = await renderableImageBlob(castToken, file);
-    const resizedBlob = needsResize() ? await resize(imageBlob) : imageBlob;
+    const resizedBlob = needsResize(file) ? await resize(imageBlob) : imageBlob;
     return URL.createObjectURL(resizedBlob);
     return URL.createObjectURL(resizedBlob);
 };
 };
 
 
@@ -353,7 +327,8 @@ const downloadFile = async (castToken: string, file: EnteFile) => {
     if (!isImageOrLivePhoto(file))
     if (!isImageOrLivePhoto(file))
         throw new Error("Can only cast images and live photos");
         throw new Error("Can only cast images and live photos");
 
 
-    const shouldUseThumbnail = true;
+    // TODO(MR):
+    const shouldUseThumbnail = false;
 
 
     const url = shouldUseThumbnail
     const url = shouldUseThumbnail
         ? getCastThumbnailURL(file.id)
         ? getCastThumbnailURL(file.id)
@@ -384,11 +359,6 @@ const downloadFile = async (castToken: string, file: EnteFile) => {
 /**
 /**
  * [Note: Chromecast media size limits]
  * [Note: Chromecast media size limits]
  *
  *
- * The Chromecast device fails to load the images if we give it too large
- * images. This was tested in practice with a 2nd Gen Chromecast.
- *
- * The documentation also states:
- *
  * > Images have a display size limitation of 720p (1280x720). Images should be
  * > Images have a display size limitation of 720p (1280x720). Images should be
  * > optimized to 1280x720 or less to avoid scaling down on the receiver device.
  * > optimized to 1280x720 or less to avoid scaling down on the receiver device.
  * >
  * >
@@ -397,15 +367,16 @@ const downloadFile = async (castToken: string, file: EnteFile) => {
  * So if the size of the image we're wanting to show is more than these limits,
  * So if the size of the image we're wanting to show is more than these limits,
  * resize it down to a JPEG whose size is clamped to these limits.
  * resize it down to a JPEG whose size is clamped to these limits.
  */
  */
-const needsResize = () => {
-    //file: EnteFile) => {
-    return false; /*
+const needsResize = (file: EnteFile) => {
+    // Resize only when running on Chromecast devices.
+    if (!isChromecast()) return false;
+
     const w = file.pubMagicMetadata?.data?.w;
     const w = file.pubMagicMetadata?.data?.w;
     const h = file.pubMagicMetadata?.data?.h;
     const h = file.pubMagicMetadata?.data?.h;
     // If we don't have the size, always resize to be on the safer side.
     // If we don't have the size, always resize to be on the safer side.
     if (!w || !h) return true;
     if (!w || !h) return true;
     // Otherwise resize if any of the dimensions is outside the recommendation.
     // Otherwise resize if any of the dimensions is outside the recommendation.
-    return Math.max(w, h) > 1280 || Math.min(w, h) > 720;*/
+    return Math.max(w, h) > 1280 || Math.min(w, h) > 720;
 };
 };
 
 
 const resize = async (blob: Blob): Promise<Blob> => {
 const resize = async (blob: Blob): Promise<Blob> => {