[cast] Tweaks to try and get it to work on older cast devices (#1657)

This commit is contained in:
Manav Rathi 2024-05-08 15:33:40 +05:30 committed by GitHub
commit dbbb3c848a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 125 additions and 60 deletions

View file

@ -3,7 +3,7 @@ name: "Lint (auth)"
on:
# Run on every push to a branch other than main that changes auth/
push:
branches-ignore: [main, "deploy/**"]
branches-ignore: [main, "deploy/**", "deploy-f/**"]
paths:
- "auth/**"
- ".github/workflows/auth-lint.yml"

View file

@ -3,7 +3,7 @@ name: "Lint (desktop)"
on:
# Run on every push to a branch other than main that changes desktop/
push:
branches-ignore: [main, "deploy/**"]
branches-ignore: [main, "deploy/**", "deploy-f/**"]
paths:
- "desktop/**"
- ".github/workflows/desktop-lint.yml"

View file

@ -6,7 +6,7 @@ name: "Verify build (docs)"
on:
# Run on every push to a branch other than main that changes docs/
push:
branches-ignore: [main, "deploy/**"]
branches-ignore: [main, "deploy/**", "deploy-f/**"]
paths:
- "docs/**"
- ".github/workflows/docs-verify-build.yml"

View file

@ -3,7 +3,7 @@ name: "Lint (mobile)"
on:
# Run on every push to a branch other than main that changes mobile/
push:
branches-ignore: [main, f-droid, "deploy/**"]
branches-ignore: [main, f-droid, "deploy/**", "deploy-f/**"]
paths:
- "mobile/**"
- ".github/workflows/mobile-lint.yml"

View file

@ -3,7 +3,7 @@ name: "Lint (server)"
on:
# Run on every push to a branch other than main that changes server/
push:
branches-ignore: [main, "deploy/**"]
branches-ignore: [main, "deploy/**", "deploy-f/**"]
paths:
- "server/**"
- ".github/workflows/server-lint.yml"

View file

@ -3,7 +3,7 @@ name: "Lint (web)"
on:
# Run on every push to a branch other than main that changes web/
push:
branches-ignore: [main, "deploy/**"]
branches-ignore: [main, "deploy/**", "deploy-f/**"]
paths:
- "web/**"
- ".github/workflows/web-lint.yml"

View file

@ -12,25 +12,28 @@ export default function Index() {
const [privateKeyB64, setPrivateKeyB64] = useState<string | undefined>();
const [pairingCode, setPairingCode] = useState<string | undefined>();
// Keep a boolean flag to ensure that Cast Receiver starts only once even if
// pairing codes change.
const [haveInitializedCast, setHaveInitializedCast] = useState(false);
const router = useRouter();
useEffect(() => {
init();
}, []);
const init = () => {
register().then((r) => {
setPublicKeyB64(r.publicKeyB64);
setPrivateKeyB64(r.privateKeyB64);
setPairingCode(r.pairingCode);
});
};
useEffect(() => {
castReceiverLoadingIfNeeded().then((cast) =>
advertiseCode(cast, () => pairingCode),
);
}, []);
if (!pairingCode) {
register().then((r) => {
setPublicKeyB64(r.publicKeyB64);
setPrivateKeyB64(r.privateKeyB64);
setPairingCode(r.pairingCode);
});
} else {
if (!haveInitializedCast) {
castReceiverLoadingIfNeeded().then((cast) => {
setHaveInitializedCast(true);
advertiseCode(cast, () => pairingCode);
});
}
}
}, [pairingCode]);
useEffect(() => {
if (!publicKeyB64 || !privateKeyB64 || !pairingCode) return;
@ -52,10 +55,12 @@ export default function Index() {
storeCastData(data);
await router.push("/slideshow");
} catch (e) {
log.error("Failed to get cast data", e);
// Start again from the beginning.
// The pairing code becomes invalid after an hour, which will cause
// `getCastData` to fail. There might be other reasons this might
// fail too, but in all such cases, it is a reasonable idea to start
// again from the beginning.
log.warn("Failed to get cast data", e);
setPairingCode(undefined);
init();
}
};

View file

@ -46,6 +46,8 @@ export default function Slideshow() {
};
}, []);
console.log("Rendering slideshow", { loading, imageURL, nextImageURL });
if (loading) return <PairedSuccessfullyOverlay />;
return <SlideView url={imageURL} nextURL={nextImageURL} />;

View file

@ -84,26 +84,17 @@ export const renderableImageURLs = async function* (castData: CastData) {
const { collectionKey, castToken } = castData;
/**
* We have a sliding window of four URLs, with the `urls[1]` being the one
* that is the one currently being shown in the slideshow.
*
* At each step, we shift the window towards the right by shifting out the
* leftmost (oldest) `urls[0]`, and adding a new one at the end.
*
* We can revoke url[0] when we shift it out because we know it is not being
* used anymore.
*
* We need to special case the first two renders to avoid revoking the
* initial URLs that are displayed the first two times. This results in a
* memory leak of the very first objectURL that we display.
* Keep a FIFO queue of the URLs that we've vended out recently so that we
* can revoke those that are not being shown anymore.
*/
const urls: string[] = [""];
let i = 0;
const previousURLs: string[] = [];
/**
* Number of milliseconds to keep the slide on the screen.
*/
/** The URL pair that we will yield */
const urls: string[] = [];
/** Number of milliseconds to keep the slide on the screen. */
const slideDuration = 10000; /* 10 s */
/**
* Time when we last yielded.
*
@ -137,16 +128,33 @@ export const renderableImageURLs = async function* (castData: CastData) {
continue;
}
if (urls.length < 4) continue;
// Need at least a pair.
//
// There are two scenarios:
//
// - First run: urls will initially be empty, so gobble two.
//
// - Subsequently, urls will have the "next" / "preloaded" URL left
// over from the last time. We'll promote that to being the one
// that'll get displayed, and preload another one.
if (urls.length < 2) continue;
const oldestURL = urls.shift();
if (oldestURL && i !== 1) URL.revokeObjectURL(oldestURL);
i += 1;
// The last element of previousURLs is the URL that is currently
// being shown on screen.
//
// The last to last element is the one that was shown prior to that,
// and now can be safely revoked.
if (previousURLs.length > 1)
URL.revokeObjectURL(previousURLs.shift());
const urlPair: RenderableImageURLPair = [
ensure(urls[0]),
ensure(urls[1]),
];
// The URL that'll now get displayed on screen.
const url = ensure(urls.shift());
// The URL that we're preloading for next time around.
const nextURL = ensure(urls[0]);
previousURLs.push(url);
const urlPair: RenderableImageURLPair = [url, nextURL];
const elapsedTime = Date.now() - lastYieldTime;
if (elapsedTime > 0 && elapsedTime < slideDuration)
@ -299,6 +307,8 @@ const downloadFile = async (castToken: string, file: EnteFile) => {
throw new Error("Can only cast images and live photos");
const url = getCastFileURL(file.id);
// TODO(MR): Remove if usused eventually
// const url = getCastThumbnailURL(file.id);
const resp = await HTTPService.get(
url,
null,
@ -313,6 +323,8 @@ const downloadFile = async (castToken: string, file: EnteFile) => {
const decrypted = await cryptoWorker.decryptFile(
new Uint8Array(resp.data),
await cryptoWorker.fromB64(file.file.decryptionHeader),
// TODO(MR): Remove if usused eventually
// await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
file.key,
);
return new Response(decrypted).blob();

View file

@ -116,24 +116,68 @@ export const advertiseCode = (
const namespace = "urn:x-cast:pair-request";
const options = new cast.framework.CastReceiverOptions();
// Do not automatically close the connection when the sender disconnects.
options.maxInactivity = 3600; /* 1 hour */
// We don't use the media features of the Cast SDK.
options.skipPlayersLoad = true;
// TODO:Is this required? The docs say "(The default type of a message bus
// is JSON; if not provided here)."
options.customNamespaces = Object.assign({});
options.customNamespaces[namespace] =
cast.framework.system.MessageType.JSON;
// TODO: This looks like the only one needed, but a comment with the reason
// might be good.
// Do not stop the casting if the receiver is unreachable. A user should be
// able to start a cast on their phone and then put it away, leaving the
// cast running on their big screen.
options.disableIdleTimeout = true;
// The collection ID with which we paired. If we get another connection
// request for a different collection ID, restart the app to allow them to
// reconnect using a freshly generated pairing code.
//
// If the request does not have a collectionID, forego this check.
let pairedCollectionID: string | undefined;
type ListenerProps = {
senderId: string;
data: unknown;
};
// Reply with the code that we have if anyone asks over Chromecast.
const incomingMessageListener = ({ senderId }: { senderId: string }) => {
const incomingMessageListener = ({ senderId, data }: ListenerProps) => {
const restart = (reason: string) => {
log.error(`Restarting app because ${reason}`);
// context.stop will close the tab but it'll get reopened again
// immediately since the client app will reconnect in the scenarios
// where we're calling this function.
context.stop();
};
const collectionID =
data &&
typeof data == "object" &&
typeof data["collectionID"] == "string"
? data["collectionID"]
: undefined;
if (pairedCollectionID && pairedCollectionID != collectionID) {
restart(`incoming request for a new collection ${collectionID}`);
return;
}
pairedCollectionID = collectionID;
const code = pairingCode();
if (!code) {
log.warn(
"Ignoring incoming Chromecast message because we do not yet have a pairing code",
);
// Our caller waits until it has a pairing code before it calls
// `advertiseCode`, but there is still an edge case where we can
// find ourselves without a pairing code:
//
// 1. The current pairing code expires. We start the process to get
// a new one.
//
// 2. But before that happens, someone connects.
//
// The window where this can happen is short, so if we do find
// ourselves in this scenario,
restart("we got a pairing request when refreshing pairing codes");
return;
}
@ -147,8 +191,10 @@ export const advertiseCode = (
incomingMessageListener as unknown as SystemEventHandler,
);
// Shutdown ourselves if the sender disconnects.
// TODO(MR): I assume the page reloads on shutdown. Is that correct?
// Close the (chromecast) tab if the sender disconnects.
//
// Chromecast does a "shutdown" of our cast app when we call `context.stop`.
// This translates into it closing the tab where it is showing our app.
context.addEventListener(
cast.framework.system.EventType.SENDER_DISCONNECTED,
() => context.stop(),