Handle audio device support checking

This commit is contained in:
hensm
2022-08-25 05:20:02 +01:00
parent 04c3dbf397
commit 41094ca4b3
8 changed files with 170 additions and 54 deletions

View File

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

View File

@@ -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
View 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;
}
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -41,4 +41,5 @@ export interface ReceiverSelectorPageInfo {
tabId: number;
frameId: number;
sessionRequest?: SessionRequest;
isRequestAppAudioCompatible?: boolean;
}

View File

@@ -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)}