Add media controls (#229)

This commit is contained in:
Matt Hensman
2022-08-24 02:17:35 +01:00
committed by GitHub
parent cbc039a355
commit ac46802431
37 changed files with 1694 additions and 432 deletions

View File

@@ -0,0 +1,89 @@
"use strict";
import mdns from "mdns";
import { ReceiverDevice } from "../../messagingTypes";
/**
* Chromecast TXT record
*/
interface CastRecord {
// Device ID
id: string;
// Model name (e.g. Chromecast, Google Nest Mini, etc...)
md: string;
// Friendly name (user-visible)
fn: string;
// Capabilities
ca: string;
// Version (?)
ve: string;
// Icon path (?)
ic: string;
cd: string;
rm: string;
st: string;
bs: string;
nf: string;
rs: string;
}
interface DiscoveryOptions {
onDeviceFound(device: ReceiverDevice): void;
onDeviceDown(deviceId: string): void;
}
export default class Discovery {
browser = mdns.createBrowser(mdns.tcp("googlecast"), {
resolverSequence: [
mdns.rst.DNSServiceResolve(),
"DNSServiceGetAddrInfo" in mdns.dns_sd
? mdns.rst.DNSServiceGetAddrInfo()
: // Some issues on Linux with IPv6, so restrict to IPv4
mdns.rst.getaddrinfo({ families: [4] }),
mdns.rst.makeAddressesUnique()
]
});
constructor(opts: DiscoveryOptions) {
/**
* When a service is found, gather device info from service object and
* TXT record, then send a `main:receiverDeviceUp` message.
*/
this.browser.on("serviceUp", service => {
// Filter invalid results
if (!service.txtRecord || !service.name) return;
const record = service.txtRecord as CastRecord;
const device: ReceiverDevice = {
id: record.id,
friendlyName: record.fn,
modelName: record.md,
capabilities: parseInt(record.ca),
host: service.addresses[0],
port: service.port
};
opts.onDeviceFound(device);
});
/**
* When a service is lost, send a `main:receiverDeviceDown` message with
* the service name as the `deviceId`.
*/
this.browser.on("serviceDown", service => {
// Filter invalid results
if (!service.name) return;
opts.onDeviceDown(service.name);
});
}
start() {
this.browser.start();
}
stop() {
this.browser.stop();
}
}

View File

@@ -43,6 +43,10 @@ export default class Remote extends CastClient {
this.transportClient?.disconnect();
}
sendMediaMessage(message: SenderMediaMessage) {
this.transportClient?.sendMediaMessage(message);
}
/**
* Handle `NS_RECEIVER` messages from the receiver device.
* On initial connection, a `GET_STATUS` message is sent that
@@ -76,7 +80,8 @@ export default class Remote extends CastClient {
this.transportClient.connect(this.host).then(() => {
this.transportClient?.sendMediaMessage({
type: "GET_STATUS"
type: "GET_STATUS",
requestId: 0
});
});

View File

@@ -284,7 +284,7 @@ export interface MediaStatus {
playerState: PlayerState;
idleReason?: IdleReason;
items?: QueueItem[];
currentTime: number;
currentTime: Nullable<number>;
supportedMediaCommands: number;
repeatMode: RepeatMode;
volume: Volume;
@@ -357,11 +357,13 @@ export type SenderMediaMessage =
type: "MEDIA_GET_STATUS";
mediaSessionId?: number;
customData?: unknown;
requestId: number;
}
| {
type: "GET_STATUS";
mediaSessionId?: number;
customData?: unknown;
requestId: number;
}
| (MediaReqBase & { type: "STOP" })
| (MediaReqBase & { type: "MEDIA_SET_VOLUME"; volume: Volume })
@@ -369,24 +371,24 @@ export type SenderMediaMessage =
| (MediaReqBase & { type: "SET_PLAYBACK_RATE"; playbackRate: number })
| (ReqBase & {
type: "LOAD";
activeTrackIds: Nullable<number[]>;
activeTrackIds?: Nullable<number[]>;
atvCredentials?: string;
atvCredentialsType?: string;
autoplay: Nullable<boolean>;
currentTime: Nullable<number>;
autoplay?: Nullable<boolean>;
currentTime?: Nullable<number>;
customData?: unknown;
media: MediaInformation;
sessionId: Nullable<string>;
sessionId?: Nullable<string>;
})
| (MediaReqBase & {
type: "SEEK";
resumeState: Nullable<ResumeState>;
currentTime: Nullable<number>;
resumeState?: Nullable<ResumeState>;
currentTime?: Nullable<number>;
})
| (MediaReqBase & {
type: "EDIT_TRACKS_INFO";
activeTrackIds: Nullable<number[]>;
textTrackStyle: Nullable<string>;
activeTrackIds?: Nullable<number[]>;
textTrackStyle?: Nullable<string>;
})
// QueueLoadRequest
| (MediaReqBase & {
@@ -394,46 +396,46 @@ export type SenderMediaMessage =
items: QueueItem[];
startIndex: number;
repeatMode: string;
sessionId: Nullable<string>;
sessionId?: Nullable<string>;
})
// QueueInsertItemsRequest
| (MediaReqBase & {
type: "QUEUE_INSERT";
items: QueueItem[];
insertBefore: Nullable<number>;
sessionId: Nullable<string>;
insertBefore?: Nullable<number>;
sessionId?: Nullable<string>;
})
// QueueUpdateItemsRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
items: QueueItem[];
sessionId: Nullable<string>;
sessionId?: Nullable<string>;
})
// QueueJumpRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
jump: Nullable<number>;
currentItemId: Nullable<number>;
sessionId: Nullable<string>;
jump?: Nullable<number>;
currentItemId?: Nullable<number>;
sessionId?: Nullable<string>;
})
// QueueRemoveItemsRequest
| (MediaReqBase & {
type: "QUEUE_REMOVE";
itemIds: number[];
sessionId: Nullable<string>;
sessionId?: Nullable<string>;
})
// QueueReorderItemsRequest
| (MediaReqBase & {
type: "QUEUE_REORDER";
itemIds: number[];
insertBefore: Nullable<number>;
sessionId: Nullable<string>;
insertBefore?: Nullable<number>;
sessionId?: Nullable<string>;
})
// QueueSetPropertiesRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
repeatMode: Nullable<string>;
sessionId: Nullable<string>;
repeatMode?: Nullable<string>;
sessionId?: Nullable<string>;
});
export type ReceiverMediaMessage =