diff --git a/web/apps/cast/src/pages/index.tsx b/web/apps/cast/src/pages/index.tsx index a6f9b2ef6..fd250c574 100644 --- a/web/apps/cast/src/pages/index.tsx +++ b/web/apps/cast/src/pages/index.tsx @@ -4,9 +4,12 @@ import { styled } from "@mui/material"; import { PairingCode } from "components/PairingCode"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import { advertiseCode, getCastData, register } from "services/pair"; +import { getCastData, register } from "services/pair"; import { storeCastData } from "services/render"; -import { castReceiverLoadingIfNeeded } from "../services/cast-receiver"; +import { + advertiseCode, + castReceiverLoadingIfNeeded, +} from "../services/cast-receiver"; export default function Index() { const [publicKeyB64, setPublicKeyB64] = useState(); diff --git a/web/apps/cast/src/services/cast-receiver.tsx b/web/apps/cast/src/services/cast-receiver.tsx index 497cd95f9..5b8d866e0 100644 --- a/web/apps/cast/src/services/cast-receiver.tsx +++ b/web/apps/cast/src/services/cast-receiver.tsx @@ -1,5 +1,7 @@ /// +import log from "@/next/log"; + export type Cast = typeof cast; /** @@ -44,3 +46,102 @@ export const castReceiverLoadingIfNeeded = async (): Promise => { return await castReceiver.loader; }; + +/** + * Listen for incoming messages on the given {@link cast} receiver, replying to + * each of them with a pairing code obtained using the given {@link pairingCode} + * callback. Phase 2 of the pairing protocol. + * + * See: [Note: Pairing protocol]. + */ +export const advertiseCode = ( + cast: Cast, + pairingCode: () => string | undefined, +) => { + // Prepare the Chromecast "context". + const context = cast.framework.CastReceiverContext.getInstance(); + const namespace = "urn:x-cast:pair-request"; + + const options = new cast.framework.CastReceiverOptions(); + // We don't use the media features of the Cast SDK. + options.skipPlayersLoad = true; + // 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, data }: ListenerProps) => { + const restart = (reason: string) => { + log.error(`Restarting app: ${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) { + // 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; + } + + context.sendCustomMessage(namespace, senderId, { code }); + }; + + context.addCustomMessageListener( + namespace, + // We need to cast, the `senderId` is present in the message we get but + // not present in the TypeScript type. + incomingMessageListener as unknown as SystemEventHandler, + ); + + // 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(), + ); + + // Start listening for Chromecast connections. + context.start(options); +}; diff --git a/web/apps/cast/src/services/pair.ts b/web/apps/cast/src/services/pair.ts index 97117fa4a..179276abc 100644 --- a/web/apps/cast/src/services/pair.ts +++ b/web/apps/cast/src/services/pair.ts @@ -100,105 +100,6 @@ export const register = async (): Promise => { return { pairingCode, publicKeyB64, privateKeyB64 }; }; -/** - * Listen for incoming messages on the given {@link cast} receiver, replying to - * each of them with a pairing code obtained using the given {@link pairingCode} - * callback. Phase 2 of the pairing protocol. - * - * See: [Note: Pairing protocol]. - */ -export const advertiseCode = ( - cast: Cast, - pairingCode: () => string | undefined, -) => { - // Prepare the Chromecast "context". - const context = cast.framework.CastReceiverContext.getInstance(); - const namespace = "urn:x-cast:pair-request"; - - const options = new cast.framework.CastReceiverOptions(); - // We don't use the media features of the Cast SDK. - options.skipPlayersLoad = true; - // 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, 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) { - // 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; - } - - context.sendCustomMessage(namespace, senderId, { code }); - }; - - context.addCustomMessageListener( - namespace, - // We need to cast, the `senderId` is present in the message we get but - // not present in the TypeScript type. - incomingMessageListener as unknown as SystemEventHandler, - ); - - // 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(), - ); - - // Start listening for Chromecast connections. - context.start(options); -}; - /** * Ask museum if anyone has sent a (encrypted) payload corresponding to the * given pairing code. If so, decrypt it using our private key and return the