Add better support for handling device capabilities and receiver objects

This commit is contained in:
hensm
2022-04-17 07:49:01 +01:00
parent 1da709eb5e
commit b672b8d722
13 changed files with 159 additions and 77 deletions

View File

@@ -27,13 +27,15 @@ export default class Remote extends CastClient {
constructor(private host: string, private options?: CastRemoteOptions) { constructor(private host: string, private options?: CastRemoteOptions) {
super(); super();
super.connect(host, { super
onReceiverMessage: message => { .connect(host, {
this.onReceiverMessage(message); onReceiverMessage: message => {
} this.onReceiverMessage(message);
}).then(() => { }
this.sendReceiverMessage({ type: "GET_STATUS" }); })
}); .then(() => {
this.sendReceiverMessage({ type: "GET_STATUS" });
});
} }
disconnect() { disconnect() {

View File

@@ -16,6 +16,8 @@ interface CastRecord {
md: string; md: string;
// Friendly name (user-visible) // Friendly name (user-visible)
fn: string; fn: string;
// Capabilities
ca: string;
// Version (?) // Version (?)
ve: string; ve: string;
// Icon path (?) // Icon path (?)
@@ -23,7 +25,6 @@ interface CastRecord {
cd: string; cd: string;
rm: string; rm: string;
ca: string;
st: string; st: string;
bs: string; bs: string;
nf: string; nf: string;
@@ -71,16 +72,18 @@ browser.on("serviceUp", service => {
const record = service.txtRecord as CastRecord; const record = service.txtRecord as CastRecord;
const device: ReceiverDevice = { const device: ReceiverDevice = {
id: record.id,
friendlyName: record.fn,
modelName: record.md,
capabilities: parseInt(record.ca),
host: service.addresses[0], host: service.addresses[0],
port: service.port, port: service.port
id: service.name,
friendlyName: record.fn
}; };
sendMessage({ sendMessage({
subject: "main:receiverDeviceUp", subject: "main:receiverDeviceUp",
data: { data: {
deviceId: service.name, deviceId: device.id,
deviceInfo: device deviceInfo: device
} }
}); });

View File

@@ -7,10 +7,21 @@ import {
Volume Volume
} from "./components/cast/types"; } from "./components/cast/types";
export enum ReceiverDeviceCapabilities {
NONE = 0,
VIDEO_OUT = 1,
VIDEO_IN = 2,
AUDIO_OUT = 4,
AUDIO_IN = 8,
MULTIZONE_GROUP = 32
}
export interface ReceiverDevice { export interface ReceiverDevice {
host: string;
friendlyName: string;
id: string; id: string;
friendlyName: string;
modelName: string;
capabilities: ReceiverDeviceCapabilities;
host: string;
port: number; port: number;
status?: ReceiverStatus; status?: ReceiverStatus;
} }

View File

@@ -137,7 +137,7 @@ browser.menus.onClicked.addListener(async (info, tab) => {
if (selection.mediaType === ReceiverSelectorMediaType.App) { if (selection.mediaType === ReceiverSelectorMediaType.App) {
await browser.tabs.executeScript(tab.id, { await browser.tabs.executeScript(tab.id, {
code: stringify` code: stringify`
window.receiver = ${selection.receiver}; window.receiver = ${selection.receiverDevice};
window.mediaUrl = ${info.srcUrl}; window.mediaUrl = ${info.srcUrl};
window.targetElementId = ${info.targetElementId}; window.targetElementId = ${info.targetElementId};
`, `,

View File

@@ -5,7 +5,7 @@ import logger from "../lib/logger";
import { TypedEventTarget } from "../lib/TypedEventTarget"; import { TypedEventTarget } from "../lib/TypedEventTarget";
import { Message, Port } from "../messaging"; import { Message, Port } from "../messaging";
import { ReceiverDevice } from "../types"; import { ReceiverDevice, ReceiverDeviceCapabilities } from "../types";
import { ReceiverStatus } from "../cast/api/types"; import { ReceiverStatus } from "../cast/api/types";
interface EventMap { interface EventMap {
@@ -84,6 +84,16 @@ export default new (class extends TypedEventTarget<EventMap> {
case "main:receiverDeviceUp": { case "main:receiverDeviceUp": {
const { deviceId, deviceInfo } = message.data; const { deviceId, deviceInfo } = message.data;
// TODO: Add proper support for Chromecast Audio devices
if (
!(
deviceInfo.capabilities &
ReceiverDeviceCapabilities.VIDEO_OUT
)
) {
break;
}
this.receiverDevices.set(deviceId, deviceInfo); this.receiverDevices.set(deviceId, deviceInfo);
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("receiverDeviceUp", { new CustomEvent("receiverDeviceUp", {
@@ -112,9 +122,7 @@ export default new (class extends TypedEventTarget<EventMap> {
case "main:receiverDeviceStatusUpdated": { case "main:receiverDeviceStatusUpdated": {
const { deviceId, status } = message.data; const { deviceId, status } = message.data;
const receiverDevice = this.receiverDevices.get(deviceId); const receiverDevice = this.receiverDevices.get(deviceId);
if (!receiverDevice) { if (!receiverDevice) {
logger.error(`Receiver ID \`${deviceId}\` not found!`);
break; break;
} }

View File

@@ -35,7 +35,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
private messagePort?: Port; private messagePort?: Port;
private messagePortDisconnected?: boolean; private messagePortDisconnected?: boolean;
private receivers?: ReceiverDevice[]; private receiverDevices?: ReceiverDevice[];
private defaultMediaType?: ReceiverSelectorMediaType; private defaultMediaType?: ReceiverSelectorMediaType;
private availableMediaTypes?: ReceiverSelectorMediaType; private availableMediaTypes?: ReceiverSelectorMediaType;
@@ -69,7 +69,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
} }
public async open( public async open(
receivers: ReceiverDevice[], receiverDevices: ReceiverDevice[],
defaultMediaType: ReceiverSelectorMediaType, defaultMediaType: ReceiverSelectorMediaType,
availableMediaTypes: ReceiverSelectorMediaType, availableMediaTypes: ReceiverSelectorMediaType,
appId?: string, appId?: string,
@@ -83,7 +83,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
await browser.windows.remove(this.windowId); await browser.windows.remove(this.windowId);
} }
this.receivers = receivers; this.receiverDevices = receiverDevices;
this.defaultMediaType = defaultMediaType; this.defaultMediaType = defaultMediaType;
this.availableMediaTypes = availableMediaTypes; this.availableMediaTypes = availableMediaTypes;
@@ -135,12 +135,12 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
} }
} }
public update(receivers: ReceiverDevice[]) { public update(receiverDevices: ReceiverDevice[]) {
this.receivers = receivers; this.receiverDevices = receiverDevices;
this.messagePort?.postMessage({ this.messagePort?.postMessage({
subject: "popup:update", subject: "popup:update",
data: { data: {
receivers: this.receivers receiverDevices: this.receiverDevices
} }
}); });
} }
@@ -176,7 +176,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
}); });
if ( if (
this.receivers === undefined || this.receiverDevices === undefined ||
this.defaultMediaType === undefined || this.defaultMediaType === undefined ||
this.availableMediaTypes === undefined this.availableMediaTypes === undefined
) { ) {
@@ -191,7 +191,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
this.messagePort.postMessage({ this.messagePort.postMessage({
subject: "popup:update", subject: "popup:update",
data: { data: {
receivers: this.receivers, receiverDevices: this.receiverDevices,
defaultMediaType: this.defaultMediaType, defaultMediaType: this.defaultMediaType,
availableMediaTypes: this.availableMediaTypes availableMediaTypes: this.availableMediaTypes
} }
@@ -250,7 +250,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
// Cleanup // Cleanup
this.windowId = undefined; this.windowId = undefined;
this.messagePort = undefined; this.messagePort = undefined;
this.receivers = undefined; this.receiverDevices = undefined;
this.defaultMediaType = undefined; this.defaultMediaType = undefined;
this.availableMediaTypes = undefined; this.availableMediaTypes = undefined;
this.wasReceiverSelected = false; this.wasReceiverSelected = false;

View File

@@ -174,7 +174,7 @@ async function getSelection(
logger.info("Selected receiver", ev.detail); logger.info("Selected receiver", ev.detail);
resolve({ resolve({
actionType: ReceiverSelectionActionType.Cast, actionType: ReceiverSelectionActionType.Cast,
receiver: ev.detail.receiver, receiverDevice: ev.detail.receiverDevice,
mediaType: ev.detail.mediaType, mediaType: ev.detail.mediaType,
filePath: ev.detail.filePath filePath: ev.detail.filePath
}); });
@@ -203,11 +203,11 @@ async function getSelection(
"stop", "stop",
storeListener("stop", async ev => { storeListener("stop", async ev => {
logger.info("Stopping receiver app...", ev.detail); logger.info("Stopping receiver app...", ev.detail);
receiverDevices.stopReceiverApp(ev.detail.receiver.id); receiverDevices.stopReceiverApp(ev.detail.receiverDevice.id);
resolve({ resolve({
actionType: ReceiverSelectionActionType.Stop, actionType: ReceiverSelectionActionType.Stop,
receiver: ev.detail.receiver receiverDevice: ev.detail.receiverDevice
}); });
removeListeners(); removeListeners();
}) })

View File

@@ -17,13 +17,13 @@ export enum ReceiverSelectionActionType {
export interface ReceiverSelectionCast { export interface ReceiverSelectionCast {
actionType: ReceiverSelectionActionType.Cast; actionType: ReceiverSelectionActionType.Cast;
receiver: ReceiverDevice; receiverDevice: ReceiverDevice;
mediaType: ReceiverSelectorMediaType; mediaType: ReceiverSelectorMediaType;
filePath?: string; filePath?: string;
} }
export interface ReceiverSelectionStop { export interface ReceiverSelectionStop {
actionType: ReceiverSelectionActionType.Stop; actionType: ReceiverSelectionActionType.Stop;
receiver: ReceiverDevice; receiverDevice: ReceiverDevice;
} }
export type ReceiverSelection = ReceiverSelectionCast | ReceiverSelectionStop; export type ReceiverSelection = ReceiverSelectionCast | ReceiverSelectionStop;

View File

@@ -2,7 +2,10 @@
import logger from "../../lib/logger"; import logger from "../../lib/logger";
import { ReceiverDevice } from "../../types"; import {
ReceiverDevice,
ReceiverDeviceCapabilities as ReceiverDeviceCapabilities
} from "../../types";
import { ErrorCallback, SuccessCallback } from "../types"; import { ErrorCallback, SuccessCallback } from "../types";
import { onMessage, sendMessageResponse } from "../eventMessageChannel"; import { onMessage, sendMessageResponse } from "../eventMessageChannel";
@@ -92,17 +95,40 @@ export const timeout = new Timeout();
// chrome.cast.media namespace // chrome.cast.media namespace
export * as media from "./media"; export * as media from "./media";
/**
* 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);
// Currently only supports CAST receivers
receiver.receiverType = ReceiverType.CAST;
return receiver;
}
function sendSessionRequest( function sendSessionRequest(
sessionRequest: SessionRequest, sessionRequest: SessionRequest,
receiverDevice: ReceiverDevice receiverDevice: ReceiverDevice
) { ) {
for (const listener of receiverActionListeners) { for (const listener of receiverActionListeners) {
const receiver = new Receiver( listener(createReceiver(receiverDevice), ReceiverAction.CAST);
receiverDevice.id,
receiverDevice.friendlyName
);
listener(receiver, ReceiverAction.CAST);
} }
sendMessageResponse({ sendMessageResponse({
@@ -258,13 +284,28 @@ onMessage(message => {
const status = message.data; const status = message.data;
// TODO: Implement persistent per-origin receiver IDs // TODO: Implement persistent per-origin receiver IDs
const receiver = new Receiver( const receiver1 = new Receiver(
status.receiverId, // label status.receiverId, // label
status.receiverFriendlyName, // friendlyName status.receiverFriendlyName, // friendlyName
[Capability.VIDEO_OUT, Capability.AUDIO_OUT], // capabilities [Capability.VIDEO_OUT, Capability.AUDIO_OUT], // capabilities
status.volume // volume status.volume // volume
); );
const receiverDevice = receiverDevices.get(status.receiverId);
if (!receiverDevice) {
logger.error(
`Could not find receiver device "${status.receiverFriendlyName}" (${status.receiverId})`
);
break;
}
const receiver = createReceiver(receiverDevice);
receiver.volume = status.volume;
receiver.displayStatus = new ReceiverDisplayStatus(
status.statusText,
status.appImages
);
const session = new Session( const session = new Session(
status.sessionId, // sessionId status.sessionId, // sessionId
status.appId, // appId status.appId, // appId
@@ -401,7 +442,7 @@ onMessage(message => {
logger.info("Selected receiver"); logger.info("Selected receiver");
if (sessionRequest) { if (sessionRequest) {
sendSessionRequest(sessionRequest, message.data.receiver); sendSessionRequest(sessionRequest, message.data.receiverDevice);
sessionRequest = null; sessionRequest = null;
} }
@@ -409,7 +450,7 @@ onMessage(message => {
} }
case "cast:selectReceiver/stopped": { case "cast:selectReceiver/stopped": {
const { receiver } = message.data; const { receiverDevice } = message.data;
logger.info("Stopped receiver"); logger.info("Stopped receiver");
@@ -417,12 +458,11 @@ onMessage(message => {
sessionRequest = null; sessionRequest = null;
for (const listener of receiverActionListeners) { for (const listener of receiverActionListeners) {
const castReceiver = new Receiver( listener(
receiver.id, // TODO: Use existing receiver object?
receiver.friendlyName createReceiver(receiverDevice),
ReceiverAction.STOP
); );
listener(castReceiver, ReceiverAction.STOP);
} }
} }
@@ -451,7 +491,10 @@ onMessage(message => {
break; break;
} }
sendSessionRequest(apiConfig.sessionRequest, message.data.receiver); sendSessionRequest(
apiConfig.sessionRequest,
message.data.receiverDevice
);
break; break;
} }

View File

@@ -42,7 +42,7 @@ export default async function loadSender(opts: LoadSenderOptions) {
instance.contentPort.postMessage({ instance.contentPort.postMessage({
subject: "cast:launchApp", subject: "cast:launchApp",
data: { receiver: opts.selection.receiver } data: { receiverDevice: opts.selection.receiverDevice }
}); });
break; break;
@@ -53,7 +53,7 @@ export default async function loadSender(opts: LoadSenderOptions) {
await browser.tabs.executeScript(opts.tabId, { await browser.tabs.executeScript(opts.tabId, {
code: stringify` code: stringify`
window.selectedMedia = ${opts.selection.mediaType}; window.selectedMedia = ${opts.selection.mediaType};
window.selectedReceiver = ${opts.selection.receiver}; window.selectedReceiver = ${opts.selection.receiverDevice};
`, `,
frameId: opts.frameId frameId: opts.frameId
}); });
@@ -72,7 +72,7 @@ export default async function loadSender(opts: LoadSenderOptions) {
init({ init({
mediaUrl: fileUrl.href, mediaUrl: fileUrl.href,
receiver: opts.selection.receiver receiver: opts.selection.receiverDevice
}); });
break; break;

View File

@@ -47,7 +47,7 @@ type ExtMessageDefinitions = {
}; };
}; };
"popup:update": { "popup:update": {
receivers: ReceiverDevice[]; receiverDevices: ReceiverDevice[];
defaultMediaType?: ReceiverSelectorMediaType; defaultMediaType?: ReceiverSelectorMediaType;
availableMediaTypes?: ReceiverSelectorMediaType; availableMediaTypes?: ReceiverSelectorMediaType;
}; };
@@ -68,7 +68,7 @@ type ExtMessageDefinitions = {
"cast:receiverDeviceUp": { receiverDevice: ReceiverDevice }; "cast:receiverDeviceUp": { receiverDevice: ReceiverDevice };
"cast:receiverDeviceDown": { receiverDeviceId: ReceiverDevice["id"] }; "cast:receiverDeviceDown": { receiverDeviceId: ReceiverDevice["id"] };
"cast:launchApp": { receiver: ReceiverDevice }; "cast:launchApp": { receiverDevice: ReceiverDevice };
}; };
/** /**

View File

@@ -2,10 +2,21 @@
import { ReceiverStatus } from "./cast/api/types"; import { ReceiverStatus } from "./cast/api/types";
export enum ReceiverDeviceCapabilities {
NONE = 0,
VIDEO_OUT = 1,
VIDEO_IN = 2,
AUDIO_OUT = 4,
AUDIO_IN = 8,
MULTIZONE_GROUP = 32
}
export interface ReceiverDevice { export interface ReceiverDevice {
host: string;
friendlyName: string;
id: string; id: string;
friendlyName: string;
modelName: string;
capabilities: ReceiverDeviceCapabilities;
host: string;
port: number; port: number;
status?: ReceiverStatus; status?: ReceiverStatus;
} }

View File

@@ -32,7 +32,7 @@ browser.runtime.getPlatformInfo().then(platformInfo => {
interface PopupAppProps {} interface PopupAppProps {}
interface PopupAppState { interface PopupAppState {
receivers: ReceiverDevice[]; receiverDevices: ReceiverDevice[];
mediaType: ReceiverSelectorMediaType; mediaType: ReceiverSelectorMediaType;
availableMediaTypes: ReceiverSelectorMediaType; availableMediaTypes: ReceiverSelectorMediaType;
isLoading: boolean; isLoading: boolean;
@@ -58,7 +58,7 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
super(props); super(props);
this.state = { this.state = {
receivers: [], receiverDevices: [],
mediaType: ReceiverSelectorMediaType.App, mediaType: ReceiverSelectorMediaType.App,
availableMediaTypes: ReceiverSelectorMediaType.App, availableMediaTypes: ReceiverSelectorMediaType.App,
isLoading: false, isLoading: false,
@@ -111,10 +111,13 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
} }
case "popup:update": { case "popup:update": {
const { receivers, availableMediaTypes, defaultMediaType } = const {
message.data; receiverDevices: receivers,
availableMediaTypes,
defaultMediaType
} = message.data;
this.setState({ receivers }); this.setState({ receiverDevices: receivers });
if ( if (
availableMediaTypes !== undefined && availableMediaTypes !== undefined &&
@@ -332,10 +335,11 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
</div> </div>
</div> </div>
<ul className="receivers"> <ul className="receivers">
{this.state.receivers && this.state.receivers.length ? ( {this.state.receiverDevices &&
this.state.receivers.map((receiver, i) => ( this.state.receiverDevices.length ? (
this.state.receiverDevices.map((receiver, i) => (
<ReceiverEntry <ReceiverEntry
receiver={receiver} receiverDevice={receiver}
onCast={this.onCast} onCast={this.onCast}
onStop={this.onStop} onStop={this.onStop}
isLoading={this.state.isLoading} isLoading={this.state.isLoading}
@@ -368,7 +372,7 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
} }
} }
private onCast(receiver: ReceiverDevice) { private onCast(receiverDevice: ReceiverDevice) {
this.setState({ this.setState({
isLoading: true isLoading: true
}); });
@@ -376,20 +380,20 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
this.port?.postMessage({ this.port?.postMessage({
subject: "receiverSelector:selected", subject: "receiverSelector:selected",
data: { data: {
receiverDevice,
actionType: ReceiverSelectionActionType.Cast, actionType: ReceiverSelectionActionType.Cast,
receiver,
mediaType: this.state.mediaType, mediaType: this.state.mediaType,
filePath: this.state.filePath filePath: this.state.filePath
} }
}); });
} }
private onStop(receiver: ReceiverDevice) { private onStop(receiverDevice: ReceiverDevice) {
this.port?.postMessage({ this.port?.postMessage({
subject: "receiverSelector:stop", subject: "receiverSelector:stop",
data: { data: {
actionType: ReceiverSelectionActionType.Stop, receiverDevice,
receiver actionType: ReceiverSelectionActionType.Stop
} }
}); });
} }
@@ -427,11 +431,11 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
} }
interface ReceiverEntryProps { interface ReceiverEntryProps {
receiver: ReceiverDevice; receiverDevice: ReceiverDevice;
isLoading: boolean; isLoading: boolean;
canCast: boolean; canCast: boolean;
onCast(receiver: ReceiverDevice): void; onCast(receiverDevice: ReceiverDevice): void;
onStop(receiver: ReceiverDevice): void; onStop(receiverDevice: ReceiverDevice): void;
} }
interface ReceiverEntryState { interface ReceiverEntryState {
@@ -472,18 +476,18 @@ class ReceiverEntry extends Component<ReceiverEntryProps, ReceiverEntryState> {
} }
public render() { public render() {
const { status } = this.props.receiver; const { status } = this.props.receiverDevice;
const application = status?.applications?.[0]; const application = status?.applications?.[0];
return ( return (
<li className="receiver"> <li className="receiver">
<div className="receiver__name"> <div className="receiver__name">
{this.props.receiver.friendlyName} {this.props.receiverDevice.friendlyName}
</div> </div>
<div className="receiver__address"> <div className="receiver__address">
{application && !application.isIdleScreen {application && !application.isIdleScreen
? application.statusText ? application.statusText
: `${this.props.receiver.host}:${this.props.receiver.port}`} : `${this.props.receiverDevice.host}:${this.props.receiverDevice.port}`}
</div> </div>
<button <button
className="button receiver__connect" className="button receiver__connect"
@@ -508,7 +512,7 @@ class ReceiverEntry extends Component<ReceiverEntryProps, ReceiverEntryState> {
} }
private handleCast() { private handleCast() {
const { status } = this.props.receiver; const { status } = this.props.receiverDevice;
if (!status) { if (!status) {
return; return;
} }
@@ -516,9 +520,9 @@ class ReceiverEntry extends Component<ReceiverEntryProps, ReceiverEntryState> {
const application = status.applications?.[0]; const application = status.applications?.[0];
if (this.state.showAlternateAction) { if (this.state.showAlternateAction) {
this.props.onStop(this.props.receiver); this.props.onStop(this.props.receiverDevice);
} else { } else {
this.props.onCast(this.props.receiver); this.props.onCast(this.props.receiverDevice);
this.setState({ this.setState({
isLoading: true isLoading: true