This commit is contained in:
Manav Rathi 2024-05-09 13:16:29 +05:30
parent e58424d2c0
commit dc525c4f8d
No known key found for this signature in database
3 changed files with 106 additions and 101 deletions

View file

@ -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<string | undefined>();

View file

@ -1,5 +1,7 @@
/// <reference types="chromecast-caf-receiver" />
import log from "@/next/log";
export type Cast = typeof cast;
/**
@ -44,3 +46,102 @@ export const castReceiverLoadingIfNeeded = async (): Promise<Cast> => {
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);
};

View file

@ -100,105 +100,6 @@ export const register = async (): Promise<Registration> => {
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