mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-10 01:29: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 { initWhitelist } from "./whitelist";
|
||||
import { baseConfigStorage, fetchBaseConfig } from "../cast/googleapi";
|
||||
|
||||
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;
|
||||
|
||||
async function init() {
|
||||
@@ -130,4 +156,5 @@ async function init() {
|
||||
});
|
||||
}
|
||||
|
||||
cacheBaseConfig();
|
||||
init();
|
||||
|
||||
@@ -4,6 +4,7 @@ import options from "../lib/options";
|
||||
import logger from "../lib/logger";
|
||||
|
||||
import { getMediaTypesForPageUrl } from "../lib/utils";
|
||||
import { BaseConfig, baseConfigStorage, getAppTag } from "../cast/googleapi";
|
||||
import { SessionRequest } from "../cast/sdk/classes";
|
||||
|
||||
import castManager from "./castManager";
|
||||
@@ -22,6 +23,16 @@ import {
|
||||
ReceiverSelectorMediaType
|
||||
} 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;
|
||||
async function getSelector() {
|
||||
if (!sharedSelector) {
|
||||
@@ -215,6 +226,12 @@ async function getSelection(
|
||||
// Ensure status manager is initialized
|
||||
await deviceManager.init();
|
||||
|
||||
let isRequestAppAudioCompatible: Optional<boolean>;
|
||||
if (castInstance?.appId) {
|
||||
const appTag = getAppTag(baseConfig, castInstance.appId);
|
||||
isRequestAppAudioCompatible = appTag?.supports_audio_only;
|
||||
}
|
||||
|
||||
sharedSelector.open({
|
||||
receiverDevices: deviceManager.getDevices(),
|
||||
defaultMediaType,
|
||||
@@ -226,7 +243,8 @@ async function getSelection(
|
||||
url: pageUrl,
|
||||
tabId: contextTabId,
|
||||
frameId: contextFrameId,
|
||||
sessionRequest: selectionOpts?.sessionRequest
|
||||
sessionRequest: selectionOpts?.sessionRequest,
|
||||
isRequestAppAudioCompatible
|
||||
}
|
||||
: 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 { ReceiverDevice, ReceiverDeviceCapabilities } from "../../types";
|
||||
import { ErrorCallback, SuccessCallback } from "../types";
|
||||
|
||||
import { Message } from "../../messaging";
|
||||
import eventMessaging from "../eventMessaging";
|
||||
|
||||
import { ReceiverDevice } from "../../types";
|
||||
import { ErrorCallback, SuccessCallback } from "../types";
|
||||
|
||||
import {
|
||||
AutoJoinPolicy,
|
||||
Capability,
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
import Session from "./Session";
|
||||
|
||||
import media from "./media";
|
||||
import { Message } from "../../messaging";
|
||||
import { convertCapabilitiesFlags } from "../utils";
|
||||
|
||||
type ReceiverActionListener = (
|
||||
receiver: Receiver,
|
||||
@@ -51,23 +52,11 @@ type RequestSessionSuccessCallback = (session: Session) => void;
|
||||
* Create `chrome.cast.Receiver` object from receiver device info.
|
||||
*/
|
||||
function createReceiver(device: ReceiverDevice) {
|
||||
// Convert capabilities bitflag to string array
|
||||
const capabilities: Capability[] = [];
|
||||
if (device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT) {
|
||||
capabilities.push(Capability.VIDEO_OUT);
|
||||
} 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);
|
||||
const receiver = new Receiver(
|
||||
device.id,
|
||||
device.friendlyName,
|
||||
convertCapabilitiesFlags(device.capabilities)
|
||||
);
|
||||
|
||||
// Currently only supports CAST receivers
|
||||
receiver.receiverType = ReceiverType.CAST;
|
||||
@@ -178,11 +167,11 @@ export default class {
|
||||
);
|
||||
|
||||
const session = new Session(
|
||||
status.sessionId, // sessionId
|
||||
status.appId, // appId
|
||||
status.displayName, // displayName
|
||||
status.appImages, // appImages
|
||||
receiver // receiver
|
||||
status.sessionId,
|
||||
status.appId,
|
||||
status.displayName,
|
||||
status.appImages,
|
||||
receiver
|
||||
);
|
||||
|
||||
session.namespaces = status.namespaces;
|
||||
@@ -221,11 +210,8 @@ export default class {
|
||||
session.namespaces = status.namespaces;
|
||||
session.receiver.volume = status.volume;
|
||||
|
||||
const updateListeners = session?._updateListeners;
|
||||
if (updateListeners) {
|
||||
for (const listener of updateListeners) {
|
||||
listener(session.status !== SessionStatus.STOPPED);
|
||||
}
|
||||
for (const listener of session._updateListeners) {
|
||||
listener(session.status !== SessionStatus.STOPPED);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -236,12 +222,8 @@ export default class {
|
||||
const session = this.#sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.status = SessionStatus.STOPPED;
|
||||
|
||||
const updateListeners = session?._updateListeners;
|
||||
if (updateListeners) {
|
||||
for (const listener of updateListeners) {
|
||||
listener(false);
|
||||
}
|
||||
for (const listener of session._updateListeners) {
|
||||
listener(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,9 +234,7 @@ export default class {
|
||||
const { sessionId, namespace, messageData } = message.data;
|
||||
const session = this.#sessions.get(sessionId);
|
||||
if (session) {
|
||||
const _messageListeners = session._messageListeners;
|
||||
const listeners = _messageListeners.get(namespace);
|
||||
|
||||
const listeners = session._messageListeners.get(namespace);
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener(namespace, messageData);
|
||||
|
||||
@@ -140,7 +140,7 @@ export default class Media {
|
||||
* information reported by the receiver.
|
||||
*/
|
||||
getEstimatedTime(): number {
|
||||
if (this.playerState === PlayerState.PLAYING && this._lastUpdateTime) {
|
||||
if (this.playerState === PlayerState.PLAYING) {
|
||||
return getEstimatedTime({
|
||||
currentTime: this.currentTime,
|
||||
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 {
|
||||
currentTime: number;
|
||||
lastUpdateTime: number;
|
||||
|
||||
@@ -41,4 +41,5 @@ export interface ReceiverSelectorPageInfo {
|
||||
tabId: number;
|
||||
frameId: number;
|
||||
sessionRequest?: SessionRequest;
|
||||
isRequestAppAudioCompatible?: boolean;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import {
|
||||
ReceiverDevice,
|
||||
ReceiverDeviceCapabilities,
|
||||
ReceiverSelectionActionType,
|
||||
ReceiverSelectorMediaType,
|
||||
ReceiverSelectorPageInfo
|
||||
@@ -58,6 +59,25 @@
|
||||
// If an app is not already loaded on the page
|
||||
!(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 browserWindow: Nullable<browser.windows.Window> = null;
|
||||
let resizeObserver = new ResizeObserver(() => fitWindowHeight());
|
||||
@@ -138,17 +158,6 @@
|
||||
break;
|
||||
|
||||
case "popup:update": {
|
||||
/**
|
||||
* Filter receiver devices without the required
|
||||
* capabilities.
|
||||
*/
|
||||
$deviceStore = message.data.receiverDevices.filter(device =>
|
||||
hasRequiredCapabilities(
|
||||
device,
|
||||
pageInfo?.sessionRequest?.capabilities
|
||||
)
|
||||
);
|
||||
|
||||
if (
|
||||
message.data.availableMediaTypes !== undefined &&
|
||||
message.data.defaultMediaType !== undefined
|
||||
@@ -161,6 +170,9 @@
|
||||
}
|
||||
|
||||
updateKnownApp();
|
||||
|
||||
$deviceStore = message.data.receiverDevices;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -418,7 +430,7 @@
|
||||
{device}
|
||||
{isMediaTypeAvailable}
|
||||
isAnyMediaTypeAvailable={availableMediaTypes !==
|
||||
ReceiverSelectorMediaType.None}
|
||||
ReceiverSelectorMediaType.None && isDeviceCompatible(device)}
|
||||
isAnyConnecting={isConnecting}
|
||||
on:cast={ev => onReceiverCast(ev.detail.device)}
|
||||
on:stop={ev => onReceiverStop(ev.detail.device)}
|
||||
|
||||
Reference in New Issue
Block a user