mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-12 18:39:58 +00:00
Handle audio device support checking
This commit is contained in:
@@ -11,6 +11,7 @@ import selectorManager from "./selectorManager";
|
|||||||
|
|
||||||
import { initMenus } from "./menus";
|
import { initMenus } from "./menus";
|
||||||
import { initWhitelist } from "./whitelist";
|
import { initWhitelist } from "./whitelist";
|
||||||
|
import { baseConfigStorage, fetchBaseConfig } from "../cast/googleapi";
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
@@ -81,6 +82,31 @@ async function notifyBridgeCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates locally-stored base config data if never downloaded or since
|
||||||
|
* expired.
|
||||||
|
*/
|
||||||
|
async function cacheBaseConfig() {
|
||||||
|
const { baseConfigUpdated } = await baseConfigStorage.get(
|
||||||
|
"baseConfigUpdated"
|
||||||
|
);
|
||||||
|
|
||||||
|
// If never updated or updated more than 48 hours ago
|
||||||
|
if (
|
||||||
|
!baseConfigUpdated ||
|
||||||
|
(Date.now() - baseConfigUpdated) / 1000 >= 172800
|
||||||
|
) {
|
||||||
|
logger.info("Fetching updated Chromecast base config...");
|
||||||
|
const baseConfig = await fetchBaseConfig();
|
||||||
|
if (baseConfig) {
|
||||||
|
await baseConfigStorage.set({
|
||||||
|
baseConfig,
|
||||||
|
baseConfigUpdated: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@@ -130,4 +156,5 @@ async function init() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cacheBaseConfig();
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import options from "../lib/options";
|
|||||||
import logger from "../lib/logger";
|
import logger from "../lib/logger";
|
||||||
|
|
||||||
import { getMediaTypesForPageUrl } from "../lib/utils";
|
import { getMediaTypesForPageUrl } from "../lib/utils";
|
||||||
|
import { BaseConfig, baseConfigStorage, getAppTag } from "../cast/googleapi";
|
||||||
import { SessionRequest } from "../cast/sdk/classes";
|
import { SessionRequest } from "../cast/sdk/classes";
|
||||||
|
|
||||||
import castManager from "./castManager";
|
import castManager from "./castManager";
|
||||||
@@ -22,6 +23,16 @@ import {
|
|||||||
ReceiverSelectorMediaType
|
ReceiverSelectorMediaType
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
|
let baseConfig: BaseConfig;
|
||||||
|
baseConfigStorage
|
||||||
|
.get("baseConfig")
|
||||||
|
.then(value => {
|
||||||
|
baseConfig = value.baseConfig;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
logger.error("Failed to get Chromecast base config!");
|
||||||
|
});
|
||||||
|
|
||||||
let sharedSelector: ReceiverSelector;
|
let sharedSelector: ReceiverSelector;
|
||||||
async function getSelector() {
|
async function getSelector() {
|
||||||
if (!sharedSelector) {
|
if (!sharedSelector) {
|
||||||
@@ -215,6 +226,12 @@ async function getSelection(
|
|||||||
// Ensure status manager is initialized
|
// Ensure status manager is initialized
|
||||||
await deviceManager.init();
|
await deviceManager.init();
|
||||||
|
|
||||||
|
let isRequestAppAudioCompatible: Optional<boolean>;
|
||||||
|
if (castInstance?.appId) {
|
||||||
|
const appTag = getAppTag(baseConfig, castInstance.appId);
|
||||||
|
isRequestAppAudioCompatible = appTag?.supports_audio_only;
|
||||||
|
}
|
||||||
|
|
||||||
sharedSelector.open({
|
sharedSelector.open({
|
||||||
receiverDevices: deviceManager.getDevices(),
|
receiverDevices: deviceManager.getDevices(),
|
||||||
defaultMediaType,
|
defaultMediaType,
|
||||||
@@ -226,7 +243,8 @@ async function getSelection(
|
|||||||
url: pageUrl,
|
url: pageUrl,
|
||||||
tabId: contextTabId,
|
tabId: contextTabId,
|
||||||
frameId: contextFrameId,
|
frameId: contextFrameId,
|
||||||
sessionRequest: selectionOpts?.sessionRequest
|
sessionRequest: selectionOpts?.sessionRequest,
|
||||||
|
isRequestAppAudioCompatible
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
});
|
});
|
||||||
|
|||||||
60
ext/src/cast/googleApi.ts
Normal file
60
ext/src/cast/googleApi.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import logger from "../lib/logger";
|
||||||
|
import { TypedStorageArea } from "../lib/TypedStorageArea";
|
||||||
|
|
||||||
|
const ENDPOINT = "https://clients3.google.com/cast/chromecast/device";
|
||||||
|
|
||||||
|
export interface BaseConfig {
|
||||||
|
app_tags: Array<{
|
||||||
|
supports_audio_only: boolean;
|
||||||
|
suports_video: boolean;
|
||||||
|
app_id: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseConfigStorage = new TypedStorageArea<{
|
||||||
|
baseConfig: BaseConfig;
|
||||||
|
baseConfigUpdated: number;
|
||||||
|
}>(browser.storage.local);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches Chromecast base config data subset.
|
||||||
|
*/
|
||||||
|
export async function fetchBaseConfig(): Promise<BaseConfig | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${ENDPOINT}/baseconfig`);
|
||||||
|
const baseConfig = JSON.parse((await res.text()).slice(4));
|
||||||
|
|
||||||
|
// Strip other properties
|
||||||
|
return { app_tags: baseConfig.app_tags };
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to fetch Chromecast base config!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app tag from base config.
|
||||||
|
* @param baseConfig Base config data.
|
||||||
|
* @param appId Chromecast app ID.
|
||||||
|
*/
|
||||||
|
export function getAppTag(baseConfig: BaseConfig, appId: string) {
|
||||||
|
// App tag IDs are represented as 32-bit signed integers
|
||||||
|
const signedAppId = (parseInt(appId, 16) << 32) >> 32;
|
||||||
|
return baseConfig.app_tags.find(tag => tag.app_id === signedAppId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches Chromecast app config.
|
||||||
|
*
|
||||||
|
* @param appId Chromecast app ID
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function fetchAppConfig(appId: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${ENDPOINT}/app?a=${appId}`);
|
||||||
|
return JSON.parse((await res.text()).slice(4));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to fetch Chromecast app config!", { appId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import logger from "../../lib/logger";
|
import logger from "../../lib/logger";
|
||||||
|
|
||||||
import { ReceiverDevice, ReceiverDeviceCapabilities } from "../../types";
|
import { Message } from "../../messaging";
|
||||||
import { ErrorCallback, SuccessCallback } from "../types";
|
|
||||||
|
|
||||||
import eventMessaging from "../eventMessaging";
|
import eventMessaging from "../eventMessaging";
|
||||||
|
|
||||||
|
import { ReceiverDevice } from "../../types";
|
||||||
|
import { ErrorCallback, SuccessCallback } from "../types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AutoJoinPolicy,
|
AutoJoinPolicy,
|
||||||
Capability,
|
Capability,
|
||||||
@@ -38,7 +39,7 @@ import {
|
|||||||
import Session from "./Session";
|
import Session from "./Session";
|
||||||
|
|
||||||
import media from "./media";
|
import media from "./media";
|
||||||
import { Message } from "../../messaging";
|
import { convertCapabilitiesFlags } from "../utils";
|
||||||
|
|
||||||
type ReceiverActionListener = (
|
type ReceiverActionListener = (
|
||||||
receiver: Receiver,
|
receiver: Receiver,
|
||||||
@@ -51,23 +52,11 @@ type RequestSessionSuccessCallback = (session: Session) => void;
|
|||||||
* Create `chrome.cast.Receiver` object from receiver device info.
|
* Create `chrome.cast.Receiver` object from receiver device info.
|
||||||
*/
|
*/
|
||||||
function createReceiver(device: ReceiverDevice) {
|
function createReceiver(device: ReceiverDevice) {
|
||||||
// Convert capabilities bitflag to string array
|
const receiver = new Receiver(
|
||||||
const capabilities: Capability[] = [];
|
device.id,
|
||||||
if (device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT) {
|
device.friendlyName,
|
||||||
capabilities.push(Capability.VIDEO_OUT);
|
convertCapabilitiesFlags(device.capabilities)
|
||||||
} else if (device.capabilities & ReceiverDeviceCapabilities.VIDEO_IN) {
|
);
|
||||||
capabilities.push(Capability.VIDEO_IN);
|
|
||||||
} else if (device.capabilities & ReceiverDeviceCapabilities.AUDIO_OUT) {
|
|
||||||
capabilities.push(Capability.AUDIO_OUT);
|
|
||||||
} else if (device.capabilities & ReceiverDeviceCapabilities.AUDIO_IN) {
|
|
||||||
capabilities.push(Capability.AUDIO_IN);
|
|
||||||
} else if (
|
|
||||||
device.capabilities & ReceiverDeviceCapabilities.MULTIZONE_GROUP
|
|
||||||
) {
|
|
||||||
capabilities.push(Capability.MULTIZONE_GROUP);
|
|
||||||
}
|
|
||||||
|
|
||||||
const receiver = new Receiver(device.id, device.friendlyName, capabilities);
|
|
||||||
|
|
||||||
// Currently only supports CAST receivers
|
// Currently only supports CAST receivers
|
||||||
receiver.receiverType = ReceiverType.CAST;
|
receiver.receiverType = ReceiverType.CAST;
|
||||||
@@ -178,11 +167,11 @@ export default class {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const session = new Session(
|
const session = new Session(
|
||||||
status.sessionId, // sessionId
|
status.sessionId,
|
||||||
status.appId, // appId
|
status.appId,
|
||||||
status.displayName, // displayName
|
status.displayName,
|
||||||
status.appImages, // appImages
|
status.appImages,
|
||||||
receiver // receiver
|
receiver
|
||||||
);
|
);
|
||||||
|
|
||||||
session.namespaces = status.namespaces;
|
session.namespaces = status.namespaces;
|
||||||
@@ -221,11 +210,8 @@ export default class {
|
|||||||
session.namespaces = status.namespaces;
|
session.namespaces = status.namespaces;
|
||||||
session.receiver.volume = status.volume;
|
session.receiver.volume = status.volume;
|
||||||
|
|
||||||
const updateListeners = session?._updateListeners;
|
for (const listener of session._updateListeners) {
|
||||||
if (updateListeners) {
|
listener(session.status !== SessionStatus.STOPPED);
|
||||||
for (const listener of updateListeners) {
|
|
||||||
listener(session.status !== SessionStatus.STOPPED);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -236,12 +222,8 @@ export default class {
|
|||||||
const session = this.#sessions.get(sessionId);
|
const session = this.#sessions.get(sessionId);
|
||||||
if (session) {
|
if (session) {
|
||||||
session.status = SessionStatus.STOPPED;
|
session.status = SessionStatus.STOPPED;
|
||||||
|
for (const listener of session._updateListeners) {
|
||||||
const updateListeners = session?._updateListeners;
|
listener(false);
|
||||||
if (updateListeners) {
|
|
||||||
for (const listener of updateListeners) {
|
|
||||||
listener(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,9 +234,7 @@ export default class {
|
|||||||
const { sessionId, namespace, messageData } = message.data;
|
const { sessionId, namespace, messageData } = message.data;
|
||||||
const session = this.#sessions.get(sessionId);
|
const session = this.#sessions.get(sessionId);
|
||||||
if (session) {
|
if (session) {
|
||||||
const _messageListeners = session._messageListeners;
|
const listeners = session._messageListeners.get(namespace);
|
||||||
const listeners = _messageListeners.get(namespace);
|
|
||||||
|
|
||||||
if (listeners) {
|
if (listeners) {
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
listener(namespace, messageData);
|
listener(namespace, messageData);
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export default class Media {
|
|||||||
* information reported by the receiver.
|
* information reported by the receiver.
|
||||||
*/
|
*/
|
||||||
getEstimatedTime(): number {
|
getEstimatedTime(): number {
|
||||||
if (this.playerState === PlayerState.PLAYING && this._lastUpdateTime) {
|
if (this.playerState === PlayerState.PLAYING) {
|
||||||
return getEstimatedTime({
|
return getEstimatedTime({
|
||||||
currentTime: this.currentTime,
|
currentTime: this.currentTime,
|
||||||
lastUpdateTime: this._lastUpdateTime,
|
lastUpdateTime: this._lastUpdateTime,
|
||||||
|
|||||||
@@ -28,6 +28,24 @@ export function hasRequiredCapabilities(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertCapabilitiesFlags(flags: ReceiverDeviceCapabilities) {
|
||||||
|
// Convert capabilities bitflag to string array
|
||||||
|
const capabilities: Capability[] = [];
|
||||||
|
if (flags & ReceiverDeviceCapabilities.VIDEO_OUT)
|
||||||
|
capabilities.push(Capability.VIDEO_OUT);
|
||||||
|
if (flags & ReceiverDeviceCapabilities.VIDEO_IN)
|
||||||
|
capabilities.push(Capability.VIDEO_IN);
|
||||||
|
if (flags & ReceiverDeviceCapabilities.AUDIO_OUT)
|
||||||
|
capabilities.push(Capability.AUDIO_OUT);
|
||||||
|
if (flags & ReceiverDeviceCapabilities.AUDIO_IN)
|
||||||
|
capabilities.push(Capability.AUDIO_IN);
|
||||||
|
|
||||||
|
if (flags & ReceiverDeviceCapabilities.MULTIZONE_GROUP)
|
||||||
|
capabilities.push(Capability.MULTIZONE_GROUP);
|
||||||
|
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
interface GetEstimatedTimeOpts {
|
interface GetEstimatedTimeOpts {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
lastUpdateTime: number;
|
lastUpdateTime: number;
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ export interface ReceiverSelectorPageInfo {
|
|||||||
tabId: number;
|
tabId: number;
|
||||||
frameId: number;
|
frameId: number;
|
||||||
sessionRequest?: SessionRequest;
|
sessionRequest?: SessionRequest;
|
||||||
|
isRequestAppAudioCompatible?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ReceiverDevice,
|
ReceiverDevice,
|
||||||
|
ReceiverDeviceCapabilities,
|
||||||
ReceiverSelectionActionType,
|
ReceiverSelectionActionType,
|
||||||
ReceiverSelectorMediaType,
|
ReceiverSelectorMediaType,
|
||||||
ReceiverSelectorPageInfo
|
ReceiverSelectorPageInfo
|
||||||
@@ -58,6 +59,25 @@
|
|||||||
// If an app is not already loaded on the page
|
// If an app is not already loaded on the page
|
||||||
!(availableMediaTypes & ReceiverSelectorMediaType.App);
|
!(availableMediaTypes & ReceiverSelectorMediaType.App);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if device is compatible with the requested app and
|
||||||
|
* capabilities.
|
||||||
|
*/
|
||||||
|
function isDeviceCompatible(device: ReceiverDevice) {
|
||||||
|
// If device is audio-only, check app's audio support flag
|
||||||
|
if (
|
||||||
|
!(device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT) &&
|
||||||
|
pageInfo?.isRequestAppAudioCompatible === false
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasRequiredCapabilities(
|
||||||
|
device,
|
||||||
|
pageInfo?.sessionRequest?.capabilities
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let port: Nullable<Port> = null;
|
let port: Nullable<Port> = null;
|
||||||
let browserWindow: Nullable<browser.windows.Window> = null;
|
let browserWindow: Nullable<browser.windows.Window> = null;
|
||||||
let resizeObserver = new ResizeObserver(() => fitWindowHeight());
|
let resizeObserver = new ResizeObserver(() => fitWindowHeight());
|
||||||
@@ -138,17 +158,6 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "popup:update": {
|
case "popup:update": {
|
||||||
/**
|
|
||||||
* Filter receiver devices without the required
|
|
||||||
* capabilities.
|
|
||||||
*/
|
|
||||||
$deviceStore = message.data.receiverDevices.filter(device =>
|
|
||||||
hasRequiredCapabilities(
|
|
||||||
device,
|
|
||||||
pageInfo?.sessionRequest?.capabilities
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
message.data.availableMediaTypes !== undefined &&
|
message.data.availableMediaTypes !== undefined &&
|
||||||
message.data.defaultMediaType !== undefined
|
message.data.defaultMediaType !== undefined
|
||||||
@@ -161,6 +170,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateKnownApp();
|
updateKnownApp();
|
||||||
|
|
||||||
|
$deviceStore = message.data.receiverDevices;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,7 +430,7 @@
|
|||||||
{device}
|
{device}
|
||||||
{isMediaTypeAvailable}
|
{isMediaTypeAvailable}
|
||||||
isAnyMediaTypeAvailable={availableMediaTypes !==
|
isAnyMediaTypeAvailable={availableMediaTypes !==
|
||||||
ReceiverSelectorMediaType.None}
|
ReceiverSelectorMediaType.None && isDeviceCompatible(device)}
|
||||||
isAnyConnecting={isConnecting}
|
isAnyConnecting={isConnecting}
|
||||||
on:cast={ev => onReceiverCast(ev.detail.device)}
|
on:cast={ev => onReceiverCast(ev.detail.device)}
|
||||||
on:stop={ev => onReceiverStop(ev.detail.device)}
|
on:stop={ev => onReceiverStop(ev.detail.device)}
|
||||||
|
|||||||
Reference in New Issue
Block a user