[cast] Tweaks to try and get it to work on older cast devices (#1657)
This commit is contained in:
commit
dbbb3c848a
10 changed files with 125 additions and 60 deletions
2
.github/workflows/auth-lint.yml
vendored
2
.github/workflows/auth-lint.yml
vendored
|
@ -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"
|
||||
|
|
2
.github/workflows/desktop-lint.yml
vendored
2
.github/workflows/desktop-lint.yml
vendored
|
@ -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"
|
||||
|
|
2
.github/workflows/docs-verify-build.yml
vendored
2
.github/workflows/docs-verify-build.yml
vendored
|
@ -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"
|
||||
|
|
2
.github/workflows/mobile-lint.yml
vendored
2
.github/workflows/mobile-lint.yml
vendored
|
@ -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"
|
||||
|
|
2
.github/workflows/server-lint.yml
vendored
2
.github/workflows/server-lint.yml
vendored
|
@ -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"
|
||||
|
|
2
.github/workflows/web-lint.yml
vendored
2
.github/workflows/web-lint.yml
vendored
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Add table
Reference in a new issue