mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-11 18:19:58 +00:00
Improve session/media bridge messaging
This commit is contained in:
@@ -2,27 +2,16 @@
|
|||||||
|
|
||||||
import castv2 from "castv2";
|
import castv2 from "castv2";
|
||||||
|
|
||||||
import Session from "./Session";
|
import { ReceiverMediaMessage } from "./types";
|
||||||
|
|
||||||
import { Message } from "../../messaging";
|
import { Message } from "../../messaging";
|
||||||
import { sendMessage } from "../../lib/nativeMessaging";
|
import { sendMessage } from "../../lib/nativeMessaging";
|
||||||
|
|
||||||
|
import Session from "./Session";
|
||||||
|
|
||||||
|
|
||||||
const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
||||||
|
|
||||||
export interface UpdateMessageData {
|
|
||||||
_volumeLevel?: number;
|
|
||||||
_volumeMuted?: boolean;
|
|
||||||
_lastCurrentTime: number;
|
|
||||||
currentTime: number;
|
|
||||||
customData?: any;
|
|
||||||
playbackRate: number;
|
|
||||||
playerState: string;
|
|
||||||
repeatMode?: string;
|
|
||||||
media?: any;
|
|
||||||
mediaSessionId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default class Media {
|
export default class Media {
|
||||||
private channel: castv2.Channel;
|
private channel: castv2.Channel;
|
||||||
@@ -31,40 +20,32 @@ export default class Media {
|
|||||||
private referenceId: string
|
private referenceId: string
|
||||||
, private session: Session) {
|
, private session: Session) {
|
||||||
|
|
||||||
|
// Ensure channel exists
|
||||||
this.session.createChannel(NS_MEDIA);
|
this.session.createChannel(NS_MEDIA);
|
||||||
this.channel = this.session.channelMap.get(NS_MEDIA)!;
|
|
||||||
|
|
||||||
this.channel.on("message", (data: any) => {
|
const channel = this.session.channelMap.get(NS_MEDIA);
|
||||||
if (data && data.type === "MEDIA_STATUS"
|
if (!channel) {
|
||||||
&& data.status && data.status.length > 0) {
|
throw new Error("Media message cannel not found");
|
||||||
|
}
|
||||||
|
|
||||||
const status = data.status[0];
|
this.channel = channel;
|
||||||
|
this.channel.on("message", this.onMediaMessage);
|
||||||
|
}
|
||||||
|
|
||||||
const messageData: UpdateMessageData = {
|
private onMediaMessage = (message: ReceiverMediaMessage) => {
|
||||||
_lastCurrentTime: Date.now() / 1000
|
switch (message.type) {
|
||||||
|
case "MEDIA_STATUS": {
|
||||||
|
// TODO: Fix for multiple media statuses
|
||||||
|
const status = message.status[0];
|
||||||
|
|
||||||
, currentTime: status.currentTime
|
this.sendMessage({
|
||||||
, customData: status.customData
|
subject: "shim:media/updateStatus"
|
||||||
, playbackRate: status.playbackRate
|
, data: { status }
|
||||||
, playerState: status.playerState
|
});
|
||||||
, repeatMode: status.repeatMode
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status.volume) {
|
break;
|
||||||
messageData._volumeLevel = status.volume.level;
|
|
||||||
messageData._volumeMuted = status.volume.muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.media) {
|
|
||||||
messageData.media = status.media;
|
|
||||||
}
|
|
||||||
if (status.mediaSessionId) {
|
|
||||||
messageData.mediaSessionId = status.mediaSessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendMessage("shim:media/update", messageData);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public messageHandler(message: Message) {
|
public messageHandler(message: Message) {
|
||||||
@@ -77,9 +58,12 @@ export default class Media {
|
|||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendMessage("shim:media/sendMediaMessageResponse", {
|
this.sendMessage({
|
||||||
messageId: message.data.messageId
|
subject: "shim:media/sendMediaMessageResponse"
|
||||||
, error
|
, data: {
|
||||||
|
messageId: message.data.messageId
|
||||||
|
, error
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -87,11 +71,8 @@ export default class Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessage(subject: string, data: any) {
|
private sendMessage(message: Message) {
|
||||||
data._id = this.referenceId;
|
(message.data as any)._id = this.referenceId;
|
||||||
(sendMessage as any)({
|
sendMessage(message);
|
||||||
subject
|
|
||||||
, data
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,32 +5,39 @@ import { Channel, Client } from "castv2";
|
|||||||
import { Message } from "../../messaging";
|
import { Message } from "../../messaging";
|
||||||
import { sendMessage } from "../../lib/nativeMessaging";
|
import { sendMessage } from "../../lib/nativeMessaging";
|
||||||
|
|
||||||
|
import { ReceiverApplication, ReceiverMessage, SenderMessage } from "./types";
|
||||||
|
|
||||||
|
|
||||||
export const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
|
export const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
|
||||||
export const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
|
export const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
|
||||||
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
|
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
|
||||||
|
|
||||||
|
const HEARTBEAT_INTERVAL = 5000;
|
||||||
|
|
||||||
|
|
||||||
export default class Session {
|
export default class Session {
|
||||||
public channelMap = new Map<string, Channel>();
|
private isSessionCreated = false;
|
||||||
|
|
||||||
private client: Client;
|
private client: Client;
|
||||||
private clientConnection?: Channel;
|
private clientId = `client-${Math.floor(Math.random() * 10e5)}`;
|
||||||
private clientHeartbeat?: Channel;
|
|
||||||
private clientReceiver?: Channel;
|
|
||||||
private clientHeartbeatIntervalId?: NodeJS.Timeout;
|
|
||||||
|
|
||||||
private isSessionCreated = false;
|
|
||||||
|
|
||||||
private clientId?: string;
|
|
||||||
private transportId?: string;
|
private transportId?: string;
|
||||||
|
|
||||||
|
public channelMap = new Map<string, Channel>();
|
||||||
|
|
||||||
|
private platformConnection?: Channel;
|
||||||
|
private platformHeartbeat?: Channel;
|
||||||
|
private platformReceiver?: Channel;
|
||||||
|
private platformHeartbeatIntervalId?: NodeJS.Timeout;
|
||||||
|
|
||||||
private transportConnection?: Channel;
|
private transportConnection?: Channel;
|
||||||
private app: any;
|
private transportHeartbeat?: Channel;
|
||||||
|
|
||||||
|
private app?: ReceiverApplication;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public host: string
|
public host: string
|
||||||
, public port: number
|
, public port: number
|
||||||
, private appId: string
|
, private appId: string
|
||||||
, private sessionId: string
|
|
||||||
, private referenceId: string) {
|
, private referenceId: string) {
|
||||||
|
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
@@ -38,135 +45,18 @@ export default class Session {
|
|||||||
client.on("error", err => {
|
client.on("error", err => {
|
||||||
console.error(`castv2 error: ${err}`);
|
console.error(`castv2 error: ${err}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("close", () => {
|
client.on("close", () => {
|
||||||
// TODO: Don't send new data
|
// TODO: Don't send new data
|
||||||
|
if (this.platformHeartbeatIntervalId) {
|
||||||
|
clearInterval(this.platformHeartbeatIntervalId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.connect({ host, port }, this.onConnect.bind(this));
|
client.connect({ host, port }, this.onConnect.bind(this));
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onConnect() {
|
|
||||||
let transportHeartbeat: Channel;
|
|
||||||
|
|
||||||
const sourceId = "sender-0";
|
|
||||||
const destinationId = "receiver-0";
|
|
||||||
|
|
||||||
this.clientConnection = this.client.createChannel(
|
|
||||||
sourceId, destinationId, NS_CONNECTION, "JSON");
|
|
||||||
this.clientHeartbeat = this.client.createChannel(
|
|
||||||
sourceId, destinationId, NS_HEARTBEAT, "JSON");
|
|
||||||
this.clientReceiver = this.client.createChannel(
|
|
||||||
sourceId, destinationId, NS_RECEIVER, "JSON");
|
|
||||||
|
|
||||||
this.clientConnection.send({ type: "CONNECT" });
|
|
||||||
this.clientHeartbeat.send({ type: "PING" });
|
|
||||||
|
|
||||||
this.clientHeartbeatIntervalId = setInterval(() => {
|
|
||||||
if (transportHeartbeat) {
|
|
||||||
transportHeartbeat.send({ type: "PING" });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clientHeartbeat?.send({ type: "PING" });
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
this.clientReceiver.send({
|
|
||||||
type: "LAUNCH"
|
|
||||||
, appId: this.appId
|
|
||||||
, requestId: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
this.clientReceiver.on("message", (message: any) => {
|
|
||||||
if (message.type === "RECEIVER_STATUS") {
|
|
||||||
this.sendMessage("shim:session/updateStatus", message.status);
|
|
||||||
|
|
||||||
if (message.status.applications) {
|
|
||||||
const receiverApp = message.status.applications[0];
|
|
||||||
const receiverAppId = receiverApp.appId;
|
|
||||||
|
|
||||||
this.app = receiverApp;
|
|
||||||
|
|
||||||
if (receiverAppId !== this.appId) {
|
|
||||||
// Close session
|
|
||||||
this.sendMessage("shim:session/stopped");
|
|
||||||
this.client.close();
|
|
||||||
clearInterval(this.clientHeartbeatIntervalId!);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isSessionCreated) {
|
|
||||||
this.isSessionCreated = true;
|
|
||||||
|
|
||||||
this.transportId = this.app.transportId;
|
|
||||||
this.clientId =
|
|
||||||
`client-${Math.floor(Math.random() * 10e5)}`;
|
|
||||||
|
|
||||||
this.transportConnection = this.client.createChannel(
|
|
||||||
this.clientId, this.transportId!
|
|
||||||
, NS_CONNECTION, "JSON");
|
|
||||||
transportHeartbeat = this.client.createChannel(
|
|
||||||
this.clientId, this.transportId!
|
|
||||||
, NS_HEARTBEAT, "JSON");
|
|
||||||
|
|
||||||
this.transportConnection.send({ type: "CONNECT" });
|
|
||||||
|
|
||||||
this.sessionId = this.app.sessionId;
|
|
||||||
|
|
||||||
this.sendMessage("shim:session/connected", {
|
|
||||||
sessionId: this.app.sessionId
|
|
||||||
, namespaces: this.app.namespaces
|
|
||||||
, displayName: this.app.displayName
|
|
||||||
, statusText: this.app.displayName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public messageHandler(message: Message) {
|
|
||||||
switch (message.subject) {
|
|
||||||
case "bridge:session/close":
|
|
||||||
this.close();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "bridge:session/sendReceiverMessage": {
|
|
||||||
let wasError = false;
|
|
||||||
try {
|
|
||||||
this.clientReceiver?.send(message.data.message);
|
|
||||||
} catch (err) {
|
|
||||||
wasError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.data.message.type === "STOP") {
|
|
||||||
if (this.clientHeartbeatIntervalId) {
|
|
||||||
clearInterval(this.clientHeartbeatIntervalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendMessage("shim:session/sendReceiverMessageResponse", {
|
|
||||||
messageId: message.data.messageId
|
|
||||||
, wasError
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "bridge:session/impl_addMessageListener":
|
|
||||||
this._impl_addMessageListener(message.data.namespace);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "bridge:session/impl_sendMessage":
|
|
||||||
this._impl_sendMessage(
|
|
||||||
message.data.namespace
|
|
||||||
, message.data.message
|
|
||||||
, message.data.messageId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public createChannel(namespace: string) {
|
public createChannel(namespace: string) {
|
||||||
if (!this.channelMap.has(namespace)) {
|
if (!this.channelMap.has(namespace)) {
|
||||||
this.channelMap.set(namespace, this.client.createChannel(
|
this.channelMap.set(namespace, this.client.createChannel(
|
||||||
@@ -175,40 +65,152 @@ export default class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private establishSession(app: ReceiverApplication) {
|
||||||
|
this.transportId = app.transportId;
|
||||||
|
|
||||||
|
// Mesage channel to app
|
||||||
|
this.transportConnection = this.client.createChannel(
|
||||||
|
this.clientId, this.transportId, NS_CONNECTION, "JSON");
|
||||||
|
this.transportHeartbeat = this.client.createChannel(
|
||||||
|
this.clientId, this.transportId, NS_HEARTBEAT, "JSON");
|
||||||
|
|
||||||
|
this.transportConnection.send({
|
||||||
|
type: "CONNECT"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onConnect() {
|
||||||
|
const sourceId = "sender-0";
|
||||||
|
const destinationId = "receiver-0";
|
||||||
|
|
||||||
|
this.platformConnection = this.client.createChannel(
|
||||||
|
sourceId, destinationId, NS_CONNECTION, "JSON");
|
||||||
|
this.platformHeartbeat = this.client.createChannel(
|
||||||
|
sourceId, destinationId, NS_HEARTBEAT, "JSON");
|
||||||
|
this.platformReceiver = this.client.createChannel(
|
||||||
|
sourceId, destinationId, NS_RECEIVER, "JSON");
|
||||||
|
|
||||||
|
this.platformConnection.send({ type: "CONNECT" });
|
||||||
|
this.platformHeartbeat.send({ type: "PING" });
|
||||||
|
|
||||||
|
this.platformHeartbeatIntervalId = setInterval(() => {
|
||||||
|
this.platformHeartbeat?.send({ type: "PING" });
|
||||||
|
|
||||||
|
if (this.transportHeartbeat) {
|
||||||
|
this.transportHeartbeat.send({ type: "PING" });
|
||||||
|
}
|
||||||
|
}, HEARTBEAT_INTERVAL);
|
||||||
|
|
||||||
|
this.platformReceiver.send({
|
||||||
|
type: "LAUNCH"
|
||||||
|
, appId: this.appId
|
||||||
|
, requestId: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
this.platformReceiver.on("message", (message: ReceiverMessage) => {
|
||||||
|
switch (message.type) {
|
||||||
|
case "RECEIVER_STATUS": {
|
||||||
|
const { status } = message;
|
||||||
|
|
||||||
|
if (status.applications) {
|
||||||
|
// TODO: Fix for multiple applications?
|
||||||
|
const app = status.applications[0];
|
||||||
|
|
||||||
|
if (app.appId !== this.appId) {
|
||||||
|
this.sendMessage({
|
||||||
|
subject: "shim:session/stopped"
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isSessionCreated) {
|
||||||
|
this.isSessionCreated = true;
|
||||||
|
this.establishSession(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendMessage({
|
||||||
|
subject: "shim:session/updateStatus"
|
||||||
|
, data: { status: message.status }
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
console.error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public messageHandler(message: Message) {
|
||||||
|
switch (message.subject) {
|
||||||
|
case "bridge:session/close": {
|
||||||
|
this.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "bridge:session/impl_addMessageListener": {
|
||||||
|
this._impl_addMessageListener(message.data.namespace);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "bridge:session/impl_sendMessage": {
|
||||||
|
this._impl_sendMessage(
|
||||||
|
message.data.namespace
|
||||||
|
, message.data.message
|
||||||
|
, message.data.messageId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bridge:session/impl_sendReceiverMessage": {
|
||||||
|
const { message: receiverMessage
|
||||||
|
, messageId: receiverMessageId } = message.data;
|
||||||
|
|
||||||
|
this.impl_sendReceiverMessage(
|
||||||
|
receiverMessage, receiverMessageId);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public close() {
|
public close() {
|
||||||
this.clientConnection?.send({ type: "CLOSE" });
|
this.platformConnection?.send({ type: "CLOSE" });
|
||||||
this.transportConnection?.send({ type: "CLOSE" });
|
this.transportConnection?.send({ type: "CLOSE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
this.clientConnection?.send({ type: "STOP" });
|
this.platformConnection?.send({ type: "STOP" });
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessage(subject: string, data: any = {}) {
|
private sendMessage(message: Message) {
|
||||||
data._id = this.referenceId;
|
(message.data as any)._id = this.referenceId;
|
||||||
sendMessage({
|
sendMessage(message);
|
||||||
// @ts-ignore
|
|
||||||
subject
|
|
||||||
, data
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _impl_addMessageListener(namespace: string) {
|
private _impl_addMessageListener(namespace: string) {
|
||||||
|
// TODO: Limit to one listener per namespace
|
||||||
this.createChannel(namespace);
|
this.createChannel(namespace);
|
||||||
this.channelMap.get(namespace)?.on("message", (data: any) => {
|
this.channelMap.get(namespace)?.on("message", (message: any) => {
|
||||||
this.sendMessage("shim:session/impl_addMessageListener", {
|
this.sendMessage({
|
||||||
namespace
|
subject: "shim:session/impl_addMessageListener"
|
||||||
, data: JSON.stringify(data)
|
, data: {
|
||||||
|
namespace
|
||||||
|
, message: JSON.stringify(message)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _impl_sendMessage(
|
private _impl_sendMessage(
|
||||||
namespace: string
|
namespace: string
|
||||||
, message: {} | string
|
, message: object | string
|
||||||
, messageId: string) {
|
, messageId: string) {
|
||||||
|
|
||||||
let error = false;
|
let wasError = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Decode string messages
|
// Decode string messages
|
||||||
@@ -219,12 +221,34 @@ export default class Session {
|
|||||||
this.createChannel(namespace);
|
this.createChannel(namespace);
|
||||||
this.channelMap.get(namespace)?.send(message);
|
this.channelMap.get(namespace)?.send(message);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = true;
|
wasError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendMessage("shim:session/impl_sendMessage", {
|
this.sendMessage({
|
||||||
messageId
|
subject: "shim:session/impl_sendMessage"
|
||||||
, error
|
, data: { messageId, wasError }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private impl_sendReceiverMessage(
|
||||||
|
message: SenderMessage
|
||||||
|
, messageId: string) {
|
||||||
|
|
||||||
|
let wasError = false;
|
||||||
|
try {
|
||||||
|
this.platformReceiver?.send(message);
|
||||||
|
} catch (err) {
|
||||||
|
wasError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle stop message
|
||||||
|
if (message.type === "STOP") {
|
||||||
|
this.client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendMessage({
|
||||||
|
subject: "shim:session/impl_sendReceiverMessage"
|
||||||
|
, data: { messageId, wasError }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export function handleSessionMessage(message: any) {
|
|||||||
message.data.address
|
message.data.address
|
||||||
, message.data.port
|
, message.data.port
|
||||||
, message.data.appId
|
, message.data.appId
|
||||||
, message.data.sessionId
|
|
||||||
, sessionId));
|
, sessionId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
396
app/src/bridge/components/chromecast/types.ts
Normal file
396
app/src/bridge/components/chromecast/types.ts
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
url: string;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VolumeControlType {
|
||||||
|
ATTENUATION = "attenuation"
|
||||||
|
, FIXED = "fixed"
|
||||||
|
, MASTER = "master"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Volume {
|
||||||
|
controlType?: VolumeControlType;
|
||||||
|
stepInterval?: number;
|
||||||
|
level: Nullable<number>;
|
||||||
|
muted: Nullable<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media
|
||||||
|
|
||||||
|
enum IdleReason {
|
||||||
|
CANCELLED = "CANCELLED"
|
||||||
|
, INTERRUPTED = "INTERRUPTED"
|
||||||
|
, FINISHED = "FINISHED"
|
||||||
|
, ERROR = "ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HlsSegmentFormat {
|
||||||
|
AAC = "aac"
|
||||||
|
, AC3 = "ac3"
|
||||||
|
, MP3 = "mp3"
|
||||||
|
, TS = "ts"
|
||||||
|
, TS_AAC = "ts_aac"
|
||||||
|
, E_AC3 = "e_ac3"
|
||||||
|
, FMP4 = "fmp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HlsVideoSegmentFormat {
|
||||||
|
MPEG2_TS = "mpeg2_ts"
|
||||||
|
, FMP4 = "fmp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MetadataType {
|
||||||
|
GENERIC
|
||||||
|
, MOVIE
|
||||||
|
, TV_SHOW
|
||||||
|
, MUSIC_TRACK
|
||||||
|
, PHOTO
|
||||||
|
, AUDIOBOOK_CHAPTER
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlayerState {
|
||||||
|
IDLE = "IDLE"
|
||||||
|
, PLAYING = "PLAYING"
|
||||||
|
, PAUSED = "PAUSED"
|
||||||
|
, BUFFERING = "BUFFERING"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RepeatMode {
|
||||||
|
OFF = "REPEAT_OFF"
|
||||||
|
, ALL = "REPEAT_ALL"
|
||||||
|
, SINGLE = "REPEAT_SINGLE"
|
||||||
|
, ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ResumeState {
|
||||||
|
PLAYBACK_START = "PLAYBACK_START"
|
||||||
|
, PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StreamType {
|
||||||
|
BUFFERED = "BUFFERED"
|
||||||
|
, LIVE = "LIVE"
|
||||||
|
, OTHER = "OTHER"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TrackType {
|
||||||
|
TEXT = "TEXT"
|
||||||
|
, AUDIO = "AUDIO"
|
||||||
|
, VIDEO = "VIDEO"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserAction {
|
||||||
|
LIKE = "LIKE"
|
||||||
|
, DISLIKE = "DISLIKE"
|
||||||
|
, FOLLOW = "FOLLOW"
|
||||||
|
, UNFOLLOW = "UNFOLLOW"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface Break {
|
||||||
|
breakClipIds: string[];
|
||||||
|
duration?: number;
|
||||||
|
id: string;
|
||||||
|
isEmbedded?: boolean;
|
||||||
|
isWatched: boolean;
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreakClip {
|
||||||
|
clickThroughUrl?: string;
|
||||||
|
contentId?: string;
|
||||||
|
contentType?: string;
|
||||||
|
contentUrl?: string;
|
||||||
|
customData?: {};
|
||||||
|
duration?: number;
|
||||||
|
id: string;
|
||||||
|
hlsSegmentFormat?: HlsSegmentFormat;
|
||||||
|
posterUrl?: string;
|
||||||
|
title?: string;
|
||||||
|
vastAdsRequest?: VastAdsRequest;
|
||||||
|
whenSkippable?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextTrackStyle {
|
||||||
|
backgroundColor: Nullable<string>;
|
||||||
|
customData: any;
|
||||||
|
edgeColor: Nullable<string>;
|
||||||
|
edgeType: Nullable<string>;
|
||||||
|
fontFamily: Nullable<string>;
|
||||||
|
fontGenericFamily: Nullable<string>;
|
||||||
|
fontScale: Nullable<number>;
|
||||||
|
fontStyle: Nullable<string>;
|
||||||
|
foregroundColor: Nullable<string>;
|
||||||
|
windowColor: Nullable<string>;
|
||||||
|
windowRoundedCornerRadius: Nullable<number>;
|
||||||
|
windowType: Nullable<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Track {
|
||||||
|
customData: any;
|
||||||
|
language: Nullable<string>;
|
||||||
|
name: Nullable<string>;
|
||||||
|
subtype: Nullable<string>;
|
||||||
|
trackContentId: Nullable<string>;
|
||||||
|
trackContentType: Nullable<string>;
|
||||||
|
trackId: string;
|
||||||
|
type: TrackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserActionState {
|
||||||
|
customData: any;
|
||||||
|
userAction: UserAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VastAdsRequest {
|
||||||
|
adsResponse?: string;
|
||||||
|
adTagUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata =
|
||||||
|
GenericMediaMetadata
|
||||||
|
| MovieMediaMetadata
|
||||||
|
| MusicTrackMediaMetadata
|
||||||
|
| PhotoMediaMetadata
|
||||||
|
| TvShowMediaMetadata;
|
||||||
|
|
||||||
|
interface MediaInformation {
|
||||||
|
atvEntity?: string;
|
||||||
|
breakClips?: BreakClip[];
|
||||||
|
breaks?: Break[];
|
||||||
|
contentId: string;
|
||||||
|
contentType: string;
|
||||||
|
contentUrl?: string;
|
||||||
|
customData: any;
|
||||||
|
duration: Nullable<number>;
|
||||||
|
entity?: string;
|
||||||
|
hlsSegmentFormat?: HlsSegmentFormat;
|
||||||
|
hlsVideoSegmentFormat?: HlsVideoSegmentFormat;
|
||||||
|
metadata: Nullable<Metadata>;
|
||||||
|
startAbsoluteTime?: number;
|
||||||
|
streamType: StreamType;
|
||||||
|
textTrackStyle: Nullable<TextTrackStyle>;
|
||||||
|
tracks: Nullable<Track[]>;
|
||||||
|
userActionStates?: UserActionState[];
|
||||||
|
vmapAdsRequest?: VastAdsRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenericMediaMetadata {
|
||||||
|
images?: Image[];
|
||||||
|
metadataType: number;
|
||||||
|
releaseDate?: string;
|
||||||
|
releaseYear?: number;
|
||||||
|
subtitle?: string;
|
||||||
|
title?: string;
|
||||||
|
type: MetadataType.GENERIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieMediaMetadata {
|
||||||
|
images?: Image[];
|
||||||
|
metadataType: number;
|
||||||
|
releaseDate?: string;
|
||||||
|
releaseYear?: number;
|
||||||
|
studio?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
title?: string;
|
||||||
|
type: MetadataType.MOVIE;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TvShowMediaMetadata {
|
||||||
|
episode?: number;
|
||||||
|
episodeNumber?: number;
|
||||||
|
episodeTitle?: string;
|
||||||
|
images?: Image[];
|
||||||
|
metadataType: number;
|
||||||
|
originalAirdate?: string;
|
||||||
|
releaseYear?: number;
|
||||||
|
season?: number;
|
||||||
|
seasonNumber?: number;
|
||||||
|
seriesTitle?: string;
|
||||||
|
title?: string;
|
||||||
|
type: MetadataType.TV_SHOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MusicTrackMediaMetadata {
|
||||||
|
albumArtist?: string;
|
||||||
|
albumName?: string;
|
||||||
|
artist?: string;
|
||||||
|
artistName?: string;
|
||||||
|
composer?: string;
|
||||||
|
discNumber?: number;
|
||||||
|
images?: Image[];
|
||||||
|
metadataType: number;
|
||||||
|
releaseDate?: string;
|
||||||
|
releaseYear?: number;
|
||||||
|
songName?: string;
|
||||||
|
title?: string;
|
||||||
|
trackNumber?: number;
|
||||||
|
type: MetadataType.MUSIC_TRACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PhotoMediaMetadata {
|
||||||
|
artist?: string;
|
||||||
|
creationDateTime?: string;
|
||||||
|
height?: number;
|
||||||
|
images?: Image[];
|
||||||
|
latitude?: number;
|
||||||
|
location?: string;
|
||||||
|
longitude?: number;
|
||||||
|
metadataType: number;
|
||||||
|
title?: string;
|
||||||
|
type: MetadataType.PHOTO;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueItem {
|
||||||
|
activeTrackIds: Nullable<number[]>;
|
||||||
|
autoplay: boolean;
|
||||||
|
customData: any;
|
||||||
|
itemId: Nullable<number>;
|
||||||
|
media: MediaInformation;
|
||||||
|
playbackDuration: Nullable<number>;
|
||||||
|
preloadTime: number;
|
||||||
|
startTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface MediaStatus {
|
||||||
|
mediaSessionId: number;
|
||||||
|
media?: MediaInformation;
|
||||||
|
playbackRate: number;
|
||||||
|
playerState: PlayerState;
|
||||||
|
idleReason?: IdleReason;
|
||||||
|
currentTime: number;
|
||||||
|
supportedMediaCommands: number;
|
||||||
|
repeatMode: RepeatMode;
|
||||||
|
volume: Volume
|
||||||
|
customData: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceiverApplication {
|
||||||
|
appId: string
|
||||||
|
, appType: string
|
||||||
|
, displayName: string
|
||||||
|
, iconUrl: string
|
||||||
|
, isIdleScreen: boolean
|
||||||
|
, launchedFromCloud: boolean
|
||||||
|
, namespaces: Array<{ name: string }>
|
||||||
|
, sessionId: string
|
||||||
|
, statusText: string
|
||||||
|
, transportId: string
|
||||||
|
, universalAppId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceiverStatus {
|
||||||
|
applications?: ReceiverApplication[]
|
||||||
|
, isActiveInput?: boolean
|
||||||
|
, isStandBy?: boolean
|
||||||
|
, volume: Volume
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ReqBase {
|
||||||
|
requestId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NS: urn:x-cast:com.google.cast.receiver
|
||||||
|
export type SenderMessage =
|
||||||
|
ReqBase & { type: "LAUNCH", appId: string }
|
||||||
|
| ReqBase & { type: "STOP", sessionId: string }
|
||||||
|
| ReqBase & { type: "GET_STATUS" }
|
||||||
|
| ReqBase & { type: "GET_APP_AVAILABILITY", appId: string[] }
|
||||||
|
| ReqBase & { type: "SET_VOLUME", volume: Volume };
|
||||||
|
|
||||||
|
export type ReceiverMessage =
|
||||||
|
ReqBase & {
|
||||||
|
type: "RECEIVER_STATUS"
|
||||||
|
, status: ReceiverStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface MediaReqBase extends ReqBase {
|
||||||
|
customData?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NS: urn:x-cast:com.google.cast.media
|
||||||
|
export type SenderMediaMessage =
|
||||||
|
| MediaReqBase & { type: "PLAY" }
|
||||||
|
| MediaReqBase & { type: "PAUSE" }
|
||||||
|
| MediaReqBase & { type: "MEDIA_GET_STATUS" }
|
||||||
|
| MediaReqBase & { type: "STOP" }
|
||||||
|
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Volume }
|
||||||
|
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "LOAD"
|
||||||
|
, media: MediaInformation
|
||||||
|
, autoplay: Nullable<boolean>
|
||||||
|
, currentTime: Nullable<number>
|
||||||
|
}
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "SEEK"
|
||||||
|
, resumeState: Nullable<ResumeState>
|
||||||
|
, currentTime: Nullable<number>
|
||||||
|
}
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "EDIT_TRACKS_INFO"
|
||||||
|
, activeTrackIds: Nullable<number[]>
|
||||||
|
, textTrackStyle: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueLoadRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_LOAD"
|
||||||
|
, items: QueueItem[]
|
||||||
|
, startIndex: number
|
||||||
|
, repeatMode: string
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueInsertItemsRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_INSERT"
|
||||||
|
, items: QueueItem[]
|
||||||
|
, insertBefore: Nullable<number>
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueUpdateItemsRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_UPDATE"
|
||||||
|
, items: QueueItem[]
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueJumpRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_UPDATE"
|
||||||
|
, jump: Nullable<number>
|
||||||
|
, currentItemId: Nullable<number>
|
||||||
|
, sessionId: Nullable<number>
|
||||||
|
}
|
||||||
|
// QueueRemoveItemsRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_REMOVE"
|
||||||
|
, itemIds: number[]
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueReorderItemsRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_REORDER"
|
||||||
|
, itemIds: number[]
|
||||||
|
, insertBefore: Nullable<number>
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueSetPropertiesRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_UPDATE"
|
||||||
|
, repeatMode: Nullable<string>
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReceiverMediaMessage =
|
||||||
|
MediaReqBase & { type: "MEDIA_STATUS", status: MediaStatus[] }
|
||||||
|
| MediaReqBase & { type: "INVALID_PLAYER_STATE" }
|
||||||
|
| MediaReqBase & { type: "LOAD_FAILED" }
|
||||||
|
| MediaReqBase & { type: "LOAD_CANCELLED" }
|
||||||
|
| MediaReqBase & { type: "INVALID_REQUEST" };
|
||||||
@@ -3,13 +3,12 @@
|
|||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
import { Channel, Client } from "castv2";
|
import { Channel, Client } from "castv2";
|
||||||
|
|
||||||
import mdns from "mdns";
|
import mdns from "mdns";
|
||||||
|
|
||||||
import { sendMessage } from "../lib/nativeMessaging";
|
import { sendMessage } from "../lib/nativeMessaging";
|
||||||
|
|
||||||
import { ReceiverStatus } from "../types";
|
import { ReceiverStatus } from "./chromecast/types";
|
||||||
import { Message } from "../messaging";
|
|
||||||
|
|
||||||
import { NS_CONNECTION
|
import { NS_CONNECTION
|
||||||
, NS_HEARTBEAT
|
, NS_HEARTBEAT
|
||||||
, NS_RECEIVER } from "./chromecast/Session";
|
, NS_RECEIVER } from "./chromecast/Session";
|
||||||
|
|||||||
@@ -1,34 +1,29 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
import { MediaStatus, ReceiverStatus, ReceiverApplication, SenderMessage }
|
||||||
|
from "./components/chromecast/types";
|
||||||
|
|
||||||
import { ReceiverDevice
|
import { ReceiverDevice
|
||||||
, ReceiverMessage
|
|
||||||
, ReceiverSelectionCast
|
, ReceiverSelectionCast
|
||||||
, ReceiverSelectionStop
|
, ReceiverSelectionStop } from "./types";
|
||||||
, ReceiverStatus
|
|
||||||
, Volume } from "./types";
|
|
||||||
|
|
||||||
|
|
||||||
type MessageDefinitions = {
|
type MessageDefinitions = {
|
||||||
// Session messages
|
// Session messages
|
||||||
"shim:session/stopped": {}
|
"shim:session/connected": { application: ReceiverApplication }
|
||||||
, "shim:session/connected": {
|
, "shim:session/updateStatus": { status: ReceiverStatus }
|
||||||
sessionId: string
|
, "shim:session/stopped": {}
|
||||||
, namespaces: Array<{ name: string }>
|
|
||||||
, displayName: string
|
|
||||||
, statusText: string
|
|
||||||
}
|
|
||||||
, "shim:session/updateStatus": { volume: Volume }
|
|
||||||
, "shim:session/sendReceiverMessageResponse": {
|
|
||||||
messageId: string
|
|
||||||
, wasError: boolean
|
|
||||||
}
|
|
||||||
, "shim:session/impl_addMessageListener": {
|
, "shim:session/impl_addMessageListener": {
|
||||||
namespace: string
|
namespace: string
|
||||||
, data: string
|
, message: string
|
||||||
}
|
}
|
||||||
, "shim:session/impl_sendMessage": {
|
, "shim:session/impl_sendMessage": {
|
||||||
messageId: string
|
messageId: string
|
||||||
, error: boolean
|
, wasError: boolean
|
||||||
|
}
|
||||||
|
, "shim:session/impl_sendReceiverMessage": {
|
||||||
|
messageId: string
|
||||||
|
, wasError: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bridge session messages
|
// Bridge session messages
|
||||||
@@ -40,11 +35,6 @@ type MessageDefinitions = {
|
|||||||
, _id: string
|
, _id: string
|
||||||
}
|
}
|
||||||
, "bridge:session/close": {}
|
, "bridge:session/close": {}
|
||||||
, "bridge:session/sendReceiverMessage": {
|
|
||||||
message: ReceiverMessage
|
|
||||||
, messageId: string
|
|
||||||
, _id: string
|
|
||||||
}
|
|
||||||
, "bridge:session/impl_leave": {
|
, "bridge:session/impl_leave": {
|
||||||
id: string
|
id: string
|
||||||
, _id: string
|
, _id: string
|
||||||
@@ -55,23 +45,19 @@ type MessageDefinitions = {
|
|||||||
, messageId: string
|
, messageId: string
|
||||||
, _id: string
|
, _id: string
|
||||||
}
|
}
|
||||||
|
, "bridge:session/impl_sendReceiverMessage": {
|
||||||
|
message: SenderMessage
|
||||||
|
, messageId: string
|
||||||
|
, _id: string
|
||||||
|
}
|
||||||
, "bridge:session/impl_addMessageListener": {
|
, "bridge:session/impl_addMessageListener": {
|
||||||
namespace: string;
|
namespace: string;
|
||||||
_id: string;
|
_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media messages
|
// Media messages
|
||||||
, "shim:media/update": {
|
, "shim:media/updateStatus": {
|
||||||
currentTime: number
|
status: MediaStatus
|
||||||
, _lastCurrentTime: number
|
|
||||||
, customData: any
|
|
||||||
, playbackRate: number
|
|
||||||
, playerState: string
|
|
||||||
, repeatMode: string
|
|
||||||
, _volumeLevel: number
|
|
||||||
, _volumeMuted: boolean
|
|
||||||
, media: unknown // MediaInfo
|
|
||||||
, mediaSessionId: number
|
|
||||||
}
|
}
|
||||||
, "shim:media/sendMediaMessageResponse": {
|
, "shim:media/sendMediaMessageResponse": {
|
||||||
messageId: string
|
messageId: string
|
||||||
|
|||||||
@@ -1,60 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
export interface ReceiverStatus {
|
import { ReceiverStatus } from "./components/chromecast/types";
|
||||||
applications?: Array<{
|
|
||||||
appId: string
|
|
||||||
, appType: string
|
|
||||||
, displayName: string
|
|
||||||
, iconUrl: string
|
|
||||||
, isIdleScreen: boolean
|
|
||||||
, launchedFromCloud: boolean
|
|
||||||
, namespaces: Array<{ name: string }>
|
|
||||||
, sessionId: string
|
|
||||||
, statusText: string
|
|
||||||
, transportId: string
|
|
||||||
, universalAppId: string
|
|
||||||
}>
|
|
||||||
, isActiveInput?: boolean
|
|
||||||
, isStandBy?: boolean
|
|
||||||
, userEq: unknown
|
|
||||||
, volume: Volume
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface MediaStatus {
|
|
||||||
mediaSessionId: number;
|
|
||||||
supportedMediaCommands: number;
|
|
||||||
currentTime: number;
|
|
||||||
media: {
|
|
||||||
duration: number;
|
|
||||||
contentId: string;
|
|
||||||
streamType: string;
|
|
||||||
contentType: string;
|
|
||||||
};
|
|
||||||
playbackRate: number;
|
|
||||||
volume: {
|
|
||||||
muted: boolean;
|
|
||||||
level: number;
|
|
||||||
};
|
|
||||||
currentItemId: number;
|
|
||||||
idleReason: string;
|
|
||||||
playerState: string;
|
|
||||||
extendedStatus: {
|
|
||||||
playerState: string;
|
|
||||||
media: {
|
|
||||||
contentId: string;
|
|
||||||
streamType: string;
|
|
||||||
contentType: string;
|
|
||||||
metadata: {
|
|
||||||
images: Array<{ url: string }>;
|
|
||||||
metadataType: number;
|
|
||||||
artist: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ReceiverSelectorMediaType {
|
export enum ReceiverSelectorMediaType {
|
||||||
App = 1
|
App = 1
|
||||||
, Tab = 2
|
, Tab = 2
|
||||||
@@ -86,32 +34,3 @@ export interface ReceiverDevice {
|
|||||||
port: number;
|
port: number;
|
||||||
status?: ReceiverStatus;
|
status?: ReceiverStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export enum VolumeControlType {
|
|
||||||
ATTENUATION = "attenuation"
|
|
||||||
, FIXED = "fixed"
|
|
||||||
, MASTER = "master"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class Volume {
|
|
||||||
public controlType?: VolumeControlType;
|
|
||||||
public stepInterval?: number;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public level: (number | null) = null
|
|
||||||
, public muted: (boolean | null) = null) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type ReceiverMessage =
|
|
||||||
{ type: "LAUNCH", appId: string }
|
|
||||||
| { type: "STOP", sessionId: string }
|
|
||||||
| { type: "GET_STATUS" }
|
|
||||||
| { type: "GET_APP_AVAILABILITY", appId: string[] }
|
|
||||||
| {
|
|
||||||
type: "SET_VOLUME"
|
|
||||||
, volume: { level: number }
|
|
||||||
| { muted: boolean }
|
|
||||||
};
|
|
||||||
|
|||||||
1
app/src/global.d.ts
vendored
Normal file
1
app/src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare type Nullable<T> = T | null;
|
||||||
@@ -4,8 +4,9 @@ import bridge from "../lib/bridge";
|
|||||||
import logger from "../lib/logger";
|
import logger from "../lib/logger";
|
||||||
import { TypedEventTarget } from "../lib/TypedEventTarget";
|
import { TypedEventTarget } from "../lib/TypedEventTarget";
|
||||||
|
|
||||||
import messaging, { Message, Port } from "../messaging";
|
import { Message, Port } from "../messaging";
|
||||||
import { ReceiverDevice, ReceiverStatus } from "../types";
|
import { ReceiverDevice } from "../types";
|
||||||
|
import { ReceiverStatus } from "../shim/cast/types";
|
||||||
|
|
||||||
|
|
||||||
interface EventMap {
|
interface EventMap {
|
||||||
@@ -124,7 +125,6 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
if (receiverDevice.status) {
|
if (receiverDevice.status) {
|
||||||
receiverDevice.status.isActiveInput = status.isActiveInput;
|
receiverDevice.status.isActiveInput = status.isActiveInput;
|
||||||
receiverDevice.status.isStandBy = status.isStandBy;
|
receiverDevice.status.isStandBy = status.isStandBy;
|
||||||
receiverDevice.status.userEq = status.userEq;
|
|
||||||
receiverDevice.status.volume = status.volume;
|
receiverDevice.status.volume = status.volume;
|
||||||
|
|
||||||
if (status.applications) {
|
if (status.applications) {
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import Messenger from "./lib/Messenger";
|
|||||||
import { TypedPort } from "./lib/TypedPort";
|
import { TypedPort } from "./lib/TypedPort";
|
||||||
import { BridgeInfo } from "./lib/bridge";
|
import { BridgeInfo } from "./lib/bridge";
|
||||||
|
|
||||||
import { ReceiverDevice
|
import { ReceiverDevice } from "./types";
|
||||||
, SessionReceiverMessage
|
|
||||||
, ReceiverStatus } from "./types";
|
|
||||||
|
|
||||||
import { ReceiverSelectorMediaType } from "./background/receiverSelector";
|
import { ReceiverSelectorMediaType } from "./background/receiverSelector";
|
||||||
import { ReceiverSelection
|
import { ReceiverSelection
|
||||||
@@ -16,7 +14,10 @@ import { ReceiverSelection
|
|||||||
from "./background/receiverSelector/ReceiverSelector";
|
from "./background/receiverSelector/ReceiverSelector";
|
||||||
|
|
||||||
import { Volume } from "./shim/cast/dataClasses";
|
import { Volume } from "./shim/cast/dataClasses";
|
||||||
import { MediaInfo } from "./shim/cast/media";
|
import { MediaStatus
|
||||||
|
, SenderMessage
|
||||||
|
, ReceiverApplication
|
||||||
|
, ReceiverStatus } from "./shim/cast/types";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,24 +74,19 @@ type ExtMessageDefinitions = {
|
|||||||
type AppMessageDefinitions = {
|
type AppMessageDefinitions = {
|
||||||
// Session messages
|
// Session messages
|
||||||
"shim:session/stopped": {}
|
"shim:session/stopped": {}
|
||||||
, "shim:session/connected": {
|
, "shim:session/connected": { application: ReceiverApplication }
|
||||||
sessionId: string
|
, "shim:session/updateStatus": { status: ReceiverStatus }
|
||||||
, namespaces: Array<{ name: string }>
|
, "shim:session/impl_addMessageListener": {
|
||||||
, displayName: string
|
namespace: string
|
||||||
, statusText: string
|
, message: string
|
||||||
}
|
}
|
||||||
, "shim:session/updateStatus": { volume: Volume }
|
, "shim:session/impl_sendMessage": {
|
||||||
, "shim:session/sendReceiverMessageResponse": {
|
|
||||||
messageId: string
|
messageId: string
|
||||||
, wasError: boolean
|
, wasError: boolean
|
||||||
}
|
}
|
||||||
, "shim:session/impl_addMessageListener": {
|
, "shim:session/impl_sendReceiverMessage": {
|
||||||
namespace: string
|
|
||||||
, data: string
|
|
||||||
}
|
|
||||||
, "shim:session/impl_sendMessage": {
|
|
||||||
messageId: string
|
messageId: string
|
||||||
, error: boolean
|
, wasError: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bridge session messages
|
// Bridge session messages
|
||||||
@@ -102,11 +98,6 @@ type AppMessageDefinitions = {
|
|||||||
, _id: string
|
, _id: string
|
||||||
}
|
}
|
||||||
, "bridge:session/close": {}
|
, "bridge:session/close": {}
|
||||||
, "bridge:session/sendReceiverMessage": {
|
|
||||||
message: SessionReceiverMessage
|
|
||||||
, messageId: string
|
|
||||||
, _id: string
|
|
||||||
}
|
|
||||||
, "bridge:session/impl_leave": {
|
, "bridge:session/impl_leave": {
|
||||||
id: string
|
id: string
|
||||||
, _id: string
|
, _id: string
|
||||||
@@ -117,23 +108,19 @@ type AppMessageDefinitions = {
|
|||||||
, messageId: string
|
, messageId: string
|
||||||
, _id: string
|
, _id: string
|
||||||
}
|
}
|
||||||
|
, "bridge:session/impl_sendReceiverMessage": {
|
||||||
|
message: SenderMessage
|
||||||
|
, messageId: string
|
||||||
|
, _id: string
|
||||||
|
}
|
||||||
, "bridge:session/impl_addMessageListener": {
|
, "bridge:session/impl_addMessageListener": {
|
||||||
namespace: string;
|
namespace: string;
|
||||||
_id: string;
|
_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media messages
|
// Media messages
|
||||||
, "shim:media/update": {
|
, "shim:media/updateStatus": {
|
||||||
currentTime: number
|
status: MediaStatus
|
||||||
, _lastCurrentTime: number
|
|
||||||
, customData: any
|
|
||||||
, playbackRate: number
|
|
||||||
, playerState: string
|
|
||||||
, repeatMode: string
|
|
||||||
, _volumeLevel: number
|
|
||||||
, _volumeMuted: boolean
|
|
||||||
, media: MediaInfo
|
|
||||||
, mediaSessionId: number
|
|
||||||
}
|
}
|
||||||
, "shim:media/sendMediaMessageResponse": {
|
, "shim:media/sendMediaMessageResponse": {
|
||||||
messageId: string
|
messageId: string
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
import logger from "../../lib/logger";
|
import logger from "../../lib/logger";
|
||||||
import { SessionReceiverMessage } from "../../types";
|
|
||||||
|
|
||||||
import { onMessage
|
import { onMessage
|
||||||
, sendMessageResponse } from "../eventMessageChannel";
|
, sendMessageResponse } from "../eventMessageChannel";
|
||||||
|
|
||||||
import { Callbacks
|
import { ErrorCallback
|
||||||
, ErrorCallback
|
|
||||||
, LoadSuccessCallback
|
, LoadSuccessCallback
|
||||||
, MediaListener
|
, MediaListener
|
||||||
, MessageListener
|
, MessageListener
|
||||||
, SuccessCallback
|
, SuccessCallback
|
||||||
, UpdateListener } from "../types";
|
, UpdateListener } from "../types";
|
||||||
|
|
||||||
|
import { SenderMediaMessage, SenderMessage } from "./types";
|
||||||
|
|
||||||
import { Error as _Error
|
import { Error as _Error
|
||||||
, Image, Receiver
|
, Image, Receiver
|
||||||
, SenderApplication, Volume } from "./dataClasses";
|
, SenderApplication, Volume } from "./dataClasses";
|
||||||
@@ -23,23 +23,29 @@ import { ErrorCode, SessionStatus } from "./enums";
|
|||||||
|
|
||||||
import { Media
|
import { Media
|
||||||
, LoadRequest
|
, LoadRequest
|
||||||
, QueueLoadRequest
|
, QueueLoadRequest } from "./media";
|
||||||
// Enums
|
|
||||||
, RepeatMode } from "./media";
|
|
||||||
|
|
||||||
|
|
||||||
|
type SenderMessageData<T = SenderMessage> =
|
||||||
|
T extends any
|
||||||
|
? Omit<T, "requestId">
|
||||||
|
: never;
|
||||||
|
|
||||||
type SessionSuccessCallback = (session: Session) => void;
|
type SessionSuccessCallback = (session: Session) => void;
|
||||||
|
|
||||||
export default class Session {
|
export default class Session {
|
||||||
#id = uuid();
|
#id = uuid();
|
||||||
|
|
||||||
|
#isConnected = false;
|
||||||
#successCallback?: SessionSuccessCallback;
|
#successCallback?: SessionSuccessCallback;
|
||||||
|
|
||||||
#messageListeners = new Map<string, Set<MessageListener>>();
|
#messageListeners = new Map<string, Set<MessageListener>>();
|
||||||
#updateListeners = new Set<UpdateListener>();
|
#updateListeners = new Set<UpdateListener>();
|
||||||
|
|
||||||
#sendMessageCallbacks = new Map<string, Callbacks>();
|
#sendMessageCallbacks =
|
||||||
#sendReceiverMessageCallbacks = new Map<string, Function>();
|
new Map<string, [ SuccessCallback?, ErrorCallback? ]>();
|
||||||
|
#sendReceiverMessageCallbacks =
|
||||||
|
new Map<string, (wasError: boolean) => void>();
|
||||||
|
|
||||||
#listener = onMessage(message => {
|
#listener = onMessage(message => {
|
||||||
// Filter other session messages
|
// Filter other session messages
|
||||||
@@ -61,38 +67,31 @@ export default class Session {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "shim:session/connected": {
|
|
||||||
this.status = SessionStatus.CONNECTED;
|
|
||||||
this.sessionId = message.data.sessionId;
|
|
||||||
this.transportId = message.data.sessionId;
|
|
||||||
this.namespaces = message.data.namespaces;
|
|
||||||
this.displayName = message.data.displayName;
|
|
||||||
this.statusText = message.data.statusText;
|
|
||||||
|
|
||||||
if (this.#successCallback) {
|
|
||||||
this.#successCallback(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "shim:session/updateStatus": {
|
case "shim:session/updateStatus": {
|
||||||
const status = message.data;
|
const { status } = message.data;
|
||||||
|
|
||||||
if (status.volume) {
|
// First status message indicates session creation
|
||||||
if (!this.receiver.volume) {
|
if (!this.#isConnected && status.applications) {
|
||||||
const receiverVolume = new Volume(
|
this.#isConnected = true;
|
||||||
status.volume.level, status.volume.muted);
|
|
||||||
|
|
||||||
receiverVolume.controlType = status.volume.controlType;
|
this.status = SessionStatus.CONNECTED;
|
||||||
receiverVolume.stepInterval =
|
|
||||||
status.volume.stepInterval;
|
// Update app props
|
||||||
} else {
|
const app = status.applications[0];
|
||||||
this.receiver.volume.level = status.volume.level;
|
this.sessionId = app.sessionId;
|
||||||
this.receiver.volume.muted = status.volume.muted;
|
this.namespaces = app.namespaces;
|
||||||
|
this.displayName = app.displayName;
|
||||||
|
this.statusText = app.statusText;
|
||||||
|
|
||||||
|
if (this.#successCallback) {
|
||||||
|
this.#successCallback(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.receiver.volume = status.volume;
|
||||||
|
|
||||||
for (const listener of this.#updateListeners) {
|
for (const listener of this.#updateListeners) {
|
||||||
listener(true);
|
listener(true);
|
||||||
}
|
}
|
||||||
@@ -100,25 +99,14 @@ export default class Session {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "shim:session/sendReceiverMessageResponse": {
|
|
||||||
const { messageId, wasError } = message.data;
|
|
||||||
const callback =
|
|
||||||
this.#sendReceiverMessageCallbacks.get(messageId);
|
|
||||||
if (callback) {
|
|
||||||
callback(wasError);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
case "shim:session/impl_addMessageListener": {
|
case "shim:session/impl_addMessageListener": {
|
||||||
const { namespace, data } = message.data;
|
const { namespace, message: newMessage } = message.data;
|
||||||
const messageListeners = this.#messageListeners.get(namespace);
|
const messageListeners = this.#messageListeners.get(namespace);
|
||||||
|
|
||||||
if (messageListeners) {
|
if (messageListeners) {
|
||||||
for (const listener of messageListeners) {
|
for (const listener of messageListeners) {
|
||||||
listener(namespace, data);
|
listener(namespace, newMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +114,11 @@ export default class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "shim:session/impl_sendMessage": {
|
case "shim:session/impl_sendMessage": {
|
||||||
const { messageId, error } = message.data;
|
const { messageId, wasError } = message.data;
|
||||||
const [ successCallback, errorCallback ] =
|
const [ successCallback, errorCallback ] =
|
||||||
this.#sendMessageCallbacks.get(messageId) ?? [];
|
this.#sendMessageCallbacks.get(messageId) ?? [];
|
||||||
|
|
||||||
if (error && errorCallback) {
|
if (wasError && errorCallback) {
|
||||||
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
|
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
|
||||||
} else if (successCallback) {
|
} else if (successCallback) {
|
||||||
successCallback();
|
successCallback();
|
||||||
@@ -140,31 +128,69 @@ export default class Session {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
media: Media[];
|
case "shim:session/impl_sendReceiverMessage": {
|
||||||
namespaces: Array<{ name: string }>;
|
const { messageId, wasError } = message.data;
|
||||||
senderApps: SenderApplication[];
|
const callback =
|
||||||
status: SessionStatus;
|
this.#sendReceiverMessageCallbacks.get(messageId);
|
||||||
statusText: Nullable<string>;
|
if (callback) {
|
||||||
|
callback(wasError);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the bridge that is forwarded to the
|
||||||
|
* receiver device. Promise resolves once the message is sent
|
||||||
|
* or an error occurs.
|
||||||
|
*/
|
||||||
|
#sendReceiverMessage = (message: SenderMessageData) => {
|
||||||
|
const messageId = uuid();
|
||||||
|
sendMessageResponse({
|
||||||
|
subject: "bridge:session/impl_sendReceiverMessage"
|
||||||
|
, data: {
|
||||||
|
message: { requestId: 0, ...message }
|
||||||
|
, messageId
|
||||||
|
, _id: this.#id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
this.#sendReceiverMessageCallbacks.set(messageId
|
||||||
|
, (wasError: boolean) => {
|
||||||
|
|
||||||
|
if (wasError) {
|
||||||
|
reject(new _Error(ErrorCode.SESSION_ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sendMediaMessage(message: SenderMediaMessage) {
|
||||||
|
this.sendMessage("urn:x-cast:com.google.cast.media", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
media: Media[] = [];
|
||||||
|
namespaces: Array<{ name: string }> = [];
|
||||||
|
senderApps: SenderApplication[] = [];
|
||||||
|
status = SessionStatus.CONNECTED;
|
||||||
|
statusText: Nullable<string> = null;
|
||||||
transportId: string;
|
transportId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(public sessionId: string
|
||||||
public sessionId: string
|
, public appId: string
|
||||||
, public appId: string
|
, public displayName: string
|
||||||
, public displayName: string
|
, public appImages: Image[]
|
||||||
, public appImages: Image[]
|
, public receiver: Receiver
|
||||||
, public receiver: Receiver
|
, _successCallback: SessionSuccessCallback) {
|
||||||
, _successCallback: SessionSuccessCallback) {
|
|
||||||
|
|
||||||
this.#successCallback = _successCallback;
|
this.#successCallback = _successCallback;
|
||||||
|
|
||||||
this.media = [];
|
|
||||||
this.namespaces = [];
|
|
||||||
this.senderApps = [];
|
|
||||||
this.status = SessionStatus.CONNECTED;
|
|
||||||
this.statusText = null;
|
|
||||||
this.transportId = sessionId || "";
|
this.transportId = sessionId || "";
|
||||||
|
|
||||||
if (receiver) {
|
if (receiver) {
|
||||||
@@ -186,9 +212,8 @@ export default class Session {
|
|||||||
logger.info("STUB :: Session#addMediaListener");
|
logger.info("STUB :: Session#addMediaListener");
|
||||||
}
|
}
|
||||||
|
|
||||||
addMessageListener(
|
addMessageListener(namespace: string
|
||||||
namespace: string
|
, listener: MessageListener) {
|
||||||
, listener: MessageListener) {
|
|
||||||
|
|
||||||
if (!this.#messageListeners.has(namespace)) {
|
if (!this.#messageListeners.has(namespace)) {
|
||||||
this.#messageListeners.set(namespace, new Set());
|
this.#messageListeners.set(namespace, new Set());
|
||||||
@@ -209,28 +234,17 @@ export default class Session {
|
|||||||
this.#updateListeners.add(listener);
|
this.#updateListeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
leave(
|
leave(_successCallback?: SuccessCallback
|
||||||
_successCallback?: SuccessCallback
|
, _errorCallback?: ErrorCallback): void {
|
||||||
, _errorCallback?: ErrorCallback): void {
|
|
||||||
|
|
||||||
logger.info("STUB :: Session#leave");
|
logger.info("STUB :: Session#leave");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMedia(
|
loadMedia(loadRequest: LoadRequest
|
||||||
loadRequest: LoadRequest
|
, successCallback?: LoadSuccessCallback
|
||||||
, successCallback?: LoadSuccessCallback
|
, errorCallback?: ErrorCallback): void {
|
||||||
, errorCallback?: ErrorCallback): void {
|
|
||||||
|
|
||||||
this._sendMediaMessage({
|
this._sendMediaMessage(loadRequest);
|
||||||
type: "LOAD"
|
|
||||||
, requestId: 0
|
|
||||||
, media: loadRequest.media
|
|
||||||
, activeTrackIds: loadRequest.activeTrackIds || []
|
|
||||||
, autoplay: loadRequest.autoplay || false
|
|
||||||
, currentTime: loadRequest.currentTime || 0
|
|
||||||
, customData: loadRequest.customData || {}
|
|
||||||
, repeatMode: RepeatMode.OFF
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
let hasResponded = false;
|
let hasResponded = false;
|
||||||
@@ -274,10 +288,9 @@ export default class Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
queueLoad(
|
queueLoad(_queueLoadRequest: QueueLoadRequest
|
||||||
_queueLoadRequest: QueueLoadRequest
|
, _successCallback?: LoadSuccessCallback
|
||||||
, _successCallback?: LoadSuccessCallback
|
, _errorCallback?: ErrorCallback): void {
|
||||||
, _errorCallback?: ErrorCallback): void {
|
|
||||||
|
|
||||||
logger.info("STUB :: Session#queueLoad");
|
logger.info("STUB :: Session#queueLoad");
|
||||||
}
|
}
|
||||||
@@ -285,26 +298,17 @@ export default class Session {
|
|||||||
removeMediaListener(_mediaListener: MediaListener): void {
|
removeMediaListener(_mediaListener: MediaListener): void {
|
||||||
logger.info("STUB :: Session#removeMediaListener");
|
logger.info("STUB :: Session#removeMediaListener");
|
||||||
}
|
}
|
||||||
|
removeMessageListener(namespace: string, listener: MessageListener): void {
|
||||||
removeMessageListener(
|
|
||||||
namespace: string
|
|
||||||
, listener: MessageListener): void {
|
|
||||||
|
|
||||||
this.#messageListeners.get(namespace)?.delete(listener);
|
this.#messageListeners.get(namespace)?.delete(listener);
|
||||||
}
|
}
|
||||||
|
removeUpdateListener(_namespace: string, listener: UpdateListener): void {
|
||||||
removeUpdateListener(
|
|
||||||
_namespace: string
|
|
||||||
, listener: UpdateListener): void {
|
|
||||||
|
|
||||||
this.#updateListeners.delete(listener);
|
this.#updateListeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(
|
sendMessage(namespace: string
|
||||||
namespace: string
|
, message: {} | string
|
||||||
, message: {} | string
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback): void {
|
||||||
, errorCallback?: ErrorCallback): void {
|
|
||||||
|
|
||||||
const messageId = uuid();
|
const messageId = uuid();
|
||||||
|
|
||||||
@@ -324,10 +328,9 @@ export default class Session {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReceiverMuted(
|
setReceiverMuted(muted: boolean
|
||||||
muted: boolean
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendReceiverMessage(
|
this.#sendReceiverMessage(
|
||||||
{ type: "SET_VOLUME"
|
{ type: "SET_VOLUME"
|
||||||
@@ -336,10 +339,9 @@ export default class Session {
|
|||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReceiverVolumeLevel(
|
setReceiverVolumeLevel(newLevel: number
|
||||||
newLevel: number
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback): void {
|
||||||
, errorCallback?: ErrorCallback): void {
|
|
||||||
|
|
||||||
this.#sendReceiverMessage(
|
this.#sendReceiverMessage(
|
||||||
{ type: "SET_VOLUME"
|
{ type: "SET_VOLUME"
|
||||||
@@ -348,9 +350,8 @@ export default class Session {
|
|||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(
|
stop(successCallback?: SuccessCallback
|
||||||
successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback): void {
|
||||||
, errorCallback?: ErrorCallback): void {
|
|
||||||
|
|
||||||
this.#sendReceiverMessage(
|
this.#sendReceiverMessage(
|
||||||
{ type: "STOP"
|
{ type: "STOP"
|
||||||
@@ -358,38 +359,4 @@ export default class Session {
|
|||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message to the bridge that is forwarded to the
|
|
||||||
* receiver device. Promise resolves once the message is sent
|
|
||||||
* or an error occurs.
|
|
||||||
*/
|
|
||||||
#sendReceiverMessage = (message: SessionReceiverMessage) => {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
if (!(message as any).requestId) {
|
|
||||||
(message as any).requestId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageId = uuid();
|
|
||||||
sendMessageResponse({
|
|
||||||
subject: "bridge:session/sendReceiverMessage"
|
|
||||||
, data: { message, messageId, _id: this.#id }
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#sendReceiverMessageCallbacks.set(
|
|
||||||
messageId, (wasError: boolean) => {
|
|
||||||
if (wasError) {
|
|
||||||
reject(new _Error(ErrorCode.SESSION_ERROR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _sendMediaMessage(message: string | {}) {
|
|
||||||
this.sendMessage("urn:x-cast:com.google.cast.media", message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import { AutoJoinPolicy
|
|||||||
, SenderPlatform
|
, SenderPlatform
|
||||||
, SessionStatus
|
, SessionStatus
|
||||||
, VolumeControlType } from "./enums";
|
, VolumeControlType } from "./enums";
|
||||||
import messaging from "../../messaging";
|
|
||||||
|
|
||||||
|
|
||||||
export * as media from "./media";
|
export * as media from "./media";
|
||||||
|
|||||||
@@ -1,52 +1,40 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import logger from "../../../lib/logger";
|
|
||||||
|
|
||||||
import { v1 as uuid } from "uuid";
|
import { v1 as uuid } from "uuid";
|
||||||
|
|
||||||
import { BreakStatus
|
import logger from "../../../lib/logger";
|
||||||
, EditTracksInfoRequest
|
|
||||||
, GetStatusRequest
|
|
||||||
, LiveSeekableRange
|
|
||||||
, MediaInfo
|
|
||||||
, PauseRequest
|
|
||||||
, PlayRequest
|
|
||||||
, QueueData
|
|
||||||
, QueueJumpRequest
|
|
||||||
, QueueInsertItemsRequest
|
|
||||||
, QueueItem
|
|
||||||
, QueueSetPropertiesRequest
|
|
||||||
, QueueRemoveItemsRequest
|
|
||||||
, QueueReorderItemsRequest
|
|
||||||
, QueueUpdateItemsRequest
|
|
||||||
, SeekRequest
|
|
||||||
, StopRequest
|
|
||||||
, VideoInformation
|
|
||||||
, VolumeRequest } from "./dataClasses";
|
|
||||||
|
|
||||||
import { Volume, Error as _Error } from "../dataClasses";
|
import { Volume, Error as _Error } from "../dataClasses";
|
||||||
|
import { BreakStatus, EditTracksInfoRequest, GetStatusRequest, LiveSeekableRange
|
||||||
|
, MediaInfo, PauseRequest, PlayRequest, QueueData, QueueJumpRequest
|
||||||
|
, QueueInsertItemsRequest, QueueItem, QueueSetPropertiesRequest
|
||||||
|
, QueueRemoveItemsRequest, QueueReorderItemsRequest
|
||||||
|
, QueueUpdateItemsRequest, SeekRequest, StopRequest, VideoInformation
|
||||||
|
, VolumeRequest } from "./dataClasses";
|
||||||
|
|
||||||
import { PlayerState
|
import { PlayerState, RepeatMode } from "./enums";
|
||||||
, RepeatMode } from "./enums";
|
|
||||||
|
|
||||||
import { ErrorCode } from "../enums";
|
import { ErrorCode } from "../enums";
|
||||||
|
|
||||||
import { onMessage, sendMessageResponse } from "../../eventMessageChannel";
|
import { onMessage, sendMessageResponse } from "../../eventMessageChannel";
|
||||||
|
|
||||||
import { Callbacks
|
import { ErrorCallback
|
||||||
, ErrorCallback
|
|
||||||
, SuccessCallback
|
, SuccessCallback
|
||||||
, UpdateListener } from "../../types";
|
, UpdateListener } from "../../types";
|
||||||
|
import { SenderMediaMessage } from "../types";
|
||||||
import { SessionMediaMessage } from "../../../types";
|
|
||||||
|
|
||||||
|
|
||||||
export default class Media {
|
export default class Media {
|
||||||
#id = uuid();
|
#id = uuid();
|
||||||
#isActive = true;
|
#isActive = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp of last status update
|
||||||
|
*/
|
||||||
|
#lastUpdateTime = 0;
|
||||||
|
|
||||||
#updateListeners = new Set<UpdateListener>();
|
#updateListeners = new Set<UpdateListener>();
|
||||||
#sendMediaMessageCallbacks = new Map<string, Callbacks>();
|
#sendMediaMessageCallbacks =
|
||||||
#lastCurrentTime = 0;
|
new Map<string, [ SuccessCallback?, ErrorCallback? ]>();
|
||||||
|
|
||||||
#listener = onMessage(message => {
|
#listener = onMessage(message => {
|
||||||
if ((message as any).data._id !== this.#id) {
|
if ((message as any).data._id !== this.#id) {
|
||||||
@@ -54,27 +42,24 @@ export default class Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (message.subject) {
|
switch (message.subject) {
|
||||||
case "shim:media/update": {
|
case "shim:media/updateStatus": {
|
||||||
const status = message.data;
|
const { status } = message.data;
|
||||||
|
|
||||||
|
// Store current update time
|
||||||
|
this.#lastUpdateTime = Date.now();
|
||||||
|
|
||||||
this.currentTime = status.currentTime;
|
this.currentTime = status.currentTime;
|
||||||
this.#lastCurrentTime = status._lastCurrentTime;
|
this.mediaSessionId = status.mediaSessionId;
|
||||||
this.customData = status.customData;
|
|
||||||
this.playbackRate = status.playbackRate;
|
this.playbackRate = status.playbackRate;
|
||||||
this.playerState = status.playerState;
|
this.playerState = status.playerState;
|
||||||
this.repeatMode = status.repeatMode;
|
this.repeatMode = status.repeatMode;
|
||||||
|
this.volume = status.volume;
|
||||||
|
|
||||||
if (status._volumeLevel && status._volumeMuted) {
|
if (status.customData) {
|
||||||
this.volume = new Volume(
|
this.customData = status.customData;
|
||||||
status._volumeLevel
|
|
||||||
, status._volumeMuted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.media) {
|
if (status.media) {
|
||||||
this.media = status.media;
|
this.media = status.media as MediaInfo;
|
||||||
}
|
|
||||||
if (status.mediaSessionId) {
|
|
||||||
this.mediaSessionId = status.mediaSessionId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call update listeners
|
// Call update listeners
|
||||||
@@ -106,8 +91,8 @@ export default class Media {
|
|||||||
activeTrackIds: Nullable<number[]> = null;
|
activeTrackIds: Nullable<number[]> = null;
|
||||||
breakStatus?: BreakStatus;
|
breakStatus?: BreakStatus;
|
||||||
currentItemId: Nullable<number> = null;
|
currentItemId: Nullable<number> = null;
|
||||||
customData: any = null;
|
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
|
customData: any = null;
|
||||||
idleReason: Nullable<string> = null;
|
idleReason: Nullable<string> = null;
|
||||||
items: Nullable<QueueItem[]> = null;
|
items: Nullable<QueueItem[]> = null;
|
||||||
liveSeekableRange?: LiveSeekableRange;
|
liveSeekableRange?: LiveSeekableRange;
|
||||||
@@ -123,10 +108,9 @@ export default class Media {
|
|||||||
volume: Volume = new Volume();
|
volume: Volume = new Volume();
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(public sessionId: string
|
||||||
public sessionId: string
|
, public mediaSessionId: number
|
||||||
, public mediaSessionId: number
|
, _internalSessionId: string) {
|
||||||
, _internalSessionId: string) {
|
|
||||||
|
|
||||||
sendMessageResponse({
|
sendMessageResponse({
|
||||||
subject: "bridge:media/initialize"
|
subject: "bridge:media/initialize"
|
||||||
@@ -143,10 +127,9 @@ export default class Media {
|
|||||||
this.#updateListeners.add(listener);
|
this.#updateListeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
editTracksInfo(
|
editTracksInfo(editTracksInfoRequest: EditTracksInfoRequest
|
||||||
editTracksInfoRequest: EditTracksInfoRequest
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(
|
this.#sendMediaMessage(
|
||||||
{ type: "EDIT_TRACKS_INFO", ...editTracksInfoRequest })
|
{ type: "EDIT_TRACKS_INFO", ...editTracksInfoRequest })
|
||||||
@@ -171,8 +154,8 @@ export default class Media {
|
|||||||
*/
|
*/
|
||||||
getEstimatedTime(): number {
|
getEstimatedTime(): number {
|
||||||
if (this.playerState === PlayerState.PLAYING) {
|
if (this.playerState === PlayerState.PLAYING) {
|
||||||
let estimatedTime = this.currentTime + (this.playbackRate * (
|
let estimatedTime = this.currentTime +
|
||||||
Date.now() - this.#lastCurrentTime) / 1000);
|
((Date.now() - this.#lastUpdateTime) / 1000);
|
||||||
|
|
||||||
// Enforce valid range
|
// Enforce valid range
|
||||||
if (estimatedTime < 0) {
|
if (estimatedTime < 0) {
|
||||||
@@ -192,10 +175,9 @@ export default class Media {
|
|||||||
* Request media status from the receiver application. This
|
* Request media status from the receiver application. This
|
||||||
* will also trigger any added media update listeners.
|
* will also trigger any added media update listeners.
|
||||||
*/
|
*/
|
||||||
getStatus(
|
getStatus(getStatusRequest = new GetStatusRequest()
|
||||||
getStatusRequest = new GetStatusRequest()
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(
|
this.#sendMediaMessage(
|
||||||
{ type: "MEDIA_GET_STATUS", ...getStatusRequest })
|
{ type: "MEDIA_GET_STATUS", ...getStatusRequest })
|
||||||
@@ -203,10 +185,9 @@ export default class Media {
|
|||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
pause(
|
pause(pauseRequest = new PauseRequest()
|
||||||
pauseRequest = new PauseRequest()
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(
|
this.#sendMediaMessage(
|
||||||
{ type: "PAUSE", ...pauseRequest })
|
{ type: "PAUSE", ...pauseRequest })
|
||||||
@@ -214,10 +195,9 @@ export default class Media {
|
|||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
play(
|
play(playRequest = new PlayRequest()
|
||||||
playRequest = new PlayRequest()
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(
|
this.#sendMediaMessage(
|
||||||
{ type: "PLAY", ...playRequest })
|
{ type: "PLAY", ...playRequest })
|
||||||
@@ -225,47 +205,52 @@ export default class Media {
|
|||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueAppendItem(
|
queueAppendItem(item: QueueItem
|
||||||
item: QueueItem
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(new QueueInsertItemsRequest([ item ]))
|
this.#sendMediaMessage(
|
||||||
|
{
|
||||||
|
...new QueueInsertItemsRequest([ item ])
|
||||||
|
, type: "QUEUE_INSERT"
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueInsertItems(
|
queueInsertItems(queueInsertItemsRequest: QueueInsertItemsRequest
|
||||||
queueInsertItemsRequest: QueueInsertItemsRequest
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(queueInsertItemsRequest)
|
this.#sendMediaMessage(
|
||||||
|
{
|
||||||
|
...queueInsertItemsRequest
|
||||||
|
, type: "QUEUE_INSERT"
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queueJumpToItem(
|
queueJumpToItem(itemId: number
|
||||||
itemId: number
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
if (this.items?.find(item => item.itemId === itemId)) {
|
if (this.items?.find(item => item.itemId === itemId)) {
|
||||||
const jumpRequest = new QueueJumpRequest();
|
const jumpRequest = new QueueJumpRequest();
|
||||||
jumpRequest.currentItemId = itemId;
|
jumpRequest.currentItemId = itemId;
|
||||||
|
|
||||||
this.#sendMediaMessage(jumpRequest)
|
this.#sendMediaMessage(
|
||||||
|
{ ...jumpRequest, type: "QUEUE_UPDATE" })
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueMoveItemToNewIndex(
|
queueMoveItemToNewIndex(itemId: number
|
||||||
itemId: number
|
, newIndex: number
|
||||||
, newIndex: number
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
// Return early if not in queue
|
// Return early if not in queue
|
||||||
if (!this.items) {
|
if (!this.items) {
|
||||||
@@ -295,40 +280,40 @@ export default class Media {
|
|||||||
reorderItemsRequest.insertBefore = existingItem.itemId;
|
reorderItemsRequest.insertBefore = existingItem.itemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#sendMediaMessage(reorderItemsRequest)
|
this.#sendMediaMessage(
|
||||||
|
{ ...reorderItemsRequest, type: "QUEUE_REORDER" })
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueNext(
|
queueNext(successCallback?: SuccessCallback
|
||||||
successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
const jumpRequest = new QueueJumpRequest();
|
const jumpRequest = new QueueJumpRequest();
|
||||||
jumpRequest.jump = 1;
|
jumpRequest.jump = 1;
|
||||||
|
|
||||||
this.#sendMediaMessage(jumpRequest)
|
this.#sendMediaMessage(
|
||||||
|
{ ...jumpRequest, type: "QUEUE_UPDATE" })
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queuePrev(
|
queuePrev(successCallback?: SuccessCallback
|
||||||
successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
const jumpRequest = new QueueJumpRequest();
|
const jumpRequest = new QueueJumpRequest();
|
||||||
jumpRequest.jump = -1;
|
jumpRequest.jump = -1;
|
||||||
|
|
||||||
this.#sendMediaMessage(jumpRequest)
|
this.#sendMediaMessage(
|
||||||
|
{ ...jumpRequest, type: "QUEUE_UPDATE" })
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueRemoveItem(
|
queueRemoveItem(itemId: number
|
||||||
itemId: number
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
const item = this.items?.find(item => item.itemId === itemId);
|
const item = this.items?.find(item => item.itemId === itemId);
|
||||||
if (item) {
|
if (item) {
|
||||||
@@ -338,45 +323,45 @@ export default class Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueRemoveItems(
|
queueRemoveItems(queueRemoveItemsRequest: QueueRemoveItemsRequest
|
||||||
queueRemoveItemsRequest: QueueRemoveItemsRequest
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(queueRemoveItemsRequest)
|
this.#sendMediaMessage(
|
||||||
|
{ ...queueRemoveItemsRequest, type: "QUEUE_REMOVE" })
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueReorderItems(
|
queueReorderItems(queueReorderItemsRequest: QueueReorderItemsRequest
|
||||||
queueReorderItemsRequest: QueueReorderItemsRequest
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(queueReorderItemsRequest)
|
this.#sendMediaMessage(
|
||||||
|
{ ...queueReorderItemsRequest, type: "QUEUE_REORDER" })
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueSetRepeatMode(
|
queueSetRepeatMode(repeatMode: string
|
||||||
repeatMode: string
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
const setPropertiesRequest = new QueueSetPropertiesRequest();
|
const setPropertiesRequest = new QueueSetPropertiesRequest();
|
||||||
setPropertiesRequest.repeatMode = repeatMode;
|
setPropertiesRequest.repeatMode = repeatMode;
|
||||||
|
|
||||||
this.#sendMediaMessage(setPropertiesRequest)
|
this.#sendMediaMessage(
|
||||||
|
{ ...setPropertiesRequest, type: "QUEUE_UPDATE" })
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueUpdateItems(
|
queueUpdateItems(queueUpdateItemsRequest: QueueUpdateItemsRequest
|
||||||
queueUpdateItemsRequest: QueueUpdateItemsRequest
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(queueUpdateItemsRequest)
|
this.#sendMediaMessage(
|
||||||
|
{ ...queueUpdateItemsRequest, type: "QUEUE_UPDATE" })
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
@@ -385,10 +370,9 @@ export default class Media {
|
|||||||
this.#updateListeners.delete(listener);
|
this.#updateListeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(
|
seek(seekRequest: SeekRequest
|
||||||
seekRequest: SeekRequest
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(
|
this.#sendMediaMessage(
|
||||||
{ type: "SEEK", ...seekRequest })
|
{ type: "SEEK", ...seekRequest })
|
||||||
@@ -396,10 +380,9 @@ export default class Media {
|
|||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(
|
setVolume(volumeRequest: VolumeRequest
|
||||||
volumeRequest: VolumeRequest
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
this.#sendMediaMessage(
|
this.#sendMediaMessage(
|
||||||
{ type: "MEDIA_SET_VOLUME", ...volumeRequest })
|
{ type: "MEDIA_SET_VOLUME", ...volumeRequest })
|
||||||
@@ -407,10 +390,9 @@ export default class Media {
|
|||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(
|
stop(stopRequest?: StopRequest
|
||||||
stopRequest?: StopRequest
|
, successCallback?: SuccessCallback
|
||||||
, successCallback?: SuccessCallback
|
, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
if (!stopRequest) {
|
if (!stopRequest) {
|
||||||
stopRequest = new StopRequest();
|
stopRequest = new StopRequest();
|
||||||
@@ -434,7 +416,11 @@ export default class Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#sendMediaMessage = async (message: SessionMediaMessage) => {
|
#sendMediaMessage = async (
|
||||||
|
// Allow messages without requestId
|
||||||
|
message: Omit<SenderMediaMessage, "requestId">
|
||||||
|
& { requestId?: Nullable<number> }) => {
|
||||||
|
|
||||||
if (!this.media) {
|
if (!this.media) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -465,10 +451,10 @@ export default class Media {
|
|||||||
|
|
||||||
sendMessageResponse({
|
sendMessageResponse({
|
||||||
subject: "bridge:media/sendMediaMessage"
|
subject: "bridge:media/sendMediaMessage"
|
||||||
, data: {
|
, data: {
|
||||||
message
|
message
|
||||||
, messageId
|
, messageId
|
||||||
, _id: this.#id
|
, _id: this.#id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { ContainerType
|
|||||||
, HlsVideoSegmentFormat
|
, HlsVideoSegmentFormat
|
||||||
, MetadataType
|
, MetadataType
|
||||||
, RepeatMode
|
, RepeatMode
|
||||||
, StreamType
|
, ResumeState, StreamType
|
||||||
, UserAction } from "./enums";
|
, TrackType, UserAction } from "./enums";
|
||||||
|
|
||||||
|
|
||||||
export class AudiobookChapterMediaMetadata {
|
export class AudiobookChapterMediaMetadata {
|
||||||
@@ -93,12 +93,12 @@ export class EditTracksInfoRequest {
|
|||||||
|
|
||||||
export class GenericMediaMetadata {
|
export class GenericMediaMetadata {
|
||||||
images?: Image[];
|
images?: Image[];
|
||||||
metadataType: number = MetadataType.GENERIC;
|
metadataType = MetadataType.GENERIC;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
releaseYear?: number;
|
releaseYear?: number;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
type: number = MetadataType.GENERIC;
|
type = MetadataType.GENERIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ export class LoadRequest {
|
|||||||
media: MediaInfo;
|
media: MediaInfo;
|
||||||
requestId = 0;
|
requestId = 0;
|
||||||
sessionId: Nullable<string> = null;
|
sessionId: Nullable<string> = null;
|
||||||
type = "LOAD";
|
type: "LOAD" = "LOAD";
|
||||||
|
|
||||||
constructor(mediaInfo: MediaInfo) {
|
constructor(mediaInfo: MediaInfo) {
|
||||||
this.media = mediaInfo;
|
this.media = mediaInfo;
|
||||||
@@ -134,7 +134,7 @@ export class LoadRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Metadata =
|
export type Metadata =
|
||||||
GenericMediaMetadata
|
GenericMediaMetadata
|
||||||
| MovieMediaMetadata
|
| MovieMediaMetadata
|
||||||
| MusicTrackMediaMetadata
|
| MusicTrackMediaMetadata
|
||||||
@@ -183,13 +183,13 @@ export class MediaMetadata {
|
|||||||
|
|
||||||
export class MovieMediaMetadata {
|
export class MovieMediaMetadata {
|
||||||
images?: Image[];
|
images?: Image[];
|
||||||
metadataType: number = MetadataType.MOVIE;
|
metadataType = MetadataType.MOVIE;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
releaseYear?: number;
|
releaseYear?: number;
|
||||||
studio?: string;
|
studio?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
type: number = MetadataType.MOVIE;
|
type = MetadataType.MOVIE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -201,13 +201,13 @@ export class MusicTrackMediaMetadata {
|
|||||||
composer?: string;
|
composer?: string;
|
||||||
discNumber?: number;
|
discNumber?: number;
|
||||||
images?: Image[];
|
images?: Image[];
|
||||||
metadataType: number = MetadataType.MUSIC_TRACK;
|
metadataType = MetadataType.MUSIC_TRACK;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
releaseYear?: number;
|
releaseYear?: number;
|
||||||
songName?: string;
|
songName?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
trackNumber?: number;
|
trackNumber?: number;
|
||||||
type: number = MetadataType.MUSIC_TRACK;
|
type = MetadataType.MUSIC_TRACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -224,9 +224,9 @@ export class PhotoMediaMetadata {
|
|||||||
latitude?: number;
|
latitude?: number;
|
||||||
location?: string;
|
location?: string;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
metadataType: number = MetadataType.PHOTO;
|
metadataType = MetadataType.PHOTO;
|
||||||
title?: string;
|
title?: string;
|
||||||
type: number = MetadataType.PHOTO;
|
type = MetadataType.PHOTO;
|
||||||
width?: number;
|
width?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ export class QueueUpdateItemsRequest {
|
|||||||
export class SeekRequest {
|
export class SeekRequest {
|
||||||
currentTime: Nullable<number> = null;
|
currentTime: Nullable<number> = null;
|
||||||
customData: any = null;
|
customData: any = null;
|
||||||
resumeState: Nullable<string> = null;
|
resumeState: Nullable<ResumeState> = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -382,7 +382,7 @@ export class Track {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public trackId: number
|
public trackId: number
|
||||||
, public type: string) {}
|
, public type: TrackType) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ export class TvShowMediaMetadata {
|
|||||||
seasonNumber?: number;
|
seasonNumber?: number;
|
||||||
seriesTitle?: string;
|
seriesTitle?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
type: number = MetadataType.TV_SHOW;
|
type = MetadataType.TV_SHOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
157
ext/src/shim/cast/types.ts
Normal file
157
ext/src/shim/cast/types.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep in sync with bridge types at:
|
||||||
|
* app/src/bridge/components/chromecast/types.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Volume } from "./dataClasses";
|
||||||
|
import { MediaInfo, QueueItem } from "./media/dataClasses";
|
||||||
|
import { IdleReason
|
||||||
|
, PlayerState
|
||||||
|
, RepeatMode
|
||||||
|
, ResumeState } from "./media/enums";
|
||||||
|
|
||||||
|
|
||||||
|
export interface MediaStatus {
|
||||||
|
mediaSessionId: number;
|
||||||
|
media?: MediaInfo;
|
||||||
|
playbackRate: number;
|
||||||
|
playerState: PlayerState;
|
||||||
|
idleReason?: IdleReason;
|
||||||
|
currentTime: number;
|
||||||
|
supportedMediaCommands: number;
|
||||||
|
repeatMode: RepeatMode;
|
||||||
|
volume: Volume
|
||||||
|
customData: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceiverApplication {
|
||||||
|
appId: string
|
||||||
|
, appType?: string
|
||||||
|
, displayName: string
|
||||||
|
, iconUrl: string
|
||||||
|
, isIdleScreen: boolean
|
||||||
|
, launchedFromCloud: boolean
|
||||||
|
, namespaces: Array<{ name: string }>
|
||||||
|
, sessionId: string
|
||||||
|
, statusText: string
|
||||||
|
, transportId: string
|
||||||
|
, universalAppId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceiverStatus {
|
||||||
|
applications?: ReceiverApplication[]
|
||||||
|
, isActiveInput?: boolean
|
||||||
|
, isStandBy?: boolean
|
||||||
|
, volume: Volume
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ReqBase {
|
||||||
|
requestId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NS: urn:x-cast:com.google.cast.receiver
|
||||||
|
export type SenderMessage =
|
||||||
|
ReqBase & { type: "LAUNCH", appId: string }
|
||||||
|
| ReqBase & { type: "STOP", sessionId: string }
|
||||||
|
| ReqBase & { type: "GET_STATUS" }
|
||||||
|
| ReqBase & { type: "GET_APP_AVAILABILITY", appId: string[] }
|
||||||
|
| ReqBase & { type: "SET_VOLUME", volume: Partial<Volume> };
|
||||||
|
|
||||||
|
export type ReceiverMessage =
|
||||||
|
ReqBase & {
|
||||||
|
type: "RECEIVER_STATUS"
|
||||||
|
, status: ReceiverStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface MediaReqBase extends ReqBase {
|
||||||
|
customData?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NS: urn:x-cast:com.google.cast.media
|
||||||
|
export type SenderMediaMessage =
|
||||||
|
| MediaReqBase & { type: "PLAY" }
|
||||||
|
| MediaReqBase & { type: "PAUSE" }
|
||||||
|
| MediaReqBase & { type: "MEDIA_GET_STATUS" }
|
||||||
|
| MediaReqBase & { type: "STOP" }
|
||||||
|
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Partial<Volume> }
|
||||||
|
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "LOAD"
|
||||||
|
, activeTrackIds: Nullable<number[]>
|
||||||
|
, atvCredentials?: string
|
||||||
|
, atvCredentialsType?: string
|
||||||
|
, autoplay: Nullable<boolean>
|
||||||
|
, currentTime: Nullable<number>
|
||||||
|
, customData: any
|
||||||
|
, media: MediaInfo
|
||||||
|
, requestId: number
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "SEEK"
|
||||||
|
, resumeState: Nullable<ResumeState>
|
||||||
|
, currentTime: Nullable<number>
|
||||||
|
}
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "EDIT_TRACKS_INFO"
|
||||||
|
, activeTrackIds: Nullable<number[]>
|
||||||
|
, textTrackStyle: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueLoadRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_LOAD"
|
||||||
|
, items: QueueItem[]
|
||||||
|
, startIndex: number
|
||||||
|
, repeatMode: string
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueInsertItemsRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_INSERT"
|
||||||
|
, items: QueueItem[]
|
||||||
|
, insertBefore: Nullable<number>
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueUpdateItemsRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_UPDATE"
|
||||||
|
, items: QueueItem[]
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueJumpRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_UPDATE"
|
||||||
|
, jump: Nullable<number>
|
||||||
|
, currentItemId: Nullable<number>
|
||||||
|
, sessionId: Nullable<number>
|
||||||
|
}
|
||||||
|
// QueueRemoveItemsRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_REMOVE"
|
||||||
|
, itemIds: number[]
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueReorderItemsRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_REORDER"
|
||||||
|
, itemIds: number[]
|
||||||
|
, insertBefore: Nullable<number>
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
}
|
||||||
|
// QueueSetPropertiesRequest
|
||||||
|
| MediaReqBase & {
|
||||||
|
type: "QUEUE_UPDATE"
|
||||||
|
, repeatMode: Nullable<string>
|
||||||
|
, sessionId: Nullable<string>
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReceiverMediaMessage =
|
||||||
|
MediaReqBase & { type: "MEDIA_STATUS", status: MediaStatus[] }
|
||||||
|
| MediaReqBase & { type: "INVALID_PLAYER_STATE" }
|
||||||
|
| MediaReqBase & { type: "LOAD_FAILED" }
|
||||||
|
| MediaReqBase & { type: "LOAD_CANCELLED" }
|
||||||
|
| MediaReqBase & { type: "INVALID_REQUEST" };
|
||||||
@@ -11,6 +11,3 @@ export type MediaListener = (media: Media) => void;
|
|||||||
export type MessageListener = (namespace: string, message: string) => void;
|
export type MessageListener = (namespace: string, message: string) => void;
|
||||||
export type UpdateListener = (isAlive: boolean) => void;
|
export type UpdateListener = (isAlive: boolean) => void;
|
||||||
export type LoadSuccessCallback = (media: Media) => void;
|
export type LoadSuccessCallback = (media: Media) => void;
|
||||||
|
|
||||||
export type Callbacks = [ SuccessCallback?, ErrorCallback? ];
|
|
||||||
export type CallbacksMap = Map<string, Callbacks>;
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { Volume } from "./shim/cast/dataClasses";
|
import { ReceiverStatus } from "./shim/cast/types";
|
||||||
|
|
||||||
import { EditTracksInfoRequest, GetStatusRequest, LoadRequest, PauseRequest
|
|
||||||
, PlayRequest, QueueInsertItemsRequest, QueueJumpRequest
|
|
||||||
, QueueLoadRequest, QueueRemoveItemsRequest, QueueReorderItemsRequest
|
|
||||||
, QueueSetPropertiesRequest, QueueUpdateItemsRequest, SeekRequest
|
|
||||||
, StopRequest, VolumeRequest } from "./shim/cast/media";
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReceiverDevice {
|
export interface ReceiverDevice {
|
||||||
host: string
|
host: string
|
||||||
@@ -16,49 +9,3 @@ export interface ReceiverDevice {
|
|||||||
, port: number
|
, port: number
|
||||||
, status?: ReceiverStatus
|
, status?: ReceiverStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiverStatus {
|
|
||||||
applications?: Array<{
|
|
||||||
appId: string
|
|
||||||
, appType: string
|
|
||||||
, displayName: string
|
|
||||||
, iconUrl: string
|
|
||||||
, isIdleScreen: boolean
|
|
||||||
, launchedFromCloud: boolean
|
|
||||||
, namespaces: Array<{ name: string }>
|
|
||||||
, sessionId: string
|
|
||||||
, statusText: string
|
|
||||||
, transportId: string
|
|
||||||
, universalAppId: string
|
|
||||||
}>
|
|
||||||
, isActiveInput?: boolean
|
|
||||||
, isStandBy?: boolean
|
|
||||||
, userEq: unknown
|
|
||||||
, volume: Volume
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type SessionMediaMessage =
|
|
||||||
{ type: "PLAY" } & PlayRequest
|
|
||||||
| { type: "PAUSE" } & PauseRequest
|
|
||||||
| { type: "SEEK" } & SeekRequest
|
|
||||||
| { type: "STOP" } & StopRequest
|
|
||||||
| { type: "MEDIA_GET_STATUS" } & GetStatusRequest
|
|
||||||
| { type: "MEDIA_SET_VOLUME" } & VolumeRequest
|
|
||||||
| { type: "EDIT_TRACKS_INFO" } & EditTracksInfoRequest
|
|
||||||
| { type: "SET_PLAYBACK_RATE", playbackRate: number }
|
|
||||||
| LoadRequest
|
|
||||||
| QueueLoadRequest
|
|
||||||
| QueueInsertItemsRequest
|
|
||||||
| QueueUpdateItemsRequest
|
|
||||||
| QueueJumpRequest
|
|
||||||
| QueueRemoveItemsRequest
|
|
||||||
| QueueReorderItemsRequest
|
|
||||||
| QueueSetPropertiesRequest;
|
|
||||||
|
|
||||||
export type SessionReceiverMessage =
|
|
||||||
{ type: "LAUNCH", appId: string }
|
|
||||||
| { type: "STOP", sessionId: string }
|
|
||||||
| { type: "GET_STATUS" }
|
|
||||||
| { type: "GET_APP_AVAILABILITY", appId: string[] }
|
|
||||||
| { type: "SET_VOLUME", volume: Partial<Volume> };
|
|
||||||
|
|||||||
Reference in New Issue
Block a user