mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-10 17:49:58 +00:00
Improve session/media bridge messaging
This commit is contained in:
@@ -2,27 +2,16 @@
|
||||
|
||||
import castv2 from "castv2";
|
||||
|
||||
import Session from "./Session";
|
||||
import { ReceiverMediaMessage } from "./types";
|
||||
|
||||
import { Message } from "../../messaging";
|
||||
import { sendMessage } from "../../lib/nativeMessaging";
|
||||
|
||||
import Session from "./Session";
|
||||
|
||||
|
||||
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 {
|
||||
private channel: castv2.Channel;
|
||||
@@ -31,40 +20,32 @@ export default class Media {
|
||||
private referenceId: string
|
||||
, private session: Session) {
|
||||
|
||||
// Ensure channel exists
|
||||
this.session.createChannel(NS_MEDIA);
|
||||
this.channel = this.session.channelMap.get(NS_MEDIA)!;
|
||||
|
||||
this.channel.on("message", (data: any) => {
|
||||
if (data && data.type === "MEDIA_STATUS"
|
||||
&& data.status && data.status.length > 0) {
|
||||
const channel = this.session.channelMap.get(NS_MEDIA);
|
||||
if (!channel) {
|
||||
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 = {
|
||||
_lastCurrentTime: Date.now() / 1000
|
||||
private onMediaMessage = (message: ReceiverMediaMessage) => {
|
||||
switch (message.type) {
|
||||
case "MEDIA_STATUS": {
|
||||
// TODO: Fix for multiple media statuses
|
||||
const status = message.status[0];
|
||||
|
||||
, currentTime: status.currentTime
|
||||
, customData: status.customData
|
||||
, playbackRate: status.playbackRate
|
||||
, playerState: status.playerState
|
||||
, repeatMode: status.repeatMode
|
||||
};
|
||||
this.sendMessage({
|
||||
subject: "shim:media/updateStatus"
|
||||
, data: { status }
|
||||
});
|
||||
|
||||
if (status.volume) {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public messageHandler(message: Message) {
|
||||
@@ -77,9 +58,12 @@ export default class Media {
|
||||
error = true;
|
||||
}
|
||||
|
||||
this.sendMessage("shim:media/sendMediaMessageResponse", {
|
||||
messageId: message.data.messageId
|
||||
, error
|
||||
this.sendMessage({
|
||||
subject: "shim:media/sendMediaMessageResponse"
|
||||
, data: {
|
||||
messageId: message.data.messageId
|
||||
, error
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -87,11 +71,8 @@ export default class Media {
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage(subject: string, data: any) {
|
||||
data._id = this.referenceId;
|
||||
(sendMessage as any)({
|
||||
subject
|
||||
, data
|
||||
});
|
||||
private sendMessage(message: Message) {
|
||||
(message.data as any)._id = this.referenceId;
|
||||
sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,32 +5,39 @@ import { Channel, Client } from "castv2";
|
||||
import { Message } from "../../messaging";
|
||||
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_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
|
||||
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
|
||||
|
||||
const HEARTBEAT_INTERVAL = 5000;
|
||||
|
||||
|
||||
export default class Session {
|
||||
public channelMap = new Map<string, Channel>();
|
||||
private isSessionCreated = false;
|
||||
|
||||
private client: Client;
|
||||
private clientConnection?: Channel;
|
||||
private clientHeartbeat?: Channel;
|
||||
private clientReceiver?: Channel;
|
||||
private clientHeartbeatIntervalId?: NodeJS.Timeout;
|
||||
|
||||
private isSessionCreated = false;
|
||||
|
||||
private clientId?: string;
|
||||
private clientId = `client-${Math.floor(Math.random() * 10e5)}`;
|
||||
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 app: any;
|
||||
private transportHeartbeat?: Channel;
|
||||
|
||||
private app?: ReceiverApplication;
|
||||
|
||||
constructor(
|
||||
public host: string
|
||||
, public port: number
|
||||
, private appId: string
|
||||
, private sessionId: string
|
||||
, private referenceId: string) {
|
||||
|
||||
const client = new Client();
|
||||
@@ -38,135 +45,18 @@ export default class Session {
|
||||
client.on("error", err => {
|
||||
console.error(`castv2 error: ${err}`);
|
||||
});
|
||||
|
||||
client.on("close", () => {
|
||||
// TODO: Don't send new data
|
||||
if (this.platformHeartbeatIntervalId) {
|
||||
clearInterval(this.platformHeartbeatIntervalId);
|
||||
}
|
||||
});
|
||||
|
||||
client.connect({ host, port }, this.onConnect.bind(this));
|
||||
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) {
|
||||
if (!this.channelMap.has(namespace)) {
|
||||
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() {
|
||||
this.clientConnection?.send({ type: "CLOSE" });
|
||||
this.platformConnection?.send({ type: "CLOSE" });
|
||||
this.transportConnection?.send({ type: "CLOSE" });
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.clientConnection?.send({ type: "STOP" });
|
||||
this.platformConnection?.send({ type: "STOP" });
|
||||
}
|
||||
|
||||
private sendMessage(subject: string, data: any = {}) {
|
||||
data._id = this.referenceId;
|
||||
sendMessage({
|
||||
// @ts-ignore
|
||||
subject
|
||||
, data
|
||||
});
|
||||
private sendMessage(message: Message) {
|
||||
(message.data as any)._id = this.referenceId;
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
private _impl_addMessageListener(namespace: string) {
|
||||
// TODO: Limit to one listener per namespace
|
||||
this.createChannel(namespace);
|
||||
this.channelMap.get(namespace)?.on("message", (data: any) => {
|
||||
this.sendMessage("shim:session/impl_addMessageListener", {
|
||||
namespace
|
||||
, data: JSON.stringify(data)
|
||||
this.channelMap.get(namespace)?.on("message", (message: any) => {
|
||||
this.sendMessage({
|
||||
subject: "shim:session/impl_addMessageListener"
|
||||
, data: {
|
||||
namespace
|
||||
, message: JSON.stringify(message)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _impl_sendMessage(
|
||||
namespace: string
|
||||
, message: {} | string
|
||||
, message: object | string
|
||||
, messageId: string) {
|
||||
|
||||
let error = false;
|
||||
let wasError = false;
|
||||
|
||||
try {
|
||||
// Decode string messages
|
||||
@@ -219,12 +221,34 @@ export default class Session {
|
||||
this.createChannel(namespace);
|
||||
this.channelMap.get(namespace)?.send(message);
|
||||
} catch (err) {
|
||||
error = true;
|
||||
wasError = true;
|
||||
}
|
||||
|
||||
this.sendMessage("shim:session/impl_sendMessage", {
|
||||
messageId
|
||||
, error
|
||||
this.sendMessage({
|
||||
subject: "shim:session/impl_sendMessage"
|
||||
, 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.port
|
||||
, message.data.appId
|
||||
, message.data.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 { Channel, Client } from "castv2";
|
||||
|
||||
import mdns from "mdns";
|
||||
|
||||
import { sendMessage } from "../lib/nativeMessaging";
|
||||
|
||||
import { ReceiverStatus } from "../types";
|
||||
import { Message } from "../messaging";
|
||||
|
||||
import { ReceiverStatus } from "./chromecast/types";
|
||||
import { NS_CONNECTION
|
||||
, NS_HEARTBEAT
|
||||
, NS_RECEIVER } from "./chromecast/Session";
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
import { MediaStatus, ReceiverStatus, ReceiverApplication, SenderMessage }
|
||||
from "./components/chromecast/types";
|
||||
|
||||
import { ReceiverDevice
|
||||
, ReceiverMessage
|
||||
, ReceiverSelectionCast
|
||||
, ReceiverSelectionStop
|
||||
, ReceiverStatus
|
||||
, Volume } from "./types";
|
||||
, ReceiverSelectionStop } from "./types";
|
||||
|
||||
|
||||
type MessageDefinitions = {
|
||||
// Session messages
|
||||
"shim:session/stopped": {}
|
||||
, "shim:session/connected": {
|
||||
sessionId: string
|
||||
, namespaces: Array<{ name: string }>
|
||||
, displayName: string
|
||||
, statusText: string
|
||||
}
|
||||
, "shim:session/updateStatus": { volume: Volume }
|
||||
, "shim:session/sendReceiverMessageResponse": {
|
||||
messageId: string
|
||||
, wasError: boolean
|
||||
}
|
||||
"shim:session/connected": { application: ReceiverApplication }
|
||||
, "shim:session/updateStatus": { status: ReceiverStatus }
|
||||
, "shim:session/stopped": {}
|
||||
, "shim:session/impl_addMessageListener": {
|
||||
namespace: string
|
||||
, data: string
|
||||
, message: string
|
||||
}
|
||||
, "shim:session/impl_sendMessage": {
|
||||
messageId: string
|
||||
, error: boolean
|
||||
, wasError: boolean
|
||||
}
|
||||
, "shim:session/impl_sendReceiverMessage": {
|
||||
messageId: string
|
||||
, wasError: boolean
|
||||
}
|
||||
|
||||
// Bridge session messages
|
||||
@@ -40,11 +35,6 @@ type MessageDefinitions = {
|
||||
, _id: string
|
||||
}
|
||||
, "bridge:session/close": {}
|
||||
, "bridge:session/sendReceiverMessage": {
|
||||
message: ReceiverMessage
|
||||
, messageId: string
|
||||
, _id: string
|
||||
}
|
||||
, "bridge:session/impl_leave": {
|
||||
id: string
|
||||
, _id: string
|
||||
@@ -55,23 +45,19 @@ type MessageDefinitions = {
|
||||
, messageId: string
|
||||
, _id: string
|
||||
}
|
||||
, "bridge:session/impl_sendReceiverMessage": {
|
||||
message: SenderMessage
|
||||
, messageId: string
|
||||
, _id: string
|
||||
}
|
||||
, "bridge:session/impl_addMessageListener": {
|
||||
namespace: string;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
// Media messages
|
||||
, "shim:media/update": {
|
||||
currentTime: number
|
||||
, _lastCurrentTime: number
|
||||
, customData: any
|
||||
, playbackRate: number
|
||||
, playerState: string
|
||||
, repeatMode: string
|
||||
, _volumeLevel: number
|
||||
, _volumeMuted: boolean
|
||||
, media: unknown // MediaInfo
|
||||
, mediaSessionId: number
|
||||
, "shim:media/updateStatus": {
|
||||
status: MediaStatus
|
||||
}
|
||||
, "shim:media/sendMediaMessageResponse": {
|
||||
messageId: string
|
||||
|
||||
@@ -1,60 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
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
|
||||
}
|
||||
import { ReceiverStatus } from "./components/chromecast/types";
|
||||
|
||||
|
||||
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 {
|
||||
App = 1
|
||||
, Tab = 2
|
||||
@@ -86,32 +34,3 @@ export interface ReceiverDevice {
|
||||
port: number;
|
||||
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;
|
||||
Reference in New Issue
Block a user