mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 16:49:58 +00:00
795 lines
25 KiB
TypeScript
795 lines
25 KiB
TypeScript
import bridge from "../lib/bridge";
|
|
import {
|
|
BaseConfig,
|
|
baseConfigStorage,
|
|
getAppTag
|
|
} from "../lib/chromecastConfigApi";
|
|
import logger from "../lib/logger";
|
|
import messaging, { Message, Port } from "../messaging";
|
|
import options from "../lib/options";
|
|
import { getMediaTypesForPageUrl, stringify } from "../lib/utils";
|
|
import type { TypedMessagePort } from "../lib/TypedMessagePort";
|
|
|
|
import {
|
|
ReceiverSelectorAppInfo,
|
|
ReceiverSelectorMediaType,
|
|
ReceiverSelectorPageInfo
|
|
} from "../types";
|
|
|
|
import type { ApiConfig } from "../cast/sdk/classes";
|
|
import { ReceiverAction } from "../cast/sdk/enums";
|
|
import { DEFAULT_MEDIA_RECEIVER_APP_ID } from "../cast/sdk/media";
|
|
import { createReceiver } from "../cast/utils";
|
|
|
|
import deviceManager from "./deviceManager";
|
|
|
|
import ReceiverSelector, {
|
|
ReceiverSelection,
|
|
ReceiverSelectorMediaMessage,
|
|
ReceiverSelectorReceiverMessage
|
|
} from "./ReceiverSelector";
|
|
|
|
type AnyPort = Port | TypedMessagePort<Message>;
|
|
|
|
export interface ContentContext {
|
|
tabId: number;
|
|
frameId: number;
|
|
}
|
|
|
|
interface CastSession {
|
|
sessionId: string;
|
|
deviceId: string;
|
|
}
|
|
|
|
export interface CastInstance {
|
|
bridgePort: Port;
|
|
contentPort: AnyPort;
|
|
contentContext?: ContentContext;
|
|
|
|
/** From an extension-source, grants additional permissions. */
|
|
isTrusted: boolean;
|
|
|
|
/** ApiConfig provided on initialization. */
|
|
apiConfig?: ApiConfig;
|
|
/** Established session details. */
|
|
session?: CastSession;
|
|
}
|
|
|
|
/** Creates a cast instance object and associated bridge instance. */
|
|
async function createCastInstance(opts: {
|
|
bridgePort?: Port;
|
|
contentPort: AnyPort;
|
|
contentContext?: { tabId: number; frameId?: number };
|
|
isTrusted?: boolean;
|
|
}) {
|
|
const instance: CastInstance = {
|
|
bridgePort: opts.bridgePort ?? (await bridge.connect()),
|
|
contentPort: opts.contentPort,
|
|
isTrusted: opts.isTrusted ?? false
|
|
};
|
|
|
|
/**
|
|
* Set content context with fallback to extension message sender
|
|
* context for content scripts.
|
|
*/
|
|
if (opts.contentContext) {
|
|
instance.contentContext = {
|
|
tabId: opts.contentContext.tabId,
|
|
frameId: opts.contentContext.frameId ?? 0
|
|
};
|
|
} else if (
|
|
!(opts.contentPort instanceof MessagePort) &&
|
|
opts.contentPort.sender?.tab?.id
|
|
) {
|
|
instance.contentContext = {
|
|
tabId: opts.contentPort.sender.tab.id,
|
|
frameId: opts.contentPort.sender.frameId ?? 0
|
|
};
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
/** Disconnects either instance content port type. */
|
|
function disconnectContentPort(port: AnyPort) {
|
|
if (port instanceof MessagePort) {
|
|
port.close();
|
|
} else {
|
|
port.disconnect();
|
|
}
|
|
}
|
|
|
|
/** Checks if two content contexts match. */
|
|
function isSameContext(ctx1?: ContentContext, ctx2?: ContentContext) {
|
|
if (!ctx1 || !ctx2) return false;
|
|
return ctx1?.tabId === ctx2?.tabId && ctx1?.frameId === ctx2?.frameId;
|
|
}
|
|
|
|
let baseConfig: BaseConfig;
|
|
let receiverSelector: Optional<ReceiverSelector>;
|
|
|
|
const activeInstances = new Set<CastInstance>();
|
|
|
|
/** Keeps track of cast API instances and provides bridge messaging. */
|
|
const castManager = new (class {
|
|
async init() {
|
|
// Handle incoming instance connections
|
|
messaging.onConnect.addListener(async port => {
|
|
if (port.name === "cast") {
|
|
this.createInstance(port);
|
|
} else if (port.name === "trusted-cast") {
|
|
// Create trusted instance
|
|
this.createInstance(port, undefined, true);
|
|
}
|
|
});
|
|
|
|
// Pass receiver availability updates to cast API.
|
|
const updateReceiverAvailability = () => {
|
|
const isAvailable = deviceManager.getDevices().length > 0;
|
|
|
|
for (const instance of activeInstances) {
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:receiverAvailabilityUpdated",
|
|
data: { isAvailable }
|
|
});
|
|
}
|
|
};
|
|
|
|
deviceManager.addEventListener("deviceUp", updateReceiverAvailability);
|
|
deviceManager.addEventListener(
|
|
"deviceDown",
|
|
updateReceiverAvailability
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Finds a cast instance at the given tab (and optionally frame) ID.
|
|
*/
|
|
getInstanceAt(tabId: number, frameId?: number) {
|
|
for (const instance of activeInstances) {
|
|
if (instance.contentContext?.tabId === tabId) {
|
|
// If frame ID doesn't match go to next instance
|
|
if (frameId && instance.contentContext.frameId !== frameId) {
|
|
continue;
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
}
|
|
}
|
|
|
|
getInstanceByDeviceId(deviceId: string) {
|
|
for (const instance of activeInstances) {
|
|
if (instance.session?.deviceId === deviceId) return instance;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a cast instance with a given port and connects messaging
|
|
* correctly depending on the type of port.
|
|
*/
|
|
async createInstance(
|
|
port: AnyPort,
|
|
contentContext?: ContentContext,
|
|
isTrusted?: boolean
|
|
) {
|
|
const instance = await (port instanceof MessagePort
|
|
? this.createInstanceFromBackground(port, contentContext)
|
|
: this.createInstanceFromContent(port, isTrusted));
|
|
|
|
activeInstances.add(instance);
|
|
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:instanceCreated",
|
|
data: { isAvailable: (await bridge.getInfo()).isVersionCompatible }
|
|
});
|
|
|
|
return instance;
|
|
}
|
|
|
|
/** Creates a cast instance with a `MessagePort` content port. */
|
|
private async createInstanceFromBackground(
|
|
contentPort: MessagePort,
|
|
contentContext?: ContentContext
|
|
): Promise<CastInstance> {
|
|
const instance = await createCastInstance({
|
|
bridgePort: await bridge.connect(),
|
|
contentPort,
|
|
contentContext,
|
|
isTrusted: true
|
|
});
|
|
|
|
// Ensure only one instance per context
|
|
if (contentContext) {
|
|
for (const instance of activeInstances) {
|
|
if (isSameContext(instance.contentContext, contentContext)) {
|
|
instance.bridgePort.disconnect();
|
|
activeInstances.delete(instance);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
instance.bridgePort.onDisconnect.addListener(() => {
|
|
contentPort.close();
|
|
activeInstances.delete(instance);
|
|
});
|
|
|
|
// bridge -> cast instance
|
|
instance.bridgePort.onMessage.addListener(message => {
|
|
this.handleBridgeMessage(instance, message);
|
|
});
|
|
|
|
// cast instance -> (any)
|
|
contentPort.addEventListener("message", ev => {
|
|
this.handleContentMessage(instance, ev.data);
|
|
});
|
|
contentPort.start();
|
|
|
|
return instance;
|
|
}
|
|
|
|
/**
|
|
* Creates a cast instance with a WebExtension `Port` content port.
|
|
*/
|
|
private async createInstanceFromContent(
|
|
contentPort: Port,
|
|
isTrusted?: boolean
|
|
): Promise<CastInstance> {
|
|
if (
|
|
contentPort.sender?.tab?.id === undefined ||
|
|
contentPort.sender?.frameId === undefined
|
|
) {
|
|
throw logger.error(
|
|
"Cast instance created from content with an invalid port context."
|
|
);
|
|
}
|
|
|
|
// Ensure only one instance per context
|
|
for (const instance of activeInstances) {
|
|
if (
|
|
isSameContext(
|
|
instance.contentContext,
|
|
contentPort.sender as ContentContext
|
|
)
|
|
) {
|
|
instance.bridgePort.disconnect();
|
|
disconnectContentPort(instance.contentPort);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const instance = await createCastInstance({ contentPort, isTrusted });
|
|
|
|
// cast instance -> (any)
|
|
const onContentPortMessage = (message: Message) => {
|
|
this.handleContentMessage(instance, message);
|
|
};
|
|
// bridge -> cast instance
|
|
const onBridgePortMessage = (message: Message) => {
|
|
this.handleBridgeMessage(instance, message);
|
|
};
|
|
|
|
const onDisconnect = () => {
|
|
instance.bridgePort.onMessage.removeListener(onBridgePortMessage);
|
|
contentPort.onMessage.removeListener(onContentPortMessage);
|
|
|
|
instance.bridgePort.disconnect();
|
|
contentPort.disconnect();
|
|
|
|
activeInstances.delete(instance);
|
|
};
|
|
|
|
instance.bridgePort.onDisconnect.addListener(onDisconnect);
|
|
instance.bridgePort.onMessage.addListener(onBridgePortMessage);
|
|
|
|
contentPort.onDisconnect.addListener(onDisconnect);
|
|
contentPort.onMessage.addListener(onContentPortMessage);
|
|
|
|
return instance;
|
|
}
|
|
|
|
private async handleBridgeMessage(
|
|
instance: CastInstance,
|
|
message: Message
|
|
) {
|
|
// Intercept messages to store relevant info
|
|
switch (message.subject) {
|
|
case "main:castSessionCreated": {
|
|
// Close after session is created
|
|
if (
|
|
receiverSelector?.isOpen &&
|
|
// If selector context is the same as the instance context
|
|
isSameContext(
|
|
receiverSelector.pageInfo,
|
|
instance.contentContext
|
|
) &&
|
|
// If selector is supposed to close
|
|
(await options.get("receiverSelectorWaitForConnection"))
|
|
) {
|
|
receiverSelector.close();
|
|
}
|
|
|
|
const { receiverId: deviceId } = message.data;
|
|
|
|
instance.session = {
|
|
deviceId,
|
|
sessionId: message.data.sessionId
|
|
};
|
|
|
|
const device = deviceManager.getDeviceById(deviceId);
|
|
if (!device) {
|
|
logger.error(
|
|
"[on main:castSessionCreated]: Could not find device with ID:",
|
|
deviceId
|
|
);
|
|
break;
|
|
}
|
|
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:sessionCreated",
|
|
data: {
|
|
...message.data,
|
|
receiver: createReceiver(device)
|
|
}
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
case "main:castSessionUpdated":
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:sessionUpdated",
|
|
data: message.data
|
|
});
|
|
}
|
|
|
|
instance.contentPort.postMessage(message);
|
|
}
|
|
|
|
/**
|
|
* Handle content messages from the cast instance. These will either
|
|
* be handled here in the background script or forwarded to the
|
|
* bridge associated with the cast instance.
|
|
*/
|
|
private async handleContentMessage(
|
|
instance: CastInstance,
|
|
message: Message
|
|
) {
|
|
const [destination] = message.subject.split(":");
|
|
if (destination === "bridge") {
|
|
instance.bridgePort.postMessage(message);
|
|
}
|
|
|
|
switch (message.subject) {
|
|
case "main:initializeCastSdk":
|
|
instance.apiConfig = message.data.apiConfig;
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:receiverAvailabilityUpdated",
|
|
data: {
|
|
isAvailable: deviceManager.getDevices().length > 0
|
|
}
|
|
});
|
|
|
|
break;
|
|
|
|
// User has triggered receiver selection via the cast API
|
|
case "main:requestSession": {
|
|
const { sessionRequest, receiverDevice } = message.data;
|
|
|
|
// Handle trusted instance receiver selection bypass
|
|
if (receiverDevice) {
|
|
if (!instance.isTrusted) {
|
|
logger.error(
|
|
"Cast instance not trusted to bypass receiver selection!"
|
|
);
|
|
break;
|
|
}
|
|
|
|
instance.bridgePort.postMessage({
|
|
subject: "bridge:createCastSession",
|
|
data: {
|
|
appId: sessionRequest.appId,
|
|
receiverDevice
|
|
}
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
try {
|
|
const selection = await getReceiverSelection({
|
|
castInstance: instance
|
|
});
|
|
|
|
// Handle cancellation
|
|
if (!selection) {
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:sessionRequestCancelled"
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
/**
|
|
* If the media type returned from the selector has
|
|
* been changed, we need to cancel the current
|
|
* sender and switch it out for the right one.
|
|
*/
|
|
if (selection.mediaType !== ReceiverSelectorMediaType.App) {
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:sessionRequestCancelled"
|
|
});
|
|
|
|
if (!instance.contentContext) {
|
|
throw logger.error("Missing content context");
|
|
}
|
|
this.loadSender(selection, instance.contentContext);
|
|
|
|
break;
|
|
}
|
|
|
|
instance.bridgePort.postMessage({
|
|
subject: "bridge:createCastSession",
|
|
data: {
|
|
appId: sessionRequest.appId,
|
|
receiverDevice: selection.device
|
|
}
|
|
});
|
|
} catch (err) {
|
|
// TODO: Report errors properly
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:sessionRequestCancelled"
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a receiver selection and loads the appropriate sender for a
|
|
* given context.
|
|
*/
|
|
async triggerCast(tabId: number, frameId = 0) {
|
|
let selection: Nullable<ReceiverSelection>;
|
|
try {
|
|
selection = await getReceiverSelection({ tabId, frameId });
|
|
} catch (err) {
|
|
logger.error("Failed to get receiver selection (triggerCast)", err);
|
|
return;
|
|
}
|
|
|
|
if (!selection) return;
|
|
|
|
this.loadSender(selection, { tabId, frameId });
|
|
}
|
|
|
|
/**
|
|
* Loads the appropriate sender for a given receiver selector
|
|
* response.
|
|
*/
|
|
private async loadSender(
|
|
selection: ReceiverSelection,
|
|
contentContext: ContentContext
|
|
) {
|
|
// Cancelled
|
|
if (!selection) {
|
|
return;
|
|
}
|
|
|
|
switch (selection.mediaType) {
|
|
case ReceiverSelectorMediaType.App: {
|
|
const instance = this.getInstanceAt(
|
|
contentContext.tabId,
|
|
contentContext.frameId
|
|
);
|
|
if (!instance) {
|
|
throw logger.error(
|
|
`Cast instance not found at tabId ${contentContext.tabId} / frameId ${contentContext.frameId}`
|
|
);
|
|
}
|
|
|
|
if (!instance.apiConfig?.sessionRequest.appId) {
|
|
throw logger.error("Invalid session request");
|
|
}
|
|
|
|
instance.contentPort.postMessage({
|
|
subject: "cast:receiverAction",
|
|
data: {
|
|
receiver: createReceiver(selection.device),
|
|
action: ReceiverAction.CAST
|
|
}
|
|
});
|
|
|
|
instance.bridgePort.postMessage({
|
|
subject: "bridge:createCastSession",
|
|
data: {
|
|
appId: instance.apiConfig?.sessionRequest.appId,
|
|
receiverDevice: selection.device
|
|
}
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
case ReceiverSelectorMediaType.Tab:
|
|
case ReceiverSelectorMediaType.Screen:
|
|
await browser.tabs.executeScript(contentContext.tabId, {
|
|
code: stringify`
|
|
window.mirroringMediaType = ${selection.mediaType};
|
|
window.receiverDevice = ${selection.device};
|
|
window.contextTabId = ${contentContext.tabId};
|
|
`,
|
|
frameId: contentContext.frameId
|
|
});
|
|
|
|
await browser.tabs.executeScript(contentContext.tabId, {
|
|
file: "cast/senders/mirroring.js",
|
|
frameId: contentContext.frameId
|
|
});
|
|
|
|
break;
|
|
}
|
|
}
|
|
})();
|
|
|
|
/**
|
|
* Opens a receiver selector with the specified default/available media
|
|
* types.
|
|
*
|
|
* Returns a promise that:
|
|
* - Resolves to a ReceiverSelection object if selection is
|
|
* successful.
|
|
* - Resolves to null if the selection is cancelled.
|
|
* - Rejects if the selection fails.
|
|
*/
|
|
async function getReceiverSelection(selectionOpts: {
|
|
tabId?: number;
|
|
frameId?: number;
|
|
castInstance?: CastInstance;
|
|
}): Promise<ReceiverSelection | null> {
|
|
/**
|
|
* If the current context is running the mirroring app, pretend
|
|
* it doesn't exist because it shouldn't be launched like this.
|
|
*/
|
|
if (
|
|
selectionOpts.castInstance?.apiConfig?.sessionRequest.appId ===
|
|
(await options.get("mirroringAppId"))
|
|
) {
|
|
selectionOpts.castInstance = undefined;
|
|
}
|
|
|
|
let defaultMediaType = ReceiverSelectorMediaType.Tab;
|
|
let availableMediaTypes = ReceiverSelectorMediaType.None;
|
|
|
|
// Default frame ID
|
|
if (selectionOpts.frameId === undefined) selectionOpts.frameId = 0;
|
|
|
|
// Fallback to instance context
|
|
if (
|
|
selectionOpts.tabId === undefined &&
|
|
selectionOpts.castInstance?.contentContext
|
|
) {
|
|
selectionOpts.tabId = selectionOpts.castInstance.contentContext.tabId;
|
|
selectionOpts.frameId =
|
|
selectionOpts.castInstance.contentContext.frameId;
|
|
}
|
|
|
|
const opts = await options.getAll();
|
|
|
|
/**
|
|
* If context supplied, but no instance, check for an instance at
|
|
* that context.
|
|
*/
|
|
if (
|
|
!selectionOpts.castInstance &&
|
|
selectionOpts.tabId !== undefined &&
|
|
selectionOpts.frameId !== undefined
|
|
) {
|
|
const contextInstance = castManager.getInstanceAt(
|
|
selectionOpts.tabId,
|
|
selectionOpts.frameId
|
|
);
|
|
/**
|
|
* If the app in that context is the extension mirroring app or
|
|
* the default receiver, just ignore it.
|
|
*/
|
|
const contextAppId = contextInstance?.apiConfig?.sessionRequest.appId;
|
|
if (
|
|
contextAppId !== opts.mirroringAppId &&
|
|
contextAppId !== DEFAULT_MEDIA_RECEIVER_APP_ID
|
|
) {
|
|
selectionOpts.castInstance = contextInstance;
|
|
}
|
|
}
|
|
|
|
let pageInfo: Optional<ReceiverSelectorPageInfo>;
|
|
if (selectionOpts.tabId !== undefined) {
|
|
try {
|
|
pageInfo = {
|
|
tabId: selectionOpts.tabId,
|
|
frameId: selectionOpts.frameId,
|
|
url: (
|
|
await browser.webNavigation.getFrame({
|
|
tabId: selectionOpts.tabId,
|
|
frameId: selectionOpts.frameId
|
|
})
|
|
).url
|
|
};
|
|
|
|
availableMediaTypes = getMediaTypesForPageUrl(pageInfo.url);
|
|
} catch {
|
|
logger.error(
|
|
"Failed to locate frame, falling back to default available media types."
|
|
);
|
|
}
|
|
}
|
|
|
|
// Enable app media type if sender application is present
|
|
if (selectionOpts.castInstance) {
|
|
defaultMediaType = ReceiverSelectorMediaType.App;
|
|
availableMediaTypes |= ReceiverSelectorMediaType.App;
|
|
}
|
|
|
|
// Disable mirroring media types if mirroring is not enabled
|
|
if (!opts.mirroringEnabled) {
|
|
availableMediaTypes &= ~(
|
|
ReceiverSelectorMediaType.Tab | ReceiverSelectorMediaType.Screen
|
|
);
|
|
}
|
|
|
|
// Ensure status manager is initialized
|
|
await deviceManager.init();
|
|
|
|
let appInfo: Optional<ReceiverSelectorAppInfo>;
|
|
if (selectionOpts.castInstance?.apiConfig) {
|
|
if (!baseConfig) {
|
|
try {
|
|
baseConfig = (await baseConfigStorage.get("baseConfig"))
|
|
.baseConfig;
|
|
} catch (err) {
|
|
throw logger.error("Failed to get Chromecast base config!");
|
|
}
|
|
}
|
|
|
|
appInfo = {
|
|
sessionRequest:
|
|
selectionOpts.castInstance.apiConfig?.sessionRequest,
|
|
isRequestAppAudioCompatible: getAppTag(
|
|
baseConfig,
|
|
selectionOpts.castInstance.apiConfig?.sessionRequest.appId
|
|
)?.supports_audio_only
|
|
};
|
|
}
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
// Close an existing open selector
|
|
if (receiverSelector?.isOpen) {
|
|
await receiverSelector.close();
|
|
}
|
|
receiverSelector = createSelector();
|
|
|
|
// Handle selected return value
|
|
const onSelected = (ev: CustomEvent<ReceiverSelection>) =>
|
|
resolve(ev.detail);
|
|
receiverSelector.addEventListener("selected", onSelected);
|
|
|
|
// Handle cancelled return value
|
|
const onCancelled = () => resolve(null);
|
|
receiverSelector.addEventListener("cancelled", onCancelled);
|
|
|
|
const onError = (ev: CustomEvent<string>) => reject(ev.detail);
|
|
receiverSelector.addEventListener("error", onError);
|
|
|
|
// Cleanup listeners
|
|
receiverSelector.addEventListener(
|
|
"close",
|
|
() => {
|
|
receiverSelector?.removeEventListener("selected", onSelected);
|
|
receiverSelector?.removeEventListener("cancelled", onCancelled);
|
|
receiverSelector?.removeEventListener("error", onError);
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
receiverSelector.open({
|
|
devices: deviceManager.getDevices(),
|
|
defaultMediaType,
|
|
availableMediaTypes,
|
|
appInfo,
|
|
pageInfo
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates new ReceiverSelector object and adds listeners for
|
|
* updates/messages.
|
|
*/
|
|
function createSelector() {
|
|
// Get a new selector for each selection
|
|
const selector = new ReceiverSelector(
|
|
deviceManager.getBridgeInfo()?.isVersionCompatible ?? false
|
|
);
|
|
|
|
/**
|
|
* Sends message to cast instance to trigger stopped receiver action
|
|
* (if applicable).
|
|
*/
|
|
const onStop = (ev: CustomEvent<{ deviceId: string }>) => {
|
|
const castInstance = castManager.getInstanceByDeviceId(
|
|
ev.detail.deviceId
|
|
);
|
|
if (!castInstance) return;
|
|
|
|
const device = deviceManager.getDeviceById(ev.detail.deviceId);
|
|
if (!device) return;
|
|
|
|
castInstance.contentPort.postMessage({
|
|
subject: "cast:receiverAction",
|
|
data: {
|
|
receiver: createReceiver(device),
|
|
action: ReceiverAction.STOP
|
|
}
|
|
});
|
|
};
|
|
selector.addEventListener("stop", onStop);
|
|
|
|
// Forward receiver messages
|
|
const onReceiverMessage = (
|
|
ev: CustomEvent<ReceiverSelectorReceiverMessage>
|
|
) =>
|
|
deviceManager.sendReceiverMessage(
|
|
ev.detail.deviceId,
|
|
ev.detail.message
|
|
);
|
|
selector.addEventListener("receiverMessage", onReceiverMessage);
|
|
|
|
// Forward media messages
|
|
const onMediaMessage = (ev: CustomEvent<ReceiverSelectorMediaMessage>) =>
|
|
deviceManager.sendMediaMessage(ev.detail.deviceId, ev.detail.message);
|
|
selector.addEventListener("mediaMessage", onMediaMessage);
|
|
|
|
// Update selector data whenever devices change/update
|
|
const onDeviceChange = () => {
|
|
const connectedSessionIds: string[] = [];
|
|
for (const instance of activeInstances) {
|
|
if (instance.session) {
|
|
connectedSessionIds.push(instance.session.sessionId);
|
|
}
|
|
}
|
|
|
|
selector.update(deviceManager.getDevices(), connectedSessionIds);
|
|
};
|
|
|
|
deviceManager.addEventListener("deviceUp", onDeviceChange);
|
|
deviceManager.addEventListener("deviceDown", onDeviceChange);
|
|
deviceManager.addEventListener("deviceUpdated", onDeviceChange);
|
|
deviceManager.addEventListener("deviceMediaUpdated", onDeviceChange);
|
|
|
|
// Cleanup listeners
|
|
selector.addEventListener(
|
|
"close",
|
|
() => {
|
|
deviceManager.removeEventListener("deviceUp", onDeviceChange);
|
|
deviceManager.removeEventListener("deviceDown", onDeviceChange);
|
|
deviceManager.removeEventListener("deviceUpdated", onDeviceChange);
|
|
deviceManager.removeEventListener(
|
|
"deviceMediaUpdated",
|
|
onDeviceChange
|
|
);
|
|
|
|
selector.removeEventListener("stop", onStop);
|
|
selector.removeEventListener("receiverMessage", onReceiverMessage);
|
|
selector.removeEventListener("mediaMessage", onMediaMessage);
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
return selector;
|
|
}
|
|
|
|
export default castManager;
|