mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-10 09:39:58 +00:00
Fix longstanding architectural issues
This commit is contained in:
@@ -4,23 +4,15 @@ import logger from "../lib/logger";
|
||||
import messaging, { Port, Message } from "../messaging";
|
||||
import options from "../lib/options";
|
||||
import { TypedEventTarget } from "../lib/TypedEventTarget";
|
||||
import { getMediaTypesForPageUrl } from "../lib/utils";
|
||||
|
||||
import {
|
||||
import type { SenderMediaMessage, SenderMessage } from "../cast/sdk/types";
|
||||
import type {
|
||||
ReceiverDevice,
|
||||
ReceiverSelectorAppInfo,
|
||||
ReceiverSelectorMediaType,
|
||||
ReceiverSelectorPageInfo
|
||||
} from "../types";
|
||||
|
||||
import deviceManager from "./deviceManager";
|
||||
import castManager from "./castManager";
|
||||
|
||||
import { BaseConfig, baseConfigStorage, getAppTag } from "../cast/googleApi";
|
||||
import type { SenderMediaMessage, SenderMessage } from "../cast/sdk/types";
|
||||
import type { SessionRequest } from "../cast/sdk/classes";
|
||||
import { ReceiverAction } from "../cast/sdk/enums";
|
||||
import { createReceiver } from "../cast/utils";
|
||||
|
||||
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
|
||||
|
||||
export interface ReceiverSelection {
|
||||
@@ -47,8 +39,6 @@ interface ReceiverSelectorEvents {
|
||||
mediaMessage: ReceiverSelectorMediaMessage;
|
||||
}
|
||||
|
||||
let baseConfig: BaseConfig;
|
||||
|
||||
/**
|
||||
* Manages the receiver selector popup window and communication with the
|
||||
* extension page hosted within.
|
||||
@@ -68,8 +58,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
||||
|
||||
private wasReceiverSelected = false;
|
||||
|
||||
private appId?: string;
|
||||
|
||||
appInfo?: ReceiverSelectorAppInfo;
|
||||
pageInfo?: ReceiverSelectorPageInfo;
|
||||
|
||||
constructor() {
|
||||
@@ -102,10 +91,10 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
||||
receiverDevices: ReceiverDevice[];
|
||||
defaultMediaType: ReceiverSelectorMediaType;
|
||||
availableMediaTypes: ReceiverSelectorMediaType;
|
||||
appId?: string;
|
||||
appInfo?: ReceiverSelectorAppInfo;
|
||||
pageInfo?: ReceiverSelectorPageInfo;
|
||||
}) {
|
||||
this.appId = opts.appId;
|
||||
this.appInfo = opts.appInfo;
|
||||
this.pageInfo = opts.pageInfo;
|
||||
|
||||
// If popup already exists, close it
|
||||
@@ -178,8 +167,6 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
||||
await browser.windows.remove(this.windowId);
|
||||
}
|
||||
|
||||
this.appId = undefined;
|
||||
|
||||
if (this.messagePort && !this.messagePortDisconnected) {
|
||||
this.messagePort.disconnect();
|
||||
}
|
||||
@@ -220,7 +207,10 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
||||
|
||||
this.messagePort.postMessage({
|
||||
subject: "popup:init",
|
||||
data: { appId: this.appId, pageInfo: this.pageInfo }
|
||||
data: {
|
||||
appInfo: this.appInfo,
|
||||
pageInfo: this.pageInfo
|
||||
}
|
||||
});
|
||||
|
||||
this.messagePort.postMessage({
|
||||
@@ -303,226 +293,4 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static sharedInstance = new ReceiverSelector();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
static async getSelection(
|
||||
contextTabId: number,
|
||||
contextFrameId = 0,
|
||||
selectionOpts?: {
|
||||
sessionRequest?: SessionRequest;
|
||||
withMediaSender?: boolean;
|
||||
}
|
||||
): Promise<ReceiverSelection | null> {
|
||||
let castInstance = castManager.getInstance(
|
||||
contextTabId,
|
||||
contextFrameId
|
||||
);
|
||||
/**
|
||||
* If the current context is running the mirroring app, pretend
|
||||
* it doesn't exist because it shouldn't be launched like this.
|
||||
*/
|
||||
if (
|
||||
castInstance?.apiConfig?.sessionRequest.appId ===
|
||||
(await options.get("mirroringAppId"))
|
||||
) {
|
||||
castInstance = undefined;
|
||||
}
|
||||
|
||||
let defaultMediaType = ReceiverSelectorMediaType.Tab;
|
||||
let availableMediaTypes = ReceiverSelectorMediaType.None;
|
||||
|
||||
let pageUrl: string | undefined;
|
||||
try {
|
||||
pageUrl = (
|
||||
await browser.webNavigation.getFrame({
|
||||
tabId: contextTabId,
|
||||
frameId: contextFrameId
|
||||
})
|
||||
).url;
|
||||
|
||||
availableMediaTypes = getMediaTypesForPageUrl(pageUrl);
|
||||
} catch {
|
||||
logger.error(
|
||||
"Failed to locate frame, falling back to default available media types."
|
||||
);
|
||||
}
|
||||
|
||||
// Enable app media type if sender application is present
|
||||
if (castInstance || selectionOpts?.withMediaSender) {
|
||||
defaultMediaType = ReceiverSelectorMediaType.App;
|
||||
availableMediaTypes |= ReceiverSelectorMediaType.App;
|
||||
}
|
||||
|
||||
const opts = await options.getAll();
|
||||
|
||||
// Disable mirroring media types if mirroring is not enabled
|
||||
if (!opts.mirroringEnabled) {
|
||||
availableMediaTypes &= ~(
|
||||
ReceiverSelectorMediaType.Tab | ReceiverSelectorMediaType.Screen
|
||||
);
|
||||
}
|
||||
|
||||
// Remove file media type if local media is not enabled
|
||||
if (!opts.mediaEnabled || !opts.localMediaEnabled) {
|
||||
availableMediaTypes &= ~ReceiverSelectorMediaType.File;
|
||||
}
|
||||
|
||||
// Ensure status manager is initialized
|
||||
await deviceManager.init();
|
||||
|
||||
let isRequestAppAudioCompatible: Optional<boolean>;
|
||||
if (castInstance?.apiConfig?.sessionRequest.appId) {
|
||||
if (!baseConfig) {
|
||||
try {
|
||||
baseConfig = (await baseConfigStorage.get("baseConfig"))
|
||||
.baseConfig;
|
||||
} catch (err) {
|
||||
throw logger.error("Failed to get Chromecast base config!");
|
||||
}
|
||||
}
|
||||
|
||||
isRequestAppAudioCompatible = getAppTag(
|
||||
baseConfig,
|
||||
castInstance.apiConfig?.sessionRequest.appId
|
||||
)?.supports_audio_only;
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Close an existing open selector
|
||||
if (ReceiverSelector.sharedInstance.isOpen) {
|
||||
await ReceiverSelector.sharedInstance.close();
|
||||
}
|
||||
|
||||
const selector = createSelector();
|
||||
ReceiverSelector.sharedInstance = selector;
|
||||
|
||||
// Handle selected return value
|
||||
const onSelected = (ev: CustomEvent<ReceiverSelection>) =>
|
||||
resolve(ev.detail);
|
||||
selector.addEventListener("selected", onSelected);
|
||||
|
||||
// Handle cancelled return value
|
||||
const onCancelled = () => resolve(null);
|
||||
selector.addEventListener("cancelled", onCancelled);
|
||||
|
||||
const onError = (ev: CustomEvent<string>) => reject(ev.detail);
|
||||
selector.addEventListener("error", onError);
|
||||
|
||||
// Cleanup listeners
|
||||
selector.addEventListener(
|
||||
"close",
|
||||
() => {
|
||||
selector.removeEventListener("selected", onSelected);
|
||||
selector.removeEventListener("cancelled", onCancelled);
|
||||
selector.removeEventListener("error", onError);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
selector.open({
|
||||
receiverDevices: deviceManager.getDevices(),
|
||||
defaultMediaType,
|
||||
availableMediaTypes,
|
||||
appId: castInstance?.apiConfig?.sessionRequest.appId,
|
||||
// Create page info
|
||||
pageInfo: pageUrl
|
||||
? {
|
||||
url: pageUrl,
|
||||
tabId: contextTabId,
|
||||
frameId: contextFrameId,
|
||||
sessionRequest: selectionOpts?.sessionRequest,
|
||||
isRequestAppAudioCompatible
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new ReceiverSelector object and adds listeners for
|
||||
* updates/messages.
|
||||
*/
|
||||
function createSelector() {
|
||||
// Get a new selector for each selection
|
||||
const selector = new ReceiverSelector();
|
||||
ReceiverSelector.sharedInstance = selector;
|
||||
|
||||
/**
|
||||
* 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:sendReceiverAction",
|
||||
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 = () => selector.update(deviceManager.getDevices());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,10 @@ import bridge, { BridgeInfo } from "../lib/bridge";
|
||||
|
||||
import castManager from "./castManager";
|
||||
import deviceManager from "./deviceManager";
|
||||
import ReceiverSelector from "./ReceiverSelector";
|
||||
|
||||
import { initMenus } from "./menus";
|
||||
import { initWhitelist } from "./whitelist";
|
||||
import { baseConfigStorage, fetchBaseConfig } from "../cast/googleApi";
|
||||
import { baseConfigStorage, fetchBaseConfig } from "../lib/chromecastConfigApi";
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
@@ -133,26 +132,13 @@ async function init() {
|
||||
await initMenus();
|
||||
await initWhitelist();
|
||||
|
||||
/**
|
||||
* When the browser action is clicked, open a receiver selector and
|
||||
* load a sender for the response. The mirroring sender is loaded
|
||||
* into the current tab at the
|
||||
* top-level frame.
|
||||
*/
|
||||
browser.browserAction.onClicked.addListener(async tab => {
|
||||
if (tab.id === undefined) {
|
||||
logger.error("Tab ID not found in browser action handler.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = await ReceiverSelector.getSelection(tab.id);
|
||||
if (selection) {
|
||||
castManager.loadSender({
|
||||
tabId: tab.id,
|
||||
frameId: 0,
|
||||
selection
|
||||
});
|
||||
}
|
||||
castManager.triggerCast(tab.id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
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 { stringify } from "../lib/utils";
|
||||
import { getMediaTypesForPageUrl, stringify } from "../lib/utils";
|
||||
import type { TypedMessagePort } from "../lib/TypedMessagePort";
|
||||
|
||||
import { ReceiverSelectorMediaType } from "../types";
|
||||
import {
|
||||
ReceiverSelectorAppInfo,
|
||||
ReceiverSelectorMediaType,
|
||||
ReceiverSelectorPageInfo
|
||||
} from "../types";
|
||||
|
||||
import type { ApiConfig } from "../cast/sdk/classes";
|
||||
import { ReceiverAction } from "../cast/sdk/enums";
|
||||
import { createReceiver } from "../cast/utils";
|
||||
|
||||
import deviceManager from "./deviceManager";
|
||||
import ReceiverSelector, { ReceiverSelection } from "./ReceiverSelector";
|
||||
|
||||
type AnyPort = Port | MessagePort;
|
||||
import ReceiverSelector, {
|
||||
ReceiverSelection,
|
||||
ReceiverSelectorMediaMessage,
|
||||
ReceiverSelectorReceiverMessage
|
||||
} from "./ReceiverSelector";
|
||||
|
||||
export interface CastInstance {
|
||||
bridgePort: Port;
|
||||
contentPort: AnyPort;
|
||||
contentTabId?: number;
|
||||
contentFrameId?: number;
|
||||
type AnyPort = Port | TypedMessagePort<Message>;
|
||||
|
||||
/** ApiConfig provided on initialization. */
|
||||
apiConfig?: ApiConfig;
|
||||
/** Established session details. */
|
||||
session?: CastSession;
|
||||
export interface ContentContext {
|
||||
tabId: number;
|
||||
frameId: number;
|
||||
}
|
||||
|
||||
interface CastSession {
|
||||
@@ -34,11 +42,73 @@ interface CastSession {
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export interface CastInstance {
|
||||
bridgePort: Port;
|
||||
contentPort: AnyPort;
|
||||
contentContext?: ContentContext;
|
||||
|
||||
/** 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 };
|
||||
}) {
|
||||
const instance: CastInstance = {
|
||||
bridgePort: opts.bridgePort ?? (await bridge.connect()),
|
||||
contentPort: opts.contentPort
|
||||
};
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/** Keeps track of cast API instances and provides bridge messaging. */
|
||||
export default new (class {
|
||||
const castManager = new (class {
|
||||
private activeInstances = new Set<CastInstance>();
|
||||
|
||||
public async init() {
|
||||
async init() {
|
||||
// Handle incoming instance connections
|
||||
messaging.onConnect.addListener(async port => {
|
||||
if (port.name === "cast") {
|
||||
@@ -52,7 +122,7 @@ export default new (class {
|
||||
|
||||
for (const instance of this.activeInstances) {
|
||||
instance.contentPort.postMessage({
|
||||
subject: "cast:updateReceiverAvailability",
|
||||
subject: "cast:receiverAvailabilityUpdated",
|
||||
data: { isAvailable }
|
||||
});
|
||||
}
|
||||
@@ -68,11 +138,11 @@ export default new (class {
|
||||
/**
|
||||
* Finds a cast instance at the given tab (and optionally frame) ID.
|
||||
*/
|
||||
public getInstance(tabId: number, frameId?: number) {
|
||||
getInstanceAt(tabId: number, frameId?: number) {
|
||||
for (const instance of this.activeInstances) {
|
||||
if (instance.contentTabId === tabId) {
|
||||
if (instance.contentContext?.tabId === tabId) {
|
||||
// If frame ID doesn't match go to next instance
|
||||
if (frameId && instance.contentFrameId !== frameId) {
|
||||
if (frameId && instance.contentContext.frameId !== frameId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -81,7 +151,7 @@ export default new (class {
|
||||
}
|
||||
}
|
||||
|
||||
public getInstanceByDeviceId(deviceId: string) {
|
||||
getInstanceByDeviceId(deviceId: string) {
|
||||
for (const instance of this.activeInstances) {
|
||||
if (instance.session?.deviceId === deviceId) return instance;
|
||||
}
|
||||
@@ -91,9 +161,9 @@ export default new (class {
|
||||
* Creates a cast instance with a given port and connects messaging
|
||||
* correctly depending on the type of port.
|
||||
*/
|
||||
public async createInstance(port: AnyPort) {
|
||||
async createInstance(port: AnyPort, contentContext?: ContentContext) {
|
||||
const instance = await (port instanceof MessagePort
|
||||
? this.createInstanceFromBackground(port)
|
||||
? this.createInstanceFromBackground(port, contentContext)
|
||||
: this.createInstanceFromContent(port));
|
||||
|
||||
this.activeInstances.add(instance);
|
||||
@@ -108,27 +178,41 @@ export default new (class {
|
||||
|
||||
/** Creates a cast instance with a `MessagePort` content port. */
|
||||
private async createInstanceFromBackground(
|
||||
contentPort: MessagePort
|
||||
contentPort: MessagePort,
|
||||
contentContext?: ContentContext
|
||||
): Promise<CastInstance> {
|
||||
const instance: CastInstance = {
|
||||
const instance = await createCastInstance({
|
||||
bridgePort: await bridge.connect(),
|
||||
contentPort
|
||||
};
|
||||
contentPort,
|
||||
contentContext
|
||||
});
|
||||
|
||||
// Ensure only one instance per context
|
||||
if (contentContext) {
|
||||
for (const instance of this.activeInstances) {
|
||||
if (isSameContext(instance.contentContext, contentContext)) {
|
||||
instance.bridgePort.disconnect();
|
||||
this.activeInstances.delete(instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance.bridgePort.onDisconnect.addListener(() => {
|
||||
contentPort.close();
|
||||
this.activeInstances.delete(instance);
|
||||
});
|
||||
|
||||
// bridge -> content
|
||||
// bridge -> cast instance
|
||||
instance.bridgePort.onMessage.addListener(message => {
|
||||
this.handleBridgeMessage(instance, message);
|
||||
});
|
||||
|
||||
// content -> (any)
|
||||
// cast instance -> (any)
|
||||
contentPort.addEventListener("message", ev => {
|
||||
this.handleContentMessage(instance, ev.data);
|
||||
});
|
||||
contentPort.start();
|
||||
|
||||
return instance;
|
||||
}
|
||||
@@ -148,33 +232,27 @@ export default new (class {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there's already an active instance for the sender
|
||||
* tab/frame ID, disconnect it.
|
||||
*
|
||||
* TODO: Fix this behaviour!
|
||||
*/
|
||||
// Ensure only one instance per context
|
||||
for (const instance of this.activeInstances) {
|
||||
if (
|
||||
instance.contentTabId === contentPort.sender.tab.id &&
|
||||
instance.contentFrameId === contentPort.sender.frameId
|
||||
isSameContext(
|
||||
instance.contentContext,
|
||||
contentPort.sender as ContentContext
|
||||
)
|
||||
) {
|
||||
instance.bridgePort.disconnect();
|
||||
disconnectContentPort(instance.contentPort);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const instance: CastInstance = {
|
||||
bridgePort: await bridge.connect(),
|
||||
contentPort,
|
||||
contentTabId: contentPort.sender.tab.id,
|
||||
contentFrameId: contentPort.sender.frameId
|
||||
};
|
||||
const instance = await createCastInstance({ contentPort });
|
||||
|
||||
// content -> (any)
|
||||
// cast instance -> (any)
|
||||
const onContentPortMessage = (message: Message) => {
|
||||
this.handleContentMessage(instance, message);
|
||||
};
|
||||
// bridge -> content
|
||||
// bridge -> cast instance
|
||||
const onBridgePortMessage = (message: Message) => {
|
||||
this.handleBridgeMessage(instance, message);
|
||||
};
|
||||
@@ -206,16 +284,17 @@ export default new (class {
|
||||
switch (message.subject) {
|
||||
case "main:castSessionCreated": {
|
||||
// Close after session is created
|
||||
const selector = ReceiverSelector.sharedInstance;
|
||||
if (
|
||||
selector.isOpen &&
|
||||
receiverSelector?.isOpen &&
|
||||
// If selector context is the same as the instance context
|
||||
selector.pageInfo?.tabId === instance.contentTabId &&
|
||||
selector.pageInfo?.frameId === instance.contentFrameId &&
|
||||
receiverSelector.pageInfo?.tabId ===
|
||||
instance.contentContext?.tabId &&
|
||||
receiverSelector.pageInfo?.frameId ===
|
||||
instance.contentContext?.frameId &&
|
||||
// If selector is supposed to close
|
||||
(await options.get("receiverSelectorWaitForConnection"))
|
||||
) {
|
||||
selector.close();
|
||||
receiverSelector.close();
|
||||
}
|
||||
|
||||
const { receiverId: deviceId } = message.data;
|
||||
@@ -270,44 +349,30 @@ export default new (class {
|
||||
}
|
||||
|
||||
switch (message.subject) {
|
||||
// Cast API has been initialized
|
||||
case "main:initializeCast": {
|
||||
case "main:initializeCast":
|
||||
instance.apiConfig = message.data.apiConfig;
|
||||
|
||||
instance.contentPort.postMessage({
|
||||
subject: "cast:updateReceiverAvailability",
|
||||
subject: "cast:receiverAvailabilityUpdated",
|
||||
data: {
|
||||
isAvailable: deviceManager.getDevices().length > 0
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// User has triggered receiver selection via the cast API
|
||||
case "main:selectReceiver": {
|
||||
if (
|
||||
instance.contentTabId === undefined ||
|
||||
instance.contentFrameId === undefined
|
||||
) {
|
||||
throw logger.error(
|
||||
"Cast instance associated with content sender missing tab/frame ID"
|
||||
);
|
||||
}
|
||||
|
||||
case "main:requestSession": {
|
||||
const { sessionRequest } = message.data;
|
||||
|
||||
try {
|
||||
const selection = await ReceiverSelector.getSelection(
|
||||
instance.contentTabId,
|
||||
instance.contentFrameId,
|
||||
{ sessionRequest }
|
||||
);
|
||||
const selection = await getReceiverSelection({
|
||||
castInstance: instance
|
||||
});
|
||||
|
||||
// Handle cancellation
|
||||
if (!selection) {
|
||||
instance.contentPort.postMessage({
|
||||
subject: "cast:selectReceiver/cancelled"
|
||||
subject: "cast:sessionRequestCancelled"
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -320,14 +385,13 @@ export default new (class {
|
||||
*/
|
||||
if (selection.mediaType !== ReceiverSelectorMediaType.App) {
|
||||
instance.contentPort.postMessage({
|
||||
subject: "cast:selectReceiver/cancelled"
|
||||
subject: "cast:sessionRequestCancelled"
|
||||
});
|
||||
|
||||
this.loadSender({
|
||||
tabId: instance.contentTabId,
|
||||
frameId: instance.contentFrameId,
|
||||
selection
|
||||
});
|
||||
if (!instance.contentContext) {
|
||||
throw logger.error("Missing content context");
|
||||
}
|
||||
this.loadSender(selection, instance.contentContext);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -342,7 +406,7 @@ export default new (class {
|
||||
} catch (err) {
|
||||
// TODO: Report errors properly
|
||||
instance.contentPort.postMessage({
|
||||
subject: "cast:selectReceiver/cancelled"
|
||||
subject: "cast:sessionRequestCancelled"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -352,25 +416,45 @@ export default new (class {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the appropriate sender for a given receiver selector
|
||||
* response.
|
||||
* Gets a receiver selection and loads the appropriate sender for a
|
||||
* given context.
|
||||
*/
|
||||
public async loadSender(opts: {
|
||||
tabId: number;
|
||||
frameId?: number;
|
||||
selection: ReceiverSelection;
|
||||
}) {
|
||||
// Cancelled
|
||||
if (!opts.selection) {
|
||||
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;
|
||||
}
|
||||
|
||||
switch (opts.selection.mediaType) {
|
||||
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.getInstance(opts.tabId, opts.frameId);
|
||||
const instance = this.getInstanceAt(
|
||||
contentContext.tabId,
|
||||
contentContext.frameId
|
||||
);
|
||||
if (!instance) {
|
||||
throw logger.error(
|
||||
`Cast instance not found at tabId ${opts.tabId} / frameId ${opts.frameId}`
|
||||
`Cast instance not found at tabId ${contentContext.tabId} / frameId ${contentContext.frameId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -379,9 +463,9 @@ export default new (class {
|
||||
}
|
||||
|
||||
instance.contentPort.postMessage({
|
||||
subject: "cast:sendReceiverAction",
|
||||
subject: "cast:receiverAction",
|
||||
data: {
|
||||
receiver: createReceiver(opts.selection.receiverDevice),
|
||||
receiver: createReceiver(selection.receiverDevice),
|
||||
action: ReceiverAction.CAST
|
||||
}
|
||||
});
|
||||
@@ -390,7 +474,7 @@ export default new (class {
|
||||
subject: "bridge:createCastSession",
|
||||
data: {
|
||||
appId: instance.apiConfig?.sessionRequest.appId,
|
||||
receiverDevice: opts.selection.receiverDevice
|
||||
receiverDevice: selection.receiverDevice
|
||||
}
|
||||
});
|
||||
|
||||
@@ -398,34 +482,239 @@ export default new (class {
|
||||
}
|
||||
|
||||
case ReceiverSelectorMediaType.Tab:
|
||||
case ReceiverSelectorMediaType.Screen: {
|
||||
await browser.tabs.executeScript(opts.tabId, {
|
||||
case ReceiverSelectorMediaType.Screen:
|
||||
await browser.tabs.executeScript(contentContext.tabId, {
|
||||
code: stringify`
|
||||
window.selectedMedia = ${opts.selection.mediaType};
|
||||
window.selectedReceiver = ${opts.selection.receiverDevice};
|
||||
window.selectedMedia = ${selection.mediaType};
|
||||
window.selectedReceiver = ${selection.receiverDevice};
|
||||
`,
|
||||
frameId: opts.frameId
|
||||
frameId: contentContext.frameId
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(opts.tabId, {
|
||||
await browser.tabs.executeScript(contentContext.tabId, {
|
||||
file: "cast/senders/mirroring.js",
|
||||
frameId: opts.frameId
|
||||
frameId: contentContext.frameId
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ReceiverSelectorMediaType.File: {
|
||||
const fileUrl = new URL(`file://${opts.selection.filePath}`);
|
||||
const { init } = await import("../cast/senders/media");
|
||||
|
||||
init({
|
||||
mediaUrl: fileUrl.href,
|
||||
receiver: opts.selection.receiverDevice
|
||||
});
|
||||
|
||||
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) selectionOpts.frameId = 0;
|
||||
|
||||
// Fallback to instance context
|
||||
if (!selectionOpts.tabId && selectionOpts.castInstance?.contentContext) {
|
||||
selectionOpts.tabId = selectionOpts.castInstance.contentContext.tabId;
|
||||
selectionOpts.frameId =
|
||||
selectionOpts.castInstance.contentContext.frameId;
|
||||
}
|
||||
|
||||
let pageInfo: Optional<ReceiverSelectorPageInfo>;
|
||||
if (selectionOpts.tabId) {
|
||||
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;
|
||||
}
|
||||
|
||||
const opts = await options.getAll();
|
||||
|
||||
// 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({
|
||||
receiverDevices: 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();
|
||||
|
||||
/**
|
||||
* 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 = () => selector.update(deviceManager.getDevices());
|
||||
|
||||
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;
|
||||
|
||||
@@ -4,13 +4,10 @@ import logger from "../lib/logger";
|
||||
import options from "../lib/options";
|
||||
import { stringify } from "../lib/utils";
|
||||
|
||||
import { ReceiverSelectorMediaType } from "../types";
|
||||
|
||||
import ReceiverSelector, { ReceiverSelection } from "./ReceiverSelector";
|
||||
import castManager from "./castManager";
|
||||
|
||||
import * as menuIds from "../menuIds";
|
||||
|
||||
import castManager from "./castManager";
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
const URL_PATTERN_HTTP = "http://*/*";
|
||||
@@ -176,49 +173,21 @@ async function onMenuClicked(
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle cast menus
|
||||
const castMenuClicked = info.menuItemId === menuIdCast;
|
||||
const castMediaMenuClicked = info.menuItemId === menuIdCastMedia;
|
||||
if (castMenuClicked || castMediaMenuClicked) {
|
||||
if (tab?.id === undefined) {
|
||||
throw logger.error("Menu handler tab ID not found.");
|
||||
}
|
||||
if (!info.pageUrl) {
|
||||
throw logger.error("Menu handler page URL not found.");
|
||||
if (tab?.id === undefined) {
|
||||
logger.error("Menu handler tab ID not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info.menuItemId) {
|
||||
case menuIdCast: {
|
||||
castManager.triggerCast(tab.id, info.frameId);
|
||||
break;
|
||||
}
|
||||
|
||||
let selection: Nullable<ReceiverSelection> = null;
|
||||
try {
|
||||
selection = await ReceiverSelector.getSelection(
|
||||
tab.id,
|
||||
info.frameId,
|
||||
{
|
||||
withMediaSender: castMediaMenuClicked
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error("Failed to get receiver selection (cast menu)", err);
|
||||
return;
|
||||
}
|
||||
// Invalid selection result
|
||||
if (!selection) return;
|
||||
|
||||
if (castMenuClicked) {
|
||||
castManager.loadSender({
|
||||
tabId: tab.id,
|
||||
frameId: info.frameId,
|
||||
selection
|
||||
});
|
||||
} else if (castMediaMenuClicked) {
|
||||
/**
|
||||
* If the selected media type is App, that refers to
|
||||
* the media sender in this context, so load media
|
||||
* sender.
|
||||
*/
|
||||
if (selection.mediaType === ReceiverSelectorMediaType.App) {
|
||||
case menuIdCastMedia:
|
||||
if (info.srcUrl) {
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
code: stringify`
|
||||
window.receiver = ${selection.receiverDevice};
|
||||
window.mediaUrl = ${info.srcUrl};
|
||||
window.targetElementId = ${info.targetElementId};
|
||||
`,
|
||||
@@ -226,18 +195,11 @@ async function onMenuClicked(
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "cast/senders/media/index.js",
|
||||
file: "cast/senders/media.js",
|
||||
frameId: info.frameId
|
||||
});
|
||||
} else {
|
||||
// Handle other responses
|
||||
castManager.loadSender({
|
||||
tabId: tab.id,
|
||||
frameId: info.frameId,
|
||||
selection
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { RemoteMatchPattern } from "../lib/matchPattern";
|
||||
import {
|
||||
CAST_FRAMEWORK_LOADER_SCRIPT_URL,
|
||||
CAST_LOADER_SCRIPT_URL
|
||||
} from "../cast/endpoints";
|
||||
} from "../cast/urls";
|
||||
|
||||
// Missing on @types/firefox-webext-browser
|
||||
type OnBeforeSendHeadersDetails = Parameters<
|
||||
@@ -207,7 +207,7 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
|
||||
});
|
||||
|
||||
return {
|
||||
redirectUrl: browser.runtime.getURL("cast/index.js")
|
||||
redirectUrl: browser.runtime.getURL("cast/content.js")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -250,6 +250,13 @@ async function registerSiteWhitelist() {
|
||||
{ urls: ["<all_urls>"] },
|
||||
["blocking", "requestHeaders"]
|
||||
);
|
||||
|
||||
browser.contentScripts.register({
|
||||
matches: siteWhitelist.map(item => item.pattern),
|
||||
js: [{ file: "cast/contentInitial.js" }],
|
||||
runAt: "document_start",
|
||||
allFrames: true
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSiteWhitelist() {
|
||||
|
||||
@@ -1,42 +1,48 @@
|
||||
"use strict";
|
||||
|
||||
import { CAST_LOADER_SCRIPT_URL, CAST_SCRIPT_URLS } from "./endpoints";
|
||||
|
||||
const _window = window.wrappedJSObject as any;
|
||||
|
||||
_window.chrome = cloneInto({}, window);
|
||||
|
||||
/**
|
||||
* YouTube won't load the cast SDK unless it thinks the
|
||||
* presentation API exists.
|
||||
* Cast Sender SDK page script loaded in place of remote cast_sender
|
||||
* script. Handles API object creation and initializes sender apps.
|
||||
*/
|
||||
if (window.location.host === "www.youtube.com") {
|
||||
_window.navigator.presentation = cloneInto({}, window);
|
||||
|
||||
import logger from "../lib/logger";
|
||||
import { loadScript } from "../lib/utils";
|
||||
|
||||
import pageMessenging from "./pageMessenging";
|
||||
import CastSDK from "./sdk";
|
||||
import { CAST_FRAMEWORK_SCRIPT_URL } from "./urls";
|
||||
|
||||
// Create page-accessible API object
|
||||
window.chrome.cast = new CastSDK();
|
||||
|
||||
let frameworkScriptPromise: Promise<HTMLScriptElement> | undefined;
|
||||
|
||||
// Load remote CAF script if requested in script URL params.
|
||||
if (document.currentScript) {
|
||||
const currentScript = document.currentScript as HTMLScriptElement;
|
||||
const currentScriptParams = new URLSearchParams(
|
||||
new URL(currentScript.src).search
|
||||
);
|
||||
|
||||
if (currentScriptParams.get("loadCastFramework") === "1") {
|
||||
frameworkScriptPromise = loadScript(CAST_FRAMEWORK_SCRIPT_URL);
|
||||
frameworkScriptPromise.catch(() => {
|
||||
logger.error("Failed to load CAF script!");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the src property setter on <script> elements to
|
||||
* intercept the new value.
|
||||
*
|
||||
* If it matches one of Chrome's cast extension sender script
|
||||
* URLs, replace it with the standard API URL, the request for
|
||||
* which is handled in the main script.
|
||||
*/
|
||||
const desc = Reflect.getOwnPropertyDescriptor(
|
||||
HTMLScriptElement.prototype.wrappedJSObject,
|
||||
"src"
|
||||
);
|
||||
pageMessenging.page.addListener(async message => {
|
||||
switch (message.subject) {
|
||||
case "cast:initialized": {
|
||||
// If framework API is loading, wait until completed
|
||||
await frameworkScriptPromise;
|
||||
|
||||
Reflect.defineProperty(HTMLScriptElement.prototype.wrappedJSObject, "src", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: desc?.get,
|
||||
// Call page script/framework API script's init function
|
||||
const initFn = window.__onGCastApiAvailable;
|
||||
if (initFn && typeof initFn === "function") {
|
||||
initFn(message.data.isAvailable);
|
||||
}
|
||||
|
||||
set: exportFunction(function (this: HTMLScriptElement, value: string) {
|
||||
if (CAST_SCRIPT_URLS.includes(value)) {
|
||||
return desc?.set?.call(this, CAST_LOADER_SCRIPT_URL);
|
||||
break;
|
||||
}
|
||||
|
||||
return desc?.set?.call(this, value);
|
||||
}, window)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
import messaging, { Message } from "../messaging";
|
||||
import { PageEventMessenger, ExtensionEventMessenger } from "./eventMessaging";
|
||||
|
||||
// Create messengers manually instead of relying on getters
|
||||
const eventMessaging = {
|
||||
page: new PageEventMessenger(),
|
||||
extension: new ExtensionEventMessenger()
|
||||
};
|
||||
import pageMessenging from "./pageMessenging";
|
||||
|
||||
// Message port to background script
|
||||
export const backgroundPort = messaging.connect({ name: "cast" });
|
||||
export const managerPort = messaging.connect({ name: "cast" });
|
||||
|
||||
const forwardToPage = (message: Message) => {
|
||||
eventMessaging.extension.sendMessage(message);
|
||||
pageMessenging.extension.sendMessage(message);
|
||||
};
|
||||
const forwardToMain = (message: Message) => {
|
||||
backgroundPort.postMessage(message);
|
||||
managerPort.postMessage(message);
|
||||
};
|
||||
|
||||
// Add message listeners
|
||||
backgroundPort.onMessage.addListener(forwardToPage);
|
||||
eventMessaging.extension.addListener(forwardToMain);
|
||||
managerPort.onMessage.addListener(forwardToPage);
|
||||
pageMessenging.extension.addListener(forwardToMain);
|
||||
|
||||
// Remove listeners
|
||||
backgroundPort.onDisconnect.addListener(() => {
|
||||
backgroundPort.onMessage.removeListener(forwardToPage);
|
||||
eventMessaging.extension.addListener(forwardToMain);
|
||||
managerPort.onDisconnect.addListener(() => {
|
||||
pageMessenging.extension.close();
|
||||
});
|
||||
|
||||
57
ext/src/cast/contentInitial.ts
Normal file
57
ext/src/cast/contentInitial.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Content script loaded on whitelisted URLs. Sets some window
|
||||
* properties to help with Chrome compatibility and handles dynamic
|
||||
* chrome-extension:// cast script loads.
|
||||
*/
|
||||
|
||||
import { CAST_LOADER_SCRIPT_URL, CAST_SCRIPT_URLS } from "./urls";
|
||||
|
||||
declare global {
|
||||
interface Object {
|
||||
wrappedJSObject: this;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
wrappedJSObject: Window;
|
||||
chrome: {
|
||||
cast?: object;
|
||||
};
|
||||
__onGCastApiAvailable: (isAvailable: boolean) => void;
|
||||
}
|
||||
interface Navigator {
|
||||
presentation: object;
|
||||
}
|
||||
}
|
||||
|
||||
window.wrappedJSObject.chrome = cloneInto({}, window);
|
||||
|
||||
/**
|
||||
* YouTube won't load the cast SDK unless it thinks the presentation API
|
||||
* exists.
|
||||
*/
|
||||
if (window.location.host === "www.youtube.com") {
|
||||
window.wrappedJSObject.navigator.presentation = cloneInto({}, window);
|
||||
}
|
||||
|
||||
const srcPropDesc = Reflect.getOwnPropertyDescriptor(
|
||||
HTMLScriptElement.prototype.wrappedJSObject,
|
||||
"src"
|
||||
);
|
||||
/**
|
||||
* Intercept script element src attribute changes and rewrite cast
|
||||
* script URLs to the remote loader script URL to be redirected by the
|
||||
* extension's webRequest handlers in the background script.
|
||||
*/
|
||||
Reflect.defineProperty(HTMLScriptElement.prototype.wrappedJSObject, "src", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: srcPropDesc?.get,
|
||||
|
||||
set: exportFunction(function (this: HTMLScriptElement, value: string) {
|
||||
if (CAST_SCRIPT_URLS.includes(value)) {
|
||||
return srcPropDesc?.set?.call(this, CAST_LOADER_SCRIPT_URL);
|
||||
}
|
||||
|
||||
return srcPropDesc?.set?.call(this, value);
|
||||
}, window)
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import logger from "../lib/logger";
|
||||
import type { Message } from "../messaging";
|
||||
|
||||
type EventMessengerListener = (message: Message) => void;
|
||||
|
||||
/**
|
||||
* Messenger class for cross-context messages via CustomEvent.
|
||||
*
|
||||
* Supplied with an incoming and outgoing event name, it provides a
|
||||
* message channel from content scripts to page scripts provided that
|
||||
* the opposite event names are used with instances on either side.
|
||||
*
|
||||
* Note:
|
||||
* Extending EventTarget seems to cause issues with dispatching custom
|
||||
* events in WebExtension content scripts (sandbox issue?), so custom
|
||||
* addListener/removeListener methods are used instead.
|
||||
*/
|
||||
abstract class EventMessenger {
|
||||
private listeners = new Set<EventMessengerListener>();
|
||||
|
||||
constructor(
|
||||
private incomingMessageEventName: string,
|
||||
private outgoingMessageEventName: string
|
||||
) {
|
||||
// @ts-ignore
|
||||
document.addEventListener(
|
||||
this.incomingMessageEventName,
|
||||
(ev: CustomEvent<string>) => {
|
||||
for (const listener of this.listeners) {
|
||||
listener(JSON.parse(ev.detail));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
addListener(listener: EventMessengerListener) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
removeListener(listener: EventMessengerListener) {
|
||||
this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
sendMessage(message: Message) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<string>(this.outgoingMessageEventName, {
|
||||
detail: JSON.stringify(message)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const EV_TO_PAGE = "__castMessage";
|
||||
const EV_FROM_PAGE = "__castMessageResponse";
|
||||
|
||||
export class PageEventMessenger extends EventMessenger {
|
||||
constructor() {
|
||||
super(EV_TO_PAGE, EV_FROM_PAGE);
|
||||
}
|
||||
}
|
||||
export class ExtensionEventMessenger extends EventMessenger {
|
||||
constructor() {
|
||||
super(EV_FROM_PAGE, EV_TO_PAGE);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure only one instance of the type initially created is used
|
||||
let messenger: EventMessenger;
|
||||
function getMessenger(messengerType: { new (): EventMessenger }) {
|
||||
if (!messenger) {
|
||||
messenger = new messengerType();
|
||||
} else if (!(messenger instanceof messengerType)) {
|
||||
throw logger.error(
|
||||
"Requested messenger does not match type of instantiated messenger!"
|
||||
);
|
||||
}
|
||||
|
||||
return messenger;
|
||||
}
|
||||
|
||||
export default {
|
||||
/** Event messenger for page scripts. */
|
||||
get page() {
|
||||
return getMessenger(PageEventMessenger);
|
||||
},
|
||||
|
||||
/** Event messenger for extension content scripts. */
|
||||
get extension() {
|
||||
return getMessenger(ExtensionEventMessenger);
|
||||
}
|
||||
};
|
||||
@@ -1,111 +1,106 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
"use strict";
|
||||
|
||||
import type { TypedMessagePort } from "../lib/TypedMessagePort";
|
||||
import type { Message } from "../messaging";
|
||||
|
||||
import type { BridgeInfo } from "../lib/bridge";
|
||||
import type { TypedMessagePort } from "../lib/TypedMessagePort";
|
||||
|
||||
import pageMessenging from "./pageMessenging";
|
||||
import CastSDK from "./sdk";
|
||||
|
||||
import { PageEventMessenger, ExtensionEventMessenger } from "./eventMessaging";
|
||||
export type CastPort = TypedMessagePort<Message>;
|
||||
|
||||
// Create messengers manually instead of relying on getters
|
||||
const eventMessaging = {
|
||||
page: new PageEventMessenger(),
|
||||
extension: new ExtensionEventMessenger()
|
||||
};
|
||||
let existingPort: CastPort;
|
||||
let existingInstance = new CastSDK();
|
||||
|
||||
let initializedBridgeInfo: BridgeInfo;
|
||||
let initializedBackgroundPort: MessagePort;
|
||||
export default existingInstance;
|
||||
|
||||
/**
|
||||
* To support exporting an API from a module, we need to
|
||||
* retain the event-based message passing despite not
|
||||
* actually crossing any context boundaries. The cast instance
|
||||
* listens for and emits these messages, and changing that
|
||||
* behavior is too messy.
|
||||
* To support exporting the API from a module, we need to retain the
|
||||
* MessageChannel-based pageMessaging layer despite not crossing any
|
||||
* context boundaries.
|
||||
*
|
||||
* The ensureInit function creates a messaging connection to the
|
||||
* castManager, hooks it up to the pageMessaging layer and also provides
|
||||
* a messaging port so consumers of this module can communicate with the
|
||||
* castManager.
|
||||
*/
|
||||
export function ensureInit(): Promise<TypedMessagePort<Message>> {
|
||||
export function ensureInit(contextTabId?: number): Promise<CastPort> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// If already initialized, just return existing bridge info
|
||||
if (initializedBridgeInfo) {
|
||||
if (initializedBridgeInfo.isVersionCompatible) {
|
||||
resolve(initializedBackgroundPort);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
|
||||
return;
|
||||
// If already initialized
|
||||
if (existingPort) {
|
||||
existingPort.close();
|
||||
existingInstance = new CastSDK();
|
||||
}
|
||||
|
||||
const channel = new MessageChannel();
|
||||
initializedBackgroundPort = channel.port1;
|
||||
|
||||
/**
|
||||
* If the module is imported into a background script
|
||||
* context, the location will be the internal extension URL,
|
||||
* whereas in a content script, it will be the content page
|
||||
* URL.
|
||||
* If imported into a background script context, the location
|
||||
* will be the internal extension URL, whereas in a content
|
||||
* script, it will be the content page URL.
|
||||
*/
|
||||
if (window.location.protocol === "moz-extension:") {
|
||||
const { default: castManager } = await import(
|
||||
"../background/castManager"
|
||||
);
|
||||
|
||||
// port2 will post bridge messages to port 1
|
||||
await castManager.init();
|
||||
await castManager.createInstance(channel.port2);
|
||||
|
||||
// bridge -> cast instance
|
||||
channel.port1.onmessage = ev => {
|
||||
const message = ev.data as Message;
|
||||
|
||||
// Send message to cast instance
|
||||
eventMessaging.extension.sendMessage(message);
|
||||
handleIncomingMessageToCast(message);
|
||||
};
|
||||
|
||||
// cast instance -> bridge
|
||||
eventMessaging.extension.addListener(message =>
|
||||
channel.port1.postMessage(message)
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
* Import reference to message port created by contentBridge.
|
||||
* Creation of the port triggers side-effects in the
|
||||
* background script.
|
||||
* port1 will handle castManager messages.
|
||||
* port2 will handle cast instance messages.
|
||||
*/
|
||||
const { backgroundPort } = await import("./contentBridge");
|
||||
const { port1: managerPort, port2: instancePort } =
|
||||
new MessageChannel();
|
||||
|
||||
// backgroundPort -> channel.port2
|
||||
backgroundPort.onMessage.addListener((message: Message) => {
|
||||
channel.port2.postMessage(message);
|
||||
});
|
||||
/**
|
||||
* Provide castManager with a port to send messages to
|
||||
* cast instance.
|
||||
*/
|
||||
if (contextTabId) {
|
||||
await castManager.createInstance(instancePort, {
|
||||
tabId: contextTabId,
|
||||
frameId: 0
|
||||
});
|
||||
} else {
|
||||
await castManager.createInstance(instancePort);
|
||||
}
|
||||
|
||||
// channel.port2 -> backgroundPort
|
||||
channel.port2.onmessage = ev => {
|
||||
// castManager -> cast instance
|
||||
managerPort.addEventListener("message", ev => {
|
||||
const message = ev.data as Message;
|
||||
backgroundPort.postMessage(message);
|
||||
};
|
||||
|
||||
// Handle cast messages
|
||||
eventMessaging.page.addListener(message =>
|
||||
handleIncomingMessageToCast(message)
|
||||
);
|
||||
}
|
||||
|
||||
function handleIncomingMessageToCast(message: Message) {
|
||||
switch (message.subject) {
|
||||
case "cast:initialized": {
|
||||
if (message.subject === "cast:initialized") {
|
||||
if (message.data.isAvailable) {
|
||||
resolve(initializedBackgroundPort);
|
||||
resolve(existingPort);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageMessenging.extension.sendMessage(message);
|
||||
});
|
||||
managerPort.start();
|
||||
|
||||
// Cast instance -> castManager
|
||||
pageMessenging.extension.addListener(message => {
|
||||
managerPort.postMessage(message);
|
||||
});
|
||||
} else {
|
||||
// Let contentBridge hook up pageMessaging
|
||||
const { managerPort: backgroundPort } = await import(
|
||||
"./contentBridge"
|
||||
);
|
||||
existingPort = pageMessenging.page.messagePort;
|
||||
|
||||
backgroundPort.onMessage.addListener(function onManagerMessage(
|
||||
message: Message
|
||||
) {
|
||||
if (message.subject === "cast:initialized") {
|
||||
if (message.data.isAvailable) {
|
||||
resolve(pageMessenging.page.messagePort);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
|
||||
backgroundPort.onMessage.removeListener(onManagerMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default new CastSDK();
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import logger from "../lib/logger";
|
||||
|
||||
import { loadScript } from "../lib/utils";
|
||||
import { CAST_FRAMEWORK_SCRIPT_URL } from "./endpoints";
|
||||
import eventMessaging from "./eventMessaging";
|
||||
|
||||
import CastSDK from "./sdk";
|
||||
|
||||
const _window = window as any;
|
||||
|
||||
if (!_window.chrome) {
|
||||
_window.chrome = {};
|
||||
}
|
||||
|
||||
// Create page-accessible API object
|
||||
_window.chrome.cast = new CastSDK();
|
||||
|
||||
let frameworkScriptPromise: Promise<HTMLScriptElement> | undefined;
|
||||
|
||||
/**
|
||||
* If loaded within a page via a <script> element,
|
||||
* document.currentScript should exist and we can check its
|
||||
* [src] query string for the loadCastFramework param.
|
||||
*/
|
||||
if (document.currentScript) {
|
||||
const currentScript = document.currentScript as HTMLScriptElement;
|
||||
const currentScriptParams = new URLSearchParams(
|
||||
new URL(currentScript.src).search
|
||||
);
|
||||
|
||||
// Load Framework API if requested
|
||||
if (currentScriptParams.get("loadCastFramework") === "1") {
|
||||
// Queue up the framework script load to speed up init
|
||||
frameworkScriptPromise = loadScript(CAST_FRAMEWORK_SCRIPT_URL);
|
||||
frameworkScriptPromise.catch(() => {
|
||||
logger.error("Failed to load CAF script!");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
eventMessaging.page.addListener(async message => {
|
||||
switch (message.subject) {
|
||||
case "cast:initialized": {
|
||||
// If framework API is requested, ensure loaded
|
||||
await frameworkScriptPromise;
|
||||
|
||||
// Call page script/framework API script's init function
|
||||
const initFn = _window.__onGCastApiAvailable;
|
||||
if (initFn && typeof initFn === "function") {
|
||||
initFn(message.data.isAvailable);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
140
ext/src/cast/pageMessenging.ts
Normal file
140
ext/src/cast/pageMessenging.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { TypedMessagePort } from "../lib/TypedMessagePort";
|
||||
import type { Message } from "../messaging";
|
||||
|
||||
const INIT_MESSAGE = "__pageMessenger_init__";
|
||||
|
||||
/** Strip anything non-serializable for message channel. */
|
||||
function simplify(input: any) {
|
||||
return JSON.parse(JSON.stringify(input));
|
||||
}
|
||||
|
||||
type MessengerListener = (message: Message) => void;
|
||||
|
||||
/**
|
||||
* Abstract messenger class for cross-context messages via
|
||||
* MessageChannel.
|
||||
*
|
||||
* Facilitates a message channel between page scripts running in the
|
||||
* page script context and the extension scripts running in the
|
||||
* sandboxed content script context.
|
||||
*/
|
||||
abstract class Messenger {
|
||||
private listeners = new Set<MessengerListener>();
|
||||
|
||||
protected onMessage = (ev: MessageEvent<Message>) => {
|
||||
for (const listener of this.listeners) {
|
||||
listener(simplify(ev.data));
|
||||
}
|
||||
};
|
||||
|
||||
addListener(listener: MessengerListener) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
removeListener(listener: MessengerListener) {
|
||||
this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
/** Sends a message across the */
|
||||
abstract sendMessage(message: Message): void;
|
||||
|
||||
close() {
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page-side of page script messenging.
|
||||
*
|
||||
* Creates a message channel, then sends an INIT_MESSAGE window message
|
||||
* with a port that is handled by an ExtensionScriptMessenger in the
|
||||
* content script.
|
||||
*/
|
||||
export class PageScriptMessenger extends Messenger {
|
||||
private port: TypedMessagePort<Message>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Create message channel and send port2 to
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
window.postMessage(INIT_MESSAGE, window.location.href, [port2]);
|
||||
|
||||
this.port = port1;
|
||||
this.port.addEventListener("message", this.onMessage);
|
||||
this.port.start();
|
||||
}
|
||||
|
||||
sendMessage(message: Message) {
|
||||
this.port.postMessage(simplify(message));
|
||||
}
|
||||
get messagePort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
close() {
|
||||
super.close();
|
||||
this.port.removeEventListener("message", this.onMessage);
|
||||
this.port.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension-side of page script messenging.
|
||||
*
|
||||
* Listens for a INIT_MESSAGE window message from a PageScriptMessenger
|
||||
* running in a page script and establishes a message channel connection
|
||||
* once received.
|
||||
*/
|
||||
export class ExtensionScriptMessenger extends Messenger {
|
||||
private port?: TypedMessagePort<Message>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
window.addEventListener("message", this.onWindowMessage);
|
||||
}
|
||||
|
||||
/** Handles init message from window and stores transferred port. */
|
||||
private onWindowMessage = (ev: MessageEvent<any>) => {
|
||||
if (ev.source !== window || ev.data !== INIT_MESSAGE) return;
|
||||
|
||||
window.removeEventListener("message", this.onWindowMessage);
|
||||
|
||||
this.port = ev.ports[0];
|
||||
this.port.addEventListener("message", ev => this.onMessage(ev));
|
||||
this.port.start();
|
||||
};
|
||||
|
||||
sendMessage(message: Message) {
|
||||
this.port?.postMessage(simplify(message));
|
||||
}
|
||||
|
||||
close() {
|
||||
super.close();
|
||||
|
||||
window.removeEventListener("message", this.onWindowMessage);
|
||||
this.port?.removeEventListener("message", this.onMessage);
|
||||
this.port?.close();
|
||||
}
|
||||
}
|
||||
|
||||
let pageMessenger: Nullable<PageScriptMessenger> = null;
|
||||
let extensionMessenger: Nullable<ExtensionScriptMessenger> = null;
|
||||
|
||||
export default {
|
||||
/** Messenger for page scripts. */
|
||||
get page() {
|
||||
if (!pageMessenger) {
|
||||
pageMessenger = new PageScriptMessenger();
|
||||
}
|
||||
|
||||
return pageMessenger;
|
||||
},
|
||||
|
||||
/** Messenger for extension scripts. */
|
||||
get extension() {
|
||||
if (!extensionMessenger) {
|
||||
extensionMessenger = new ExtensionScriptMessenger();
|
||||
}
|
||||
return extensionMessenger;
|
||||
}
|
||||
};
|
||||
@@ -4,16 +4,7 @@ import { v4 as uuid } from "uuid";
|
||||
|
||||
import logger from "../../lib/logger";
|
||||
|
||||
import eventMessaging from "../eventMessaging";
|
||||
|
||||
import type {
|
||||
ErrorCallback,
|
||||
LoadSuccessCallback,
|
||||
MediaListener,
|
||||
MessageListener,
|
||||
SuccessCallback,
|
||||
UpdateListener
|
||||
} from "../types";
|
||||
import eventMessaging from "../pageMessenging";
|
||||
|
||||
import {
|
||||
MediaStatus,
|
||||
@@ -24,7 +15,12 @@ import {
|
||||
} from "./types";
|
||||
|
||||
import { SessionStatus } from "./enums";
|
||||
import type { Image, Receiver, SenderApplication } from "./classes";
|
||||
import type {
|
||||
Error as CastError,
|
||||
Image,
|
||||
Receiver,
|
||||
SenderApplication
|
||||
} from "./classes";
|
||||
|
||||
import { MediaCommand } from "./media/enums";
|
||||
import type { LoadRequest, QueueLoadRequest, QueueItem } from "./media/classes";
|
||||
@@ -101,17 +97,20 @@ function updateMedia(media: Media, status: MediaStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
type MessageListener = (namespace: string, message: string) => void;
|
||||
type UpdateListener = (isAlive: boolean) => void;
|
||||
|
||||
export default class Session {
|
||||
#loadMediaSuccessCallback?: (media: Media) => void;
|
||||
#loadMediaErrorCallback?: ErrorCallback;
|
||||
#loadMediaRequest?: LoadRequest;
|
||||
#loadMediaSuccessCallback?: (media: Media) => void;
|
||||
#loadMediaErrorCallback?: (err: CastError) => void;
|
||||
|
||||
_messageListeners = new Map<string, Set<MessageListener>>();
|
||||
_updateListeners = new Set<UpdateListener>();
|
||||
|
||||
_sendMessageCallbacks = new Map<
|
||||
string,
|
||||
[SuccessCallback?, ErrorCallback?]
|
||||
[(() => void)?, ((err: CastError) => void)?]
|
||||
>();
|
||||
|
||||
media: Media[] = [];
|
||||
@@ -203,10 +202,10 @@ export default class Session {
|
||||
});
|
||||
};
|
||||
|
||||
addMediaListener(_mediaListener: MediaListener) {
|
||||
addMediaListener(_mediaListener: (media: Media) => void) {
|
||||
logger.info("STUB :: Session#addMediaListener");
|
||||
}
|
||||
removeMediaListener(_mediaListener: MediaListener) {
|
||||
removeMediaListener(_mediaListener: (media: Media) => void) {
|
||||
logger.info("STUB :: Session#removeMediaListener");
|
||||
}
|
||||
|
||||
@@ -228,14 +227,17 @@ export default class Session {
|
||||
this._updateListeners.delete(listener);
|
||||
}
|
||||
|
||||
leave(_successCallback?: SuccessCallback, _errorCallback?: ErrorCallback) {
|
||||
leave(
|
||||
_successCallback?: () => void,
|
||||
_errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
logger.info("STUB :: Session#leave");
|
||||
}
|
||||
|
||||
loadMedia(
|
||||
loadRequest: LoadRequest,
|
||||
successCallback?: LoadSuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: (media: Media) => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#loadMediaSuccessCallback = successCallback;
|
||||
this.#loadMediaErrorCallback = errorCallback;
|
||||
@@ -246,8 +248,8 @@ export default class Session {
|
||||
|
||||
queueLoad(
|
||||
_queueLoadRequest: QueueLoadRequest,
|
||||
_successCallback?: LoadSuccessCallback,
|
||||
_errorCallback?: ErrorCallback
|
||||
_successCallback?: (media: Media) => void,
|
||||
_errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
logger.info("STUB :: Session#queueLoad");
|
||||
}
|
||||
@@ -255,8 +257,8 @@ export default class Session {
|
||||
sendMessage(
|
||||
namespace: string,
|
||||
message: object | string,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const messageId = uuid();
|
||||
|
||||
@@ -278,8 +280,8 @@ export default class Session {
|
||||
|
||||
setReceiverMuted(
|
||||
muted: boolean,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#sendReceiverMessage({ type: "SET_VOLUME", volume: { muted } })
|
||||
.then(successCallback)
|
||||
@@ -288,8 +290,8 @@ export default class Session {
|
||||
|
||||
setReceiverVolumeLevel(
|
||||
newLevel: number,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#sendReceiverMessage({
|
||||
type: "SET_VOLUME",
|
||||
@@ -299,7 +301,10 @@ export default class Session {
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
stop(successCallback?: SuccessCallback, errorCallback?: ErrorCallback) {
|
||||
stop(
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#sendReceiverMessage({ type: "STOP", sessionId: this.sessionId })
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AutoJoinPolicy,
|
||||
Capability,
|
||||
DefaultActionPolicy,
|
||||
ReceiverAvailability,
|
||||
ReceiverType,
|
||||
VolumeControlType
|
||||
} from "./enums";
|
||||
@@ -14,7 +15,7 @@ export class ApiConfig {
|
||||
constructor(
|
||||
public sessionRequest: SessionRequest,
|
||||
public sessionListener: (session: Session) => void,
|
||||
public receiverListener: (availability: string) => void,
|
||||
public receiverListener: (availability: ReceiverAvailability) => void,
|
||||
|
||||
public autoJoinPolicy: string = AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED,
|
||||
public defaultActionPolicy: string = DefaultActionPolicy.CREATE_SESSION
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import logger from "../../lib/logger";
|
||||
|
||||
import type { Message } from "../../messaging";
|
||||
import eventMessaging from "../eventMessaging";
|
||||
|
||||
import type { ErrorCallback, SuccessCallback } from "../types";
|
||||
import pageMessenging from "../pageMessenging";
|
||||
|
||||
import {
|
||||
AutoJoinPolicy,
|
||||
@@ -25,7 +23,7 @@ import {
|
||||
ApiConfig,
|
||||
CredentialsData,
|
||||
DialRequest,
|
||||
Error as Error_,
|
||||
Error as CastError,
|
||||
Image,
|
||||
Receiver,
|
||||
ReceiverDisplayStatus,
|
||||
@@ -51,15 +49,16 @@ export default class {
|
||||
#apiConfig?: ApiConfig;
|
||||
#sessionRequest?: SessionRequest;
|
||||
|
||||
#receiverAvailability = ReceiverAvailability.UNAVAILABLE;
|
||||
|
||||
#initializeSuccessCallback?: () => void;
|
||||
|
||||
#requestSessionSuccessCallback?: RequestSessionSuccessCallback;
|
||||
#requestSessionErrorCallback?: ErrorCallback;
|
||||
#requestSessionErrorCallback?: (err: CastError) => void;
|
||||
|
||||
#initializeSuccessCallback?: SuccessCallback;
|
||||
|
||||
#sessions = new Map<string, Session>();
|
||||
#receiverActionListeners = new Set<ReceiverActionListener>();
|
||||
|
||||
#receiverAvailability = ReceiverAvailability.UNAVAILABLE;
|
||||
#sessions = new Map<string, Session>();
|
||||
|
||||
// Enums
|
||||
AutoJoinPolicy = AutoJoinPolicy;
|
||||
@@ -78,7 +77,7 @@ export default class {
|
||||
ApiConfig = ApiConfig;
|
||||
CredentialsData = CredentialsData;
|
||||
DialRequest = DialRequest;
|
||||
Error = Error_;
|
||||
Error = CastError;
|
||||
Image = Image;
|
||||
Receiver = Receiver;
|
||||
ReceiverDisplayStatus = ReceiverDisplayStatus;
|
||||
@@ -95,17 +94,17 @@ export default class {
|
||||
timeout = new Timeout();
|
||||
|
||||
constructor() {
|
||||
eventMessaging.page.addListener(this.#onMessage.bind(this));
|
||||
pageMessenging.page.addListener(this.#onMessage.bind(this));
|
||||
}
|
||||
|
||||
#onMessage(message: Message) {
|
||||
switch (message.subject) {
|
||||
case "cast:initialized":
|
||||
this.isAvailable = true;
|
||||
|
||||
this.#initializeSuccessCallback?.();
|
||||
this.#apiConfig?.receiverListener(this.#receiverAvailability);
|
||||
|
||||
this.isAvailable = true;
|
||||
|
||||
break;
|
||||
|
||||
/**
|
||||
@@ -185,7 +184,7 @@ export default class {
|
||||
break;
|
||||
}
|
||||
|
||||
case "cast:receivedSessionMessage": {
|
||||
case "cast:sessionMessageReceived": {
|
||||
const { sessionId, namespace, messageData } = message.data;
|
||||
const session = this.#sessions.get(sessionId);
|
||||
if (session) {
|
||||
@@ -213,7 +212,7 @@ export default class {
|
||||
const [successCallback, errorCallback] = callbacks;
|
||||
|
||||
if (error) {
|
||||
errorCallback?.(new Error_(error));
|
||||
errorCallback?.(new CastError(error));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -223,7 +222,7 @@ export default class {
|
||||
break;
|
||||
}
|
||||
|
||||
case "cast:updateReceiverAvailability": {
|
||||
case "cast:receiverAvailabilityUpdated": {
|
||||
const availability = message.data.isAvailable
|
||||
? ReceiverAvailability.AVAILABLE
|
||||
: ReceiverAvailability.UNAVAILABLE;
|
||||
@@ -238,19 +237,19 @@ export default class {
|
||||
}
|
||||
|
||||
// Popup closed before session established
|
||||
case "cast:selectReceiver/cancelled": {
|
||||
case "cast:sessionRequestCancelled": {
|
||||
if (this.#sessionRequest) {
|
||||
this.#sessionRequest = undefined;
|
||||
|
||||
this.#requestSessionErrorCallback?.(
|
||||
new Error_(ErrorCode.CANCEL)
|
||||
new CastError(ErrorCode.CANCEL)
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "cast:sendReceiverAction": {
|
||||
case "cast:receiverAction": {
|
||||
for (const actionListener of this.#receiverActionListeners) {
|
||||
actionListener(message.data.receiver, message.data.action);
|
||||
}
|
||||
@@ -262,14 +261,14 @@ export default class {
|
||||
|
||||
initialize(
|
||||
apiConfig: ApiConfig,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
logger.info("cast.initialize");
|
||||
|
||||
// Already initialized
|
||||
if (this.#apiConfig) {
|
||||
errorCallback?.(new Error_(ErrorCode.INVALID_PARAMETER));
|
||||
errorCallback?.(new CastError(ErrorCode.INVALID_PARAMETER));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -279,7 +278,7 @@ export default class {
|
||||
this.#initializeSuccessCallback = successCallback;
|
||||
}
|
||||
|
||||
eventMessaging.page.sendMessage({
|
||||
pageMessenging.page.sendMessage({
|
||||
subject: "main:initializeCast",
|
||||
data: { apiConfig: this.#apiConfig }
|
||||
});
|
||||
@@ -287,21 +286,21 @@ export default class {
|
||||
|
||||
requestSession(
|
||||
successCallback: RequestSessionSuccessCallback,
|
||||
errorCallback: ErrorCallback,
|
||||
errorCallback: (err: CastError) => void,
|
||||
newSessionRequest?: SessionRequest
|
||||
) {
|
||||
logger.info("cast.requestSession");
|
||||
|
||||
// Not yet initialized
|
||||
if (!this.#apiConfig) {
|
||||
errorCallback?.(new Error_(ErrorCode.API_NOT_INITIALIZED));
|
||||
errorCallback?.(new CastError(ErrorCode.API_NOT_INITIALIZED));
|
||||
return;
|
||||
}
|
||||
|
||||
// Already requesting session
|
||||
if (this.#sessionRequest) {
|
||||
errorCallback?.(
|
||||
new Error_(
|
||||
new CastError(
|
||||
ErrorCode.INVALID_PARAMETER,
|
||||
"Session request already in progress."
|
||||
)
|
||||
@@ -310,7 +309,7 @@ export default class {
|
||||
}
|
||||
|
||||
if (this.#receiverAvailability === ReceiverAvailability.UNAVAILABLE) {
|
||||
errorCallback?.(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
|
||||
errorCallback?.(new CastError(ErrorCode.RECEIVER_UNAVAILABLE));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -322,8 +321,8 @@ export default class {
|
||||
this.#requestSessionErrorCallback = errorCallback;
|
||||
|
||||
// Open receiver selector UI
|
||||
eventMessaging.page.sendMessage({
|
||||
subject: "main:selectReceiver",
|
||||
pageMessenging.page.sendMessage({
|
||||
subject: "main:requestSession",
|
||||
data: { sessionRequest: this.#sessionRequest }
|
||||
});
|
||||
}
|
||||
@@ -334,8 +333,8 @@ export default class {
|
||||
|
||||
setCustomReceivers(
|
||||
_receivers: Receiver[],
|
||||
_successCallback?: SuccessCallback,
|
||||
_errorCallback?: ErrorCallback
|
||||
_successCallback?: () => void,
|
||||
_errorCallback?: (err: CastError) => void
|
||||
): void {
|
||||
logger.info("STUB :: cast.setCustomReceivers");
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use strict";
|
||||
|
||||
import { v1 as uuid } from "uuid";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import logger from "../../../lib/logger";
|
||||
|
||||
import { Volume, Error as _Error } from "../classes";
|
||||
import { Volume, Error as CastError } from "../classes";
|
||||
import {
|
||||
BreakStatus,
|
||||
EditTracksInfoRequest,
|
||||
@@ -30,16 +30,13 @@ import {
|
||||
import { PlayerState, RepeatMode } from "./enums";
|
||||
import { ErrorCode } from "../enums";
|
||||
|
||||
import type {
|
||||
ErrorCallback,
|
||||
SuccessCallback,
|
||||
UpdateListener
|
||||
} from "../../types";
|
||||
import type { SenderMediaMessage } from "../types";
|
||||
import { getEstimatedTime } from "../../utils";
|
||||
|
||||
export const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
||||
|
||||
type UpdateListener = (isAlive: boolean) => void;
|
||||
|
||||
export default class Media {
|
||||
#id = uuid();
|
||||
|
||||
@@ -85,8 +82,8 @@ export default class Media {
|
||||
|
||||
editTracksInfo(
|
||||
editTracksInfoRequest: EditTracksInfoRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...editTracksInfoRequest,
|
||||
@@ -161,8 +158,8 @@ export default class Media {
|
||||
*/
|
||||
getStatus(
|
||||
getStatusRequest = new GetStatusRequest(),
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...getStatusRequest,
|
||||
@@ -175,8 +172,8 @@ export default class Media {
|
||||
|
||||
pause(
|
||||
pauseRequest = new PauseRequest(),
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...pauseRequest,
|
||||
@@ -189,8 +186,8 @@ export default class Media {
|
||||
|
||||
play(
|
||||
playRequest = new PlayRequest(),
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...playRequest,
|
||||
@@ -203,8 +200,8 @@ export default class Media {
|
||||
|
||||
queueAppendItem(
|
||||
item: QueueItem,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...new QueueInsertItemsRequest([item]),
|
||||
@@ -218,8 +215,8 @@ export default class Media {
|
||||
|
||||
queueInsertItems(
|
||||
queueInsertItemsRequest: QueueInsertItemsRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...queueInsertItemsRequest,
|
||||
@@ -233,8 +230,8 @@ export default class Media {
|
||||
|
||||
queueJumpToItem(
|
||||
itemId: number,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
if (this.items?.find(item => item.itemId === itemId)) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
@@ -254,8 +251,8 @@ export default class Media {
|
||||
queueMoveItemToNewIndex(
|
||||
itemId: number,
|
||||
newIndex: number,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
// Return early if not in queue
|
||||
if (!this.items) {
|
||||
@@ -268,7 +265,7 @@ export default class Media {
|
||||
// New index must not be negative
|
||||
if (newIndex < 0) {
|
||||
if (errorCallback) {
|
||||
errorCallback(new _Error(ErrorCode.INVALID_PARAMETER));
|
||||
errorCallback(new CastError(ErrorCode.INVALID_PARAMETER));
|
||||
}
|
||||
} else if (newIndex == itemIndex) {
|
||||
if (successCallback) {
|
||||
@@ -298,8 +295,8 @@ export default class Media {
|
||||
}
|
||||
|
||||
queueNext(
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
jumpRequest.jump = 1;
|
||||
@@ -315,8 +312,8 @@ export default class Media {
|
||||
}
|
||||
|
||||
queuePrev(
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
jumpRequest.jump = -1;
|
||||
@@ -333,8 +330,8 @@ export default class Media {
|
||||
|
||||
queueRemoveItem(
|
||||
itemId: number,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const item = this.items?.find(item => item.itemId === itemId);
|
||||
if (item) {
|
||||
@@ -348,8 +345,8 @@ export default class Media {
|
||||
|
||||
queueRemoveItems(
|
||||
queueRemoveItemsRequest: QueueRemoveItemsRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...queueRemoveItemsRequest,
|
||||
@@ -364,8 +361,8 @@ export default class Media {
|
||||
|
||||
queueReorderItems(
|
||||
queueReorderItemsRequest: QueueReorderItemsRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...queueReorderItemsRequest,
|
||||
@@ -380,8 +377,8 @@ export default class Media {
|
||||
|
||||
queueSetRepeatMode(
|
||||
repeatMode: string,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const setPropertiesRequest = new QueueSetPropertiesRequest();
|
||||
setPropertiesRequest.repeatMode = repeatMode;
|
||||
@@ -398,8 +395,8 @@ export default class Media {
|
||||
|
||||
queueUpdateItems(
|
||||
queueUpdateItemsRequest: QueueUpdateItemsRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...queueUpdateItemsRequest,
|
||||
@@ -413,8 +410,8 @@ export default class Media {
|
||||
|
||||
seek(
|
||||
seekRequest: SeekRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...seekRequest,
|
||||
@@ -427,8 +424,8 @@ export default class Media {
|
||||
|
||||
setVolume(
|
||||
volumeRequest: VolumeRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...volumeRequest,
|
||||
@@ -441,8 +438,8 @@ export default class Media {
|
||||
|
||||
stop(
|
||||
stopRequest?: StopRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
if (!stopRequest) {
|
||||
stopRequest = new StopRequest();
|
||||
|
||||
279
ext/src/cast/senders/media.ts
Normal file
279
ext/src/cast/senders/media.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { Logger } from "../../lib/logger";
|
||||
import options from "../../lib/options";
|
||||
|
||||
import type { Message } from "../../messaging";
|
||||
|
||||
// Cast types
|
||||
import { Capability, ReceiverAvailability } from "../sdk/enums";
|
||||
import type Session from "../sdk/Session";
|
||||
|
||||
import cast, { ensureInit, CastPort } from "../export";
|
||||
|
||||
const logger = new Logger("fx_cast [media sender]");
|
||||
|
||||
interface MediaSenderOpts {
|
||||
mediaUrl: string;
|
||||
contextTabId?: number;
|
||||
targetElementId?: number;
|
||||
}
|
||||
|
||||
export default class MediaSender {
|
||||
private port?: CastPort;
|
||||
|
||||
private mediaUrl: string;
|
||||
private contextTabId?: number;
|
||||
|
||||
private mediaElement?: HTMLMediaElement;
|
||||
|
||||
private isLocalMedia = false;
|
||||
private isLocalMediaEnabled = false;
|
||||
|
||||
// Cast API objects
|
||||
private session?: Session;
|
||||
|
||||
constructor(opts: MediaSenderOpts) {
|
||||
this.mediaUrl = opts.mediaUrl;
|
||||
this.contextTabId = opts.contextTabId;
|
||||
|
||||
if (opts.targetElementId) {
|
||||
this.mediaElement = browser.menus.getTargetElement(
|
||||
opts.targetElementId
|
||||
) as HTMLMediaElement;
|
||||
}
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.port?.postMessage({ subject: "bridge:stopMediaServer" });
|
||||
this.session?.stop();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
try {
|
||||
this.port = await ensureInit(this.contextTabId);
|
||||
} catch (err) {
|
||||
logger.error("Failed to initialize cast API", err);
|
||||
}
|
||||
|
||||
this.isLocalMedia = this.mediaUrl.startsWith("file://");
|
||||
this.isLocalMediaEnabled = await options.get("localMediaEnabled");
|
||||
|
||||
if (this.isLocalMedia && !this.isLocalMediaEnabled) {
|
||||
throw logger.error("Local media casting not enabled");
|
||||
}
|
||||
|
||||
const capabilities = [Capability.AUDIO_OUT];
|
||||
if (this.mediaElement instanceof HTMLVideoElement) {
|
||||
capabilities.push(Capability.VIDEO_OUT);
|
||||
}
|
||||
|
||||
cast.initialize(
|
||||
new cast.ApiConfig(
|
||||
new cast.SessionRequest(
|
||||
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
capabilities
|
||||
),
|
||||
this.sessionListener.bind(this),
|
||||
this.receiverListener.bind(this)
|
||||
),
|
||||
undefined,
|
||||
err => {
|
||||
logger.error("Failed to initialize cast API", err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private sessionListener() {
|
||||
// Unused
|
||||
}
|
||||
private receiverListener(availability: ReceiverAvailability) {
|
||||
// Already have session
|
||||
if (this.session) return;
|
||||
|
||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||
cast.requestSession(
|
||||
(session: Session) => {
|
||||
this.session = session;
|
||||
this.loadMedia();
|
||||
},
|
||||
err => {
|
||||
logger.error("Session request failed", err);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMedia() {
|
||||
let mediaUrl = new URL(this.mediaUrl);
|
||||
const mediaTitle = mediaUrl.pathname.slice(1);
|
||||
const subtitleUrls: URL[] = [];
|
||||
|
||||
if (this.isLocalMedia) {
|
||||
const port = await options.get("localMediaServerPort");
|
||||
try {
|
||||
const { localAddress, mediaPath, subtitlePaths } =
|
||||
await this.startMediaServer(mediaTitle, port);
|
||||
|
||||
const baseUrl = new URL(`http://${localAddress}:${port}/`);
|
||||
mediaUrl = new URL(mediaPath, baseUrl);
|
||||
subtitleUrls.push(
|
||||
...subtitlePaths.map(path => new URL(path, baseUrl))
|
||||
);
|
||||
} catch (err) {
|
||||
throw logger.error("Failed to start media server", err);
|
||||
}
|
||||
}
|
||||
|
||||
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, "");
|
||||
mediaInfo.metadata = new cast.media.GenericMediaMetadata();
|
||||
mediaInfo.metadata.title = mediaTitle;
|
||||
|
||||
const activeTrackIds: number[] = [];
|
||||
|
||||
mediaInfo.tracks = subtitleUrls.map((url, index) => {
|
||||
const track = new cast.media.Track(
|
||||
index,
|
||||
cast.media.TrackType.TEXT
|
||||
);
|
||||
track.name = url.pathname;
|
||||
track.trackContentId = url.href;
|
||||
track.trackContentType = "text/vtt";
|
||||
track.subtype = cast.media.TextTrackType.SUBTITLES;
|
||||
|
||||
return track;
|
||||
});
|
||||
|
||||
if (this.mediaElement instanceof HTMLMediaElement) {
|
||||
if (this.mediaElement instanceof HTMLVideoElement) {
|
||||
if (this.mediaElement.poster) {
|
||||
mediaInfo.metadata.images = [
|
||||
new cast.Image(this.mediaElement.poster)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mediaElement.textTracks.length) {
|
||||
const textTracks = Array.from(this.mediaElement.textTracks);
|
||||
const trackElements =
|
||||
this.mediaElement.querySelectorAll("track");
|
||||
|
||||
let mediaTrackIndex = mediaInfo.tracks.length;
|
||||
textTracks.forEach((track, index) => {
|
||||
const trackElement = trackElements[index];
|
||||
|
||||
/**
|
||||
* Create media.Track object with the index as the track ID
|
||||
* and type as TrackType.TEXT.
|
||||
*/
|
||||
const castTrack = new cast.media.Track(
|
||||
mediaTrackIndex,
|
||||
cast.media.TrackType.TEXT
|
||||
);
|
||||
|
||||
// Copy TextTrack properties
|
||||
castTrack.name = track.label || `track-${mediaTrackIndex}`;
|
||||
castTrack.language = track.language;
|
||||
castTrack.trackContentId = trackElement.src;
|
||||
castTrack.trackContentType = "text/vtt";
|
||||
|
||||
switch (track.kind) {
|
||||
case "subtitles":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.SUBTITLES;
|
||||
break;
|
||||
case "captions":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.CAPTIONS;
|
||||
break;
|
||||
case "descriptions":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.DESCRIPTIONS;
|
||||
break;
|
||||
case "chapters":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.CHAPTERS;
|
||||
break;
|
||||
case "metadata":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.METADATA;
|
||||
break;
|
||||
|
||||
// Default to subtitles
|
||||
default:
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.SUBTITLES;
|
||||
}
|
||||
|
||||
// Add track to mediaInfo
|
||||
mediaInfo.tracks?.push(castTrack);
|
||||
|
||||
// If enabled, mark as active track for load request
|
||||
if (track.mode === "showing" || trackElement.default) {
|
||||
activeTrackIds.push(mediaTrackIndex);
|
||||
}
|
||||
|
||||
mediaTrackIndex++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const loadRequest = new cast.media.LoadRequest(mediaInfo);
|
||||
loadRequest.autoplay = true;
|
||||
loadRequest.activeTrackIds = activeTrackIds;
|
||||
|
||||
this.session?.loadMedia(loadRequest);
|
||||
}
|
||||
|
||||
private startMediaServer(
|
||||
filePath: string,
|
||||
port: number
|
||||
): Promise<{
|
||||
mediaPath: string;
|
||||
subtitlePaths: string[];
|
||||
localAddress: string;
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.port) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
this.port.postMessage({
|
||||
subject: "bridge:startMediaServer",
|
||||
data: {
|
||||
filePath: decodeURI(filePath),
|
||||
port: port
|
||||
}
|
||||
});
|
||||
|
||||
const onMessage = (ev: MessageEvent<Message>) => {
|
||||
const message = ev.data;
|
||||
|
||||
if (message.subject.startsWith("mediaCast:mediaServer")) {
|
||||
this.port?.removeEventListener("message", onMessage);
|
||||
}
|
||||
|
||||
switch (message.subject) {
|
||||
case "mediaCast:mediaServerStarted":
|
||||
resolve(message.data);
|
||||
break;
|
||||
case "mediaCast:mediaServerError":
|
||||
reject(message.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.port.addEventListener("message", onMessage);
|
||||
this.port.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (window.location.protocol !== "moz-extension:") {
|
||||
const window_ = window as any;
|
||||
new MediaSender({
|
||||
mediaUrl: window_.mediaUrl,
|
||||
targetElementId: window_.targetElementId
|
||||
});
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import logger from "../../../lib/logger";
|
||||
import options from "../../../lib/options";
|
||||
import cast, { ensureInit } from "../../export";
|
||||
|
||||
import type { Message } from "../../../messaging";
|
||||
import type { ReceiverDevice } from "../../../types";
|
||||
|
||||
import type Session from "../../sdk/Session";
|
||||
import type Media from "../../sdk/media/Media";
|
||||
import type { Error as Error_ } from "../../sdk/classes";
|
||||
|
||||
function startMediaServer(
|
||||
filePath: string,
|
||||
port: number
|
||||
): Promise<{
|
||||
mediaPath: string;
|
||||
subtitlePaths: string[];
|
||||
localAddress: string;
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
backgroundPort.postMessage({
|
||||
subject: "bridge:startMediaServer",
|
||||
data: {
|
||||
filePath: decodeURI(filePath),
|
||||
port
|
||||
}
|
||||
} as Message);
|
||||
|
||||
backgroundPort.addEventListener("message", function onMessage(ev) {
|
||||
const message = ev.data as Message;
|
||||
|
||||
if (message.subject.startsWith("mediaCast:mediaServer")) {
|
||||
backgroundPort.removeEventListener("message", onMessage);
|
||||
}
|
||||
|
||||
switch (message.subject) {
|
||||
case "mediaCast:mediaServerStarted": {
|
||||
resolve(message.data);
|
||||
break;
|
||||
}
|
||||
case "mediaCast:mediaServerError": {
|
||||
reject(message.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
backgroundPort.start();
|
||||
});
|
||||
}
|
||||
|
||||
let backgroundPort: MessagePort;
|
||||
|
||||
let currentSession: Session;
|
||||
let currentMedia: Media;
|
||||
|
||||
let targetElement: HTMLElement;
|
||||
|
||||
function getSession(opts: InitOptions): Promise<Session> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
/**
|
||||
* If a receiver is available, call requestSession. If a
|
||||
* specific receiver was specified, bypass receiver selector
|
||||
* and create session directly.
|
||||
*/
|
||||
function receiverListener(availability: string) {
|
||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||
cast.requestSession(
|
||||
onRequestSessionSuccess,
|
||||
onRequestSessionError,
|
||||
undefined,
|
||||
opts.receiver
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function sessionListener() {
|
||||
// TODO: Handle this
|
||||
}
|
||||
|
||||
function onRequestSessionSuccess(session: Session) {
|
||||
resolve(session);
|
||||
}
|
||||
function onRequestSessionError(err: Error_) {
|
||||
reject(err.description);
|
||||
}
|
||||
|
||||
const sessionRequest = new cast.SessionRequest(
|
||||
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID
|
||||
);
|
||||
|
||||
const apiConfig = new cast.ApiConfig(
|
||||
sessionRequest,
|
||||
sessionListener, // sessionListener
|
||||
receiverListener
|
||||
); // receiverListener
|
||||
|
||||
cast.initialize(apiConfig);
|
||||
});
|
||||
}
|
||||
|
||||
function getMedia(opts: InitOptions): Promise<Media> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let mediaUrl = new URL(opts.mediaUrl);
|
||||
const mediaTitle = mediaUrl.pathname.slice(1);
|
||||
const subtitleUrls: URL[] = [];
|
||||
|
||||
/**
|
||||
* If the media is a local file, start an HTTP media server
|
||||
* and change the media URL to point to it.
|
||||
*/
|
||||
if (opts.mediaUrl.startsWith("file://")) {
|
||||
const port = await options.get("localMediaServerPort");
|
||||
|
||||
try {
|
||||
// Wait until media server is listening
|
||||
const { localAddress, mediaPath, subtitlePaths } =
|
||||
await startMediaServer(mediaTitle, port);
|
||||
|
||||
const baseUrl = new URL(`http://${localAddress}:${port}/`);
|
||||
mediaUrl = new URL(mediaPath, baseUrl);
|
||||
subtitleUrls.push(
|
||||
...subtitlePaths.map(path => new URL(path, baseUrl))
|
||||
);
|
||||
|
||||
console.info(mediaUrl);
|
||||
} catch (err) {
|
||||
throw logger.error("Failed to start media server", err);
|
||||
}
|
||||
}
|
||||
|
||||
const activeTrackIds: number[] = [];
|
||||
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, "");
|
||||
|
||||
mediaInfo.metadata = new cast.media.GenericMediaMetadata();
|
||||
mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC;
|
||||
mediaInfo.metadata.title = mediaTitle;
|
||||
mediaInfo.tracks = [];
|
||||
|
||||
let trackIndex = 0;
|
||||
for (const subtitleUrl of subtitleUrls) {
|
||||
const castTrack = new cast.media.Track(
|
||||
trackIndex,
|
||||
cast.media.TrackType.TEXT
|
||||
);
|
||||
|
||||
castTrack.name = subtitleUrl.pathname;
|
||||
castTrack.trackContentId = subtitleUrl.href;
|
||||
castTrack.trackContentType = "text/vtt";
|
||||
castTrack.subtype = cast.media.TextTrackType.SUBTITLES;
|
||||
|
||||
mediaInfo.tracks.push(castTrack);
|
||||
}
|
||||
|
||||
if (targetElement instanceof HTMLMediaElement) {
|
||||
if (targetElement instanceof HTMLVideoElement) {
|
||||
if (targetElement.poster) {
|
||||
mediaInfo.metadata.images = [
|
||||
new cast.Image(targetElement.poster)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (targetElement.textTracks.length) {
|
||||
const tracks = Array.from(targetElement.textTracks);
|
||||
const trackElements = targetElement.querySelectorAll("track");
|
||||
|
||||
tracks.forEach((track, index) => {
|
||||
const trackElement = trackElements[index];
|
||||
|
||||
/**
|
||||
* Create media.Track object with the index as the track ID
|
||||
* and type as TrackType.TEXT.
|
||||
*/
|
||||
const castTrack = new cast.media.Track(
|
||||
trackIndex,
|
||||
cast.media.TrackType.TEXT
|
||||
);
|
||||
|
||||
// Copy TextTrack properties
|
||||
castTrack.name = track.label || `track-${trackIndex}`;
|
||||
castTrack.language = track.language;
|
||||
castTrack.trackContentId = trackElement.src;
|
||||
castTrack.trackContentType = "text/vtt";
|
||||
|
||||
switch (track.kind) {
|
||||
case "subtitles":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.SUBTITLES;
|
||||
break;
|
||||
case "captions":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.CAPTIONS;
|
||||
break;
|
||||
case "descriptions":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.DESCRIPTIONS;
|
||||
break;
|
||||
case "chapters":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.CHAPTERS;
|
||||
break;
|
||||
case "metadata":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.METADATA;
|
||||
break;
|
||||
|
||||
// Default to subtitles
|
||||
default:
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.SUBTITLES;
|
||||
}
|
||||
|
||||
// Add track to mediaInfo
|
||||
mediaInfo.tracks?.push(castTrack);
|
||||
|
||||
// If enabled, mark as active track for load request
|
||||
if (track.mode === "showing" || trackElement.default) {
|
||||
activeTrackIds.push(trackIndex);
|
||||
}
|
||||
|
||||
trackIndex++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const loadRequest = new cast.media.LoadRequest(mediaInfo);
|
||||
loadRequest.autoplay = true;
|
||||
loadRequest.activeTrackIds = activeTrackIds;
|
||||
|
||||
currentSession.loadMedia(loadRequest, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
let ignoreMediaEvents = false;
|
||||
|
||||
async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
||||
function checkIgnore(ev: Event) {
|
||||
if (ignoreMediaEvents) {
|
||||
ignoreMediaEvents = false;
|
||||
ev.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
if (await options.get("mediaSyncElement")) {
|
||||
mediaElement.addEventListener("play", checkIgnore, true);
|
||||
mediaElement.addEventListener("pause", checkIgnore, true);
|
||||
mediaElement.addEventListener("suspend", checkIgnore, true);
|
||||
mediaElement.addEventListener("seeking", checkIgnore, true);
|
||||
mediaElement.addEventListener("ratechange", checkIgnore, true);
|
||||
mediaElement.addEventListener("volumechange", checkIgnore, true);
|
||||
|
||||
mediaElement.addEventListener("play", () => {
|
||||
currentMedia.play();
|
||||
});
|
||||
|
||||
mediaElement.addEventListener("pause", () => {
|
||||
currentMedia.pause();
|
||||
});
|
||||
|
||||
mediaElement.addEventListener("suspend", () => {
|
||||
// currentMedia.stop(null, null, null);
|
||||
});
|
||||
|
||||
mediaElement.addEventListener("seeked", () => {
|
||||
const seekRequest = new cast.media.SeekRequest();
|
||||
seekRequest.currentTime = mediaElement.currentTime;
|
||||
|
||||
currentMedia.seek(seekRequest);
|
||||
});
|
||||
|
||||
mediaElement.addEventListener("ratechange", () => {
|
||||
// TODO: Re-implement this
|
||||
});
|
||||
|
||||
mediaElement.addEventListener("volumechange", () => {
|
||||
const newVolume = new cast.Volume(
|
||||
currentMedia.volume.level,
|
||||
currentMedia.volume.muted
|
||||
);
|
||||
|
||||
const volumeRequest = new cast.media.VolumeRequest(newVolume);
|
||||
|
||||
currentMedia.setVolume(volumeRequest);
|
||||
});
|
||||
|
||||
currentMedia.addUpdateListener(isAlive => {
|
||||
if (!isAlive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localPlayerState = mediaElement.paused
|
||||
? cast.media.PlayerState.PAUSED
|
||||
: cast.media.PlayerState.PLAYING;
|
||||
|
||||
if (localPlayerState !== currentMedia.playerState) {
|
||||
ignoreMediaEvents = true;
|
||||
|
||||
switch (currentMedia.playerState) {
|
||||
case cast.media.PlayerState.PLAYING: {
|
||||
mediaElement.play();
|
||||
break;
|
||||
}
|
||||
case cast.media.PlayerState.PAUSED: {
|
||||
mediaElement.pause();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const localRepeatMode = mediaElement.loop
|
||||
? cast.media.RepeatMode.SINGLE
|
||||
: cast.media.RepeatMode.OFF;
|
||||
|
||||
if (localRepeatMode !== currentMedia.repeatMode) {
|
||||
ignoreMediaEvents = true;
|
||||
|
||||
switch (currentMedia.repeatMode) {
|
||||
case cast.media.RepeatMode.SINGLE: {
|
||||
mediaElement.loop = true;
|
||||
break;
|
||||
}
|
||||
case cast.media.RepeatMode.OFF: {
|
||||
mediaElement.loop = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentMedia.currentTime !== mediaElement.currentTime) {
|
||||
ignoreMediaEvents = true;
|
||||
mediaElement.currentTime = currentMedia.currentTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface InitOptions {
|
||||
mediaUrl: string;
|
||||
receiver?: ReceiverDevice;
|
||||
targetElementId?: number;
|
||||
}
|
||||
|
||||
export async function init(opts: InitOptions) {
|
||||
backgroundPort = await ensureInit();
|
||||
|
||||
backgroundPort.addEventListener("message", ev => {
|
||||
const message = ev.data as Message;
|
||||
switch (message.subject) {
|
||||
case "mediaCast:mediaServerError":
|
||||
logger.error("Media server error", message.data);
|
||||
}
|
||||
});
|
||||
|
||||
const isLocalMedia = opts.mediaUrl.startsWith("file://");
|
||||
const isLocalMediaEnabled = await options.get("localMediaEnabled");
|
||||
|
||||
if (isLocalMedia && !isLocalMediaEnabled) {
|
||||
cast.logMessage("Local media casting not enabled");
|
||||
return;
|
||||
}
|
||||
if (!opts.targetElementId) {
|
||||
cast.logMessage("Target element ID not found");
|
||||
return;
|
||||
}
|
||||
|
||||
targetElement = browser.menus.getTargetElement(
|
||||
opts.targetElementId
|
||||
) as HTMLMediaElement;
|
||||
|
||||
currentSession = await getSession(opts);
|
||||
currentMedia = await getMedia(opts);
|
||||
|
||||
if (targetElement instanceof HTMLMediaElement) {
|
||||
registerMediaElementListeners(targetElement);
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", async () => {
|
||||
backgroundPort.postMessage({
|
||||
subject: "bridge:mediaServer/stop"
|
||||
});
|
||||
|
||||
if (await options.get("mediaStopOnUnload")) {
|
||||
currentSession.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If loaded as a content script, the init values are
|
||||
* provided on the window object.
|
||||
*/
|
||||
if (window.location.protocol !== "moz-extension:") {
|
||||
const _window = window as any;
|
||||
|
||||
init({
|
||||
mediaUrl: _window.mediaUrl,
|
||||
receiver: _window.receiver,
|
||||
targetElementId: _window.targetElementId
|
||||
});
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import type { Error as Error_ } from "./sdk/classes";
|
||||
import type Media from "./sdk/media/Media";
|
||||
|
||||
export type SuccessCallback = () => void;
|
||||
export type ErrorCallback = (err: Error_) => void;
|
||||
|
||||
export type MediaListener = (media: Media) => void;
|
||||
export type MessageListener = (namespace: string, message: string) => void;
|
||||
export type UpdateListener = (isAlive: boolean) => void;
|
||||
export type LoadSuccessCallback = (media: Media) => void;
|
||||
5
ext/src/global.d.ts
vendored
5
ext/src/global.d.ts
vendored
@@ -11,11 +11,6 @@ declare type DistributiveOmit<T, K extends keyof any> = T extends any
|
||||
? Omit<T, K>
|
||||
: never;
|
||||
|
||||
declare interface Object {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
wrappedJSObject: Object;
|
||||
}
|
||||
|
||||
declare interface CanvasRenderingContext2D {
|
||||
DRAWWINDOW_DRAW_CARET: 0x01;
|
||||
DRAWWINDOW_DO_NOT_FLUSH: 0x02;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logger from "../lib/logger";
|
||||
import { TypedStorageArea } from "../lib/TypedStorageArea";
|
||||
import logger from "./logger";
|
||||
import { TypedStorageArea } from "./TypedStorageArea";
|
||||
|
||||
const ENDPOINT = "https://clients3.google.com/cast/chromecast/device";
|
||||
|
||||
@@ -28,15 +28,6 @@
|
||||
"scripts": ["background/background.js"]
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"all_frames": true,
|
||||
"js": ["cast/content.js"],
|
||||
"matches": ["<all_urls>"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
|
||||
"default_locale": "en",
|
||||
|
||||
"icons": {
|
||||
@@ -62,5 +53,5 @@
|
||||
"webRequestBlocking",
|
||||
"<all_urls>"
|
||||
],
|
||||
"web_accessible_resources": ["cast/index.js"]
|
||||
"web_accessible_resources": ["cast/content.js"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ import type {
|
||||
} from "./cast/sdk/types";
|
||||
import type { ApiConfig, Receiver, SessionRequest } from "./cast/sdk/classes";
|
||||
|
||||
import type { ReceiverDevice, ReceiverSelectorMediaType } from "./types";
|
||||
import type {
|
||||
ReceiverDevice,
|
||||
ReceiverSelectorAppInfo,
|
||||
ReceiverSelectorMediaType,
|
||||
ReceiverSelectorPageInfo
|
||||
} from "./types";
|
||||
import type { ReceiverAction } from "./cast/sdk/enums";
|
||||
|
||||
/**
|
||||
@@ -41,12 +46,8 @@ import type { ReceiverAction } from "./cast/sdk/enums";
|
||||
type ExtMessageDefinitions = {
|
||||
/** Initial data to send to selector popup. */
|
||||
"popup:init": {
|
||||
appId?: string;
|
||||
pageInfo?: {
|
||||
url: string;
|
||||
tabId: number;
|
||||
frameId: number;
|
||||
};
|
||||
appInfo?: ReceiverSelectorAppInfo;
|
||||
pageInfo?: ReceiverSelectorPageInfo;
|
||||
};
|
||||
/** Updates selector popup with new data. */
|
||||
"popup:update": {
|
||||
@@ -74,17 +75,17 @@ type ExtMessageDefinitions = {
|
||||
* Sent from the cast API to trigger receiver selection on session
|
||||
* request.
|
||||
*/
|
||||
"main:selectReceiver": {
|
||||
"main:requestSession": {
|
||||
sessionRequest: SessionRequest;
|
||||
};
|
||||
/** Return message to the cast API when a selection is cancelled. */
|
||||
"cast:selectReceiver/cancelled": undefined;
|
||||
"cast:sessionRequestCancelled": undefined;
|
||||
|
||||
/**
|
||||
* Sent to the cast API when a session is requested or stopped via
|
||||
* the extension UI.
|
||||
*/
|
||||
"cast:sendReceiverAction": { receiver: Receiver; action: ReceiverAction };
|
||||
"cast:receiverAction": { receiver: Receiver; action: ReceiverAction };
|
||||
|
||||
/**
|
||||
* Tells the cast manager to provide the cast API instance with
|
||||
@@ -96,7 +97,7 @@ type ExtMessageDefinitions = {
|
||||
"cast:sessionCreated": CastSessionCreatedDetails & { receiver: Receiver };
|
||||
"cast:sessionUpdated": CastSessionUpdatedDetails;
|
||||
|
||||
"cast:updateReceiverAvailability": { isAvailable: boolean };
|
||||
"cast:receiverAvailabilityUpdated": { isAvailable: boolean };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -227,7 +228,7 @@ type AppMessageDefinitions = {
|
||||
* Sent to cast API instance from bridge when session message
|
||||
* received from a receiver device.
|
||||
*/
|
||||
"cast:receivedSessionMessage": {
|
||||
"cast:sessionMessageReceived": {
|
||||
sessionId: string;
|
||||
namespace: string;
|
||||
messageData: string;
|
||||
|
||||
@@ -27,14 +27,17 @@ export enum ReceiverSelectorMediaType {
|
||||
None = 0,
|
||||
App = 1,
|
||||
Tab = 2,
|
||||
Screen = 4,
|
||||
File = 8
|
||||
Screen = 4
|
||||
}
|
||||
|
||||
export interface ReceiverSelectorAppInfo {
|
||||
sessionRequest: SessionRequest;
|
||||
isRequestAppAudioCompatible?: boolean;
|
||||
}
|
||||
|
||||
/** Info about sender page context. */
|
||||
export interface ReceiverSelectorPageInfo {
|
||||
url: string;
|
||||
tabId: number;
|
||||
frameId: number;
|
||||
sessionRequest?: SessionRequest;
|
||||
isRequestAppAudioCompatible?: boolean;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import {
|
||||
ReceiverDevice,
|
||||
ReceiverDeviceCapabilities,
|
||||
ReceiverSelectorAppInfo,
|
||||
ReceiverSelectorMediaType,
|
||||
ReceiverSelectorPageInfo
|
||||
} from "../../types";
|
||||
@@ -29,8 +30,8 @@
|
||||
/** Devices to display. */
|
||||
let devices: ReceiverDevice[] = [];
|
||||
|
||||
/** Sender app ID (if available). */
|
||||
let appId: Optional<string>;
|
||||
/** Sender app info (if available). */
|
||||
let appInfo: Optional<ReceiverSelectorAppInfo>;
|
||||
/** Page info (if launched from page context). */
|
||||
let pageInfo: Optional<ReceiverSelectorPageInfo>;
|
||||
|
||||
@@ -70,14 +71,14 @@
|
||||
// If device is audio-only, check app's audio support flag
|
||||
if (
|
||||
!(device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT) &&
|
||||
pageInfo?.isRequestAppAudioCompatible === false
|
||||
appInfo?.isRequestAppAudioCompatible === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasRequiredCapabilities(
|
||||
device,
|
||||
pageInfo?.sessionRequest?.capabilities
|
||||
appInfo?.sessionRequest?.capabilities
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,7 +149,7 @@
|
||||
function onMessage(message: Message) {
|
||||
switch (message.subject) {
|
||||
case "popup:init":
|
||||
appId = message.data.appId;
|
||||
appInfo = message.data.appInfo;
|
||||
pageInfo = message.data.pageInfo;
|
||||
break;
|
||||
|
||||
@@ -191,8 +192,8 @@
|
||||
* Check knownApps for an app with an ID matching the registered
|
||||
* app on the target page.
|
||||
*/
|
||||
if (isAppMediaTypeAvailable && appId) {
|
||||
newKnownApp = knownApps[appId];
|
||||
if (isAppMediaTypeAvailable && appInfo?.sessionRequest.appId) {
|
||||
newKnownApp = knownApps[appInfo.sessionRequest.appId];
|
||||
} else if (pageInfo) {
|
||||
const pageUrl = pageInfo.url;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user