diff --git a/web/apps/cast/src/services/pair.ts b/web/apps/cast/src/services/pair.ts
new file mode 100644
index 000000000..02c1da6d6
--- /dev/null
+++ b/web/apps/cast/src/services/pair.ts
@@ -0,0 +1,103 @@
+import log from "@/next/log";
+import { toB64 } from "@ente/shared/crypto/internal/libsodium";
+import castGateway from "@ente/shared/network/cast";
+import { wait } from "@ente/shared/utils";
+import _sodium from "libsodium-wrappers";
+import { type Cast } from "../utils/useCastReceiver";
+
+/**
+ * Listen for pairing requests using the given {@link cast} instance.
+ *
+ * [Note: Cast protocol]
+ *
+ * The Chromecast Framework (represented here by our handle to the Chromecast
+ * Web SDK, {@link cast}) itself is used for only the initial handshake, none of
+ * the data, even encrypted passes over it thereafter.
+ *
+ * The entire protocol is quite simple.
+ *
+ * 1. We (the receiver) generate a public/private keypair. and register the
+ * public part of it with museum.
+ *
+ * 2. Museum gives us a pairing "code" in lieu.
+ *
+ * 3. Listen for incoming messages over the Chromecast connection.
+ *
+ * 4. The client (our Web or mobile app) will connect using the "sender"
+ * Chromecast SDK. This will result in a bi-directional channel between us
+ * ("receiver") and the Ente client app ("sender").
+ *
+ * 5. Thereafter, if at any time the sender disconnects, close the Chromecast
+ * context. This effectively shuts us down, causing the entire page to get
+ * reloaded.
+ *
+ * 6. After connecting, the sender sends an (empty) message. We reply by sending
+ * them a message containing the pairing code. This exchange is the only data
+ * that traverses over the Chromecast connection.
+ *
+ * 5. If at anytime the
+ *
+ *
+ *
+ * in our custom // "urn:x-cast:pair-request" namespace. over Chromecast
+ protocol is minimal:
+ *
+ * 1. Client (Web or mobile) sends an (empty) message in our custom //
+ "urn:x-cast:pair-request" namespace.
+ //
+ // 2. We reply with the device code.
+*/
+export const listenForPairingRequest = async (cast: Cast) => {
+ // Generate keypair
+ const keypair = await generateKeyPair();
+ const publicKeyB64 = await toB64(keypair.publicKey);
+ const privateKeyB64 = await toB64(keypair.privateKey);
+
+ // Register keypair with museum to get a pairing code.
+ let code: string;
+ do {
+ try {
+ code = await castGateway.registerDevice(publicKeyB64);
+ } catch (e) {
+ log.error("Failed to register public key with server", e);
+ // Schedule retry after 10 seconds.
+ await wait(10000);
+ }
+ } while (code === undefined);
+
+ // Listen for incoming messages sent via the Chromecast SDK
+ const context = cast.framework.CastReceiverContext.getInstance();
+ const namespace = "urn:x-cast:pair-request";
+
+ const options = new cast.framework.CastReceiverOptions();
+ // TODO(MR): Are any of these options required?
+ options.maxInactivity = 3600;
+ options.customNamespaces = Object.assign({});
+ options.customNamespaces[namespace] =
+ cast.framework.system.MessageType.JSON;
+ options.disableIdleTimeout = true;
+
+ // Reply with the code that we have if anyone asks over chromecast.
+ const incomingMessageListener = ({ senderId }: { senderId: string }) =>
+ 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,
+ );
+
+ // Shutdown ourselves if the "sender" disconnects.
+ context.addEventListener(
+ cast.framework.system.EventType.SENDER_DISCONNECTED,
+ () => context.stop(),
+ );
+
+ context.start(options);
+};
+
+const generateKeyPair = async () => {
+ await _sodium.ready;
+ return _sodium.crypto_box_keypair();
+};
diff --git a/web/apps/cast/src/utils/useCastReceiver.tsx b/web/apps/cast/src/utils/useCastReceiver.tsx
index a2313fdd0..8cd958ec4 100644
--- a/web/apps/cast/src/utils/useCastReceiver.tsx
+++ b/web/apps/cast/src/utils/useCastReceiver.tsx
@@ -1,6 +1,8 @@
///
import { useEffect, useState } from "react";
+export type Cast = typeof cast;
+
/**
* Load the Chromecast Web Receiver SDK and return a reference to the `cast`
* global object that the SDK attaches to the window.
@@ -8,7 +10,7 @@ import { useEffect, useState } from "react";
* https://developers.google.com/cast/docs/web_receiver/basic
*/
export const useCastReceiver = () => {
- const [receiver, setReceiver] = useState();
+ const [receiver, setReceiver] = useState();
useEffect(() => {
const script = document.createElement("script");