Convert app to typescript

This commit is contained in:
hensm
2019-02-25 01:00:33 +00:00
parent db7edb70bb
commit e4dffe0cce
15 changed files with 585 additions and 2226 deletions

View File

@@ -1,5 +1,87 @@
"use strict";
import Session from "./Session";
import { Message
, SendMessageCallback } from "./types";
const MEDIA_NAMESPACE = "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 {
messageHandler (message) {
private sessionId: number;
private mediaSessionId: number;
private _id: string;
private session: Session;
private channel: any;
private _sendMessage: SendMessageCallback;
constructor (sessionId: number
, mediaSessionId: number
, _id: string
, parentSession: Session,
_sendMessage: SendMessageCallback) {
this._id = _id;
this._sendMessage = _sendMessage;
this.sessionId = sessionId;
this.mediaSessionId = mediaSessionId;
this.session = parentSession;
this.session.createChannel(MEDIA_NAMESPACE);
this.channel = this.session.channelMap.get(MEDIA_NAMESPACE);
this.channel.on("message", (data: any) => {
if (data && data.type === "MEDIA_STATUS"
&& data.status && data.status.length > 0) {
const status = data.status[0];
const messageData = {
currentTime: status.currentTime
, _lastCurrentTime: Date.now() / 1000
, customData: status.customData
, _volumeLevel: status.volume.level
, _volumeMuted: status.volume.muted
, playbackRate: status.playbackRate
, playerState: status.playerState
, repeatMode: status.repeatMode
} as UpdateMessageData;
if (status.media) {
messageData.media = status.media;
}
if (status.mediaSessionId) {
messageData.mediaSessionId = status.mediaSessionId;
}
this.sendMessage("shim:/media/update", messageData);
// Update ID
if (status.mediaSessionId) {
this.mediaSessionId = status.mediaSessionId;
}
}
});
}
messageHandler (message: Message) {
switch (message.subject) {
case "bridge:/media/sendMediaMessage": {
let error = false;
@@ -19,60 +101,7 @@ export default class Media {
}
}
constructor (sessionId
, mediaSessionId
, _id
, parentSession,
_sendMessage) {
this._id = _id;
this._sendMessage = _sendMessage;
this.sessionId = sessionId;
this.mediaSessionId = mediaSessionId;
const namespace = "urn:x-cast:com.google.cast.media";
this.session = parentSession;
this.session.createChannel(namespace);
this.channel = this.session.channelMap.get(namespace);
this.channel.on("message", data => {
if (data && data.type === "MEDIA_STATUS"
&& data.status && data.status.length > 0) {
const status = data.status[0];
const messageData = {
currentTime: status.currentTime
, _lastCurrentTime: Date.now() / 1000
, customData: status.customData
, _volumeLevel: status.volume.level
, _volumeMuted: status.volume.muted
, playbackRate: status.playbackRate
, playerState: status.playerState
, repeatMode: status.repeatMode
};
if (status.media) {
messageData.media = status.media;
}
if (status.mediaSessionId) {
messageData.mediaSessionId = status.mediaSessionId;
}
this.sendMessage("shim:/media/update", messageData);
// Update ID
if (status.mediaSessionId) {
this.mediaSessionId = status.mediaSessionId;
}
}
});
}
sendMessage (subject, data = {}) {
sendMessage (subject: string, data: any = {}) {
this._sendMessage({
subject
, data

View File

@@ -1,7 +1,128 @@
import { Client } from "castv2";
"use strict";
import { Client, ClientChannel } from "castv2";
import { Message
, SendMessageCallback } from "./types";
const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
export default class Session {
messageHandler (message) {
private _sendMessage: SendMessageCallback;
private sessionId: number;
private _id: string;
private client: Client;
private clientConnection: ClientChannel;
private clientHeartbeat: ClientChannel;
private clientReceiver: ClientChannel;
private clientHeartbeatIntervalId: NodeJS.Timer;
private isSessionCreated = false;
private clientId: string;
private transportId: string;
private transportConnection: ClientChannel;
private app: any;
public channelMap = new Map<string, ClientChannel>();
constructor (host: string
, port: number
, appId: string
, sessionId: number
, _sendMessage: SendMessageCallback) {
this._sendMessage = _sendMessage;
this.sessionId = sessionId;
this.client = new Client();
this.client.connect({ host, port }, () => {
let transportHeartbeat: ClientChannel;
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
, requestId: 1
});
this.clientReceiver.on("message", (message: any) => {
switch (message.type) {
case "RECEIVER_STATUS": {
this.sendMessage("shim:/session/updateStatus", message.status);
if (!message.status.applications) return;
const receiverApp = message.status.applications[0];
const receiverAppId = receiverApp.appId;
this.app = receiverApp;
if (receiverAppId !== 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
});
}
break;
};
}
});
});
}
messageHandler (message: Message) {
switch (message.subject) {
case "bridge:/session/close":
this.close();
@@ -36,115 +157,7 @@ export default class Session {
}
}
constructor (host, port, appId, sessionId, _sendMessage) {
this._sendMessage = _sendMessage;
this.sessionId = sessionId;
this.clientConnection;
this.clientHeartbeat;
this.clientReceiver;
this.channelMap = new Map();
this.client = new Client();
this.client.connect({ host, port }, () => {
let transportHeartbeat;
this.clientConnection = this.client.createChannel(
"sender-0"
, "receiver-0"
, "urn:x-cast:com.google.cast.tp.connection"
, "JSON");
this.clientHeartbeat = this.client.createChannel(
"sender-0"
, "receiver-0"
, "urn:x-cast:com.google.cast.tp.heartbeat"
, "JSON");
this.clientReceiver = this.client.createChannel(
"sender-0"
, "receiver-0"
, "urn:x-cast:com.google.cast.receiver"
, "JSON");
this.clientConnection.send({ type: "CONNECT" });
this.clientHeartbeat.send({ type: "PING" });
this.clientHeartbeatInterval = setInterval(() => {
if (transportHeartbeat) {
transportHeartbeat.send({ type: "PING" });
}
this.clientHeartbeat.send({ type: "PING" });
}, 5000);
this.clientReceiver.send({
type: "LAUNCH"
, appId
, requestId: 1
});
let sessionCreated = false;
this.clientReceiver.on("message", (data, broadcast) => {
switch (data.type) {
case "RECEIVER_STATUS":
this.sendMessage("shim:/session/updateStatus", data.status);
if (!data.status.applications) return;
const receiverApp = data.status.applications[0];
const receiverAppId = receiverApp.appId;
this.app = receiverApp;
if (receiverAppId !== appId) {
// Close session
this.sendMessage("shim:/session/stopped");
this.client.close();
clearInterval(this.clientHeartbeatInterval);
return;
}
if (!sessionCreated) {
sessionCreated = true;
this.transport = this.app.transportId;
this.transportId = this.app.transportId;
this.clientId = `client-${Math.floor(Math.random() * 10e5)}`;
this.transportConnect = this.client.createChannel(
this.clientId
, this.transport
, "urn:x-cast:com.google.cast.tp.connection"
, "JSON");
this.transportConnect.send({ type: "CONNECT" });
transportHeartbeat = this.client.createChannel(
this.clientId
, this.transport
, "urn:x-cast:com.google.cast.tp.heartbeat"
, "JSON");
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
});
}
break;
}
});
});
}
sendMessage (subject, data = {}) {
sendMessage (subject: string, data: any = {}) {
this._sendMessage({
subject
, data
@@ -152,24 +165,25 @@ export default class Session {
});
}
createChannel (namespace) {
createChannel (namespace: string) {
if (!this.channelMap.has(namespace)) {
this.channelMap.set(namespace
, this.client.createChannel(
this.clientId, this.transport, namespace, "JSON"));
this.clientId, this.transportId, namespace, "JSON"));
}
}
close () {
this.clientConnection.send({ type: "CLOSE" });
if (this.transportConnect) {
this.transportConnect.send({ type: "CLOSE" });
if (this.transportConnection) {
this.transportConnection.send({ type: "CLOSE" });
}
}
_impl_addMessageListener (namespace) {
_impl_addMessageListener (namespace: string) {
this.createChannel(namespace);
this.channelMap.get(namespace).on("message", data => {
this.channelMap.get(namespace).on("message", (data: any) => {
this.sendMessage("shim:/session/impl_addMessageListener", {
namespace: namespace
, data: JSON.stringify(data)
@@ -177,7 +191,7 @@ export default class Session {
})
}
_impl_sendMessage (namespace, message, messageId) {
_impl_sendMessage (namespace: string, message: object, messageId: string) {
let error = false;
try {
@@ -193,7 +207,7 @@ export default class Session {
});
}
_impl_setReceiverMuted (muted, volumeId) {
_impl_setReceiverMuted (muted: boolean, volumeId: string) {
let error = false;
try {
@@ -212,7 +226,7 @@ export default class Session {
});
}
_impl_setReceiverVolumeLevel (newLevel, volumeId) {
_impl_setReceiverVolumeLevel (newLevel: number, volumeId: string) {
let error = false;
try {
@@ -231,7 +245,7 @@ export default class Session {
});
}
_impl_stop (stopId) {
_impl_stop (stopId: string) {
let error = false;
try {
@@ -245,7 +259,8 @@ export default class Session {
}
this.client.close();
clearInterval(this.clientHeartbeatInterval);
clearInterval(this.clientHeartbeatIntervalId);
this.sendMessage("shim:/session/impl_stop", {
stopId

View File

@@ -9,14 +9,16 @@ import * as transforms from "./transforms";
import Media from "./Media";
import Session from "./Session";
import { Message } from "./types";
import { __applicationName
, __applicationVersion } from "../package.json";
const browser = dnssd.Browser(dnssd.tcp("googlecast"));
const browser = new dnssd.Browser(dnssd.tcp("googlecast"));
// Local media server
let httpServer;
let httpServer: http.Server;
process.on("SIGTERM", () => {
if (httpServer) httpServer.close();
@@ -35,7 +37,7 @@ process.stdin
/**
* Encode and send a message to the extension.
*/
function sendMessage (message) {
function sendMessage (message: object) {
try {
transforms.encode.write(message);
} catch (err) {}
@@ -43,8 +45,8 @@ function sendMessage (message) {
// Existing counterpart Media/Session objects
const existingSessions = new Map();
const existingMedia = new Map();
const existingSessions: Map<string, Session> = new Map();
const existingMedia: Map<string, Media> = new Map();
/**
* Handle incoming messages from the extension and forward
@@ -53,7 +55,7 @@ const existingMedia = new Map();
* Initializes the counterpart objects and is responsible
* for managing existing ones.
*/
async function handleMessage (message) {
async function handleMessage (message: Message) {
if (message.subject.startsWith("bridge:/media/")) {
if (existingMedia.has(message._id)) {
// Forward message to instance message handler
@@ -99,13 +101,13 @@ async function handleMessage (message) {
switch (message.subject) {
case "bridge:/getInfo": {
const extensionVersion = message.data;
return __applicationVersion;
};
case "bridge:/startDiscovery":
case "bridge:/startDiscovery": {
browser.start();
break;
};
case "bridge:/startHttpServer": {
const { filePath, port } = message.data;
@@ -155,14 +157,15 @@ async function handleMessage (message) {
break;
};
case "bridge:/stopHttpServer":
case "bridge:/stopHttpServer": {
if (httpServer) httpServer.close();
break;
};
}
}
browser.on("serviceUp", service => {
browser.on("serviceUp", (service: dnssd.Service) => {
transforms.encode.write({
subject: "shim:/serviceUp"
, data: {
@@ -175,7 +178,7 @@ browser.on("serviceUp", service => {
});
});
browser.on("serviceDown", service => {
browser.on("serviceDown", (service: dnssd.Service) => {
transforms.encode.write({
subject:"shim:/serviceDown"
, data: {

View File

@@ -1,19 +1,23 @@
"use strict";
import { Transform } from "stream";
import { Message } from "./types";
interface ResponseHandlerFunction {
(message: Message): Promise<any>
}
/**
* Takes a handler function that implements the transform
* and calls the transform callback.
*/
const response = (handler) => new Transform({
export const response = (handler: ResponseHandlerFunction) => new Transform({
readableObjectMode: true
, writableObjectMode: true
, transform (chunk, encoding, callback) {
Promise.resolve(handler(chunk, callback))
, transform (chunk: Message, encoding, callback) {
Promise.resolve(handler(chunk))
.then(response => {
if (response) {
callback(null, response);
@@ -29,43 +33,45 @@ const response = (handler) => new Transform({
* Takes input, decodes the message string, parses as JSON
* and outputs the parsed result.
*/
const decode = new Transform({
export const decode = new Transform({
readableObjectMode: true
, transform (chunk, encoding, callback) {
// Setup persistent data
if (!this.hasOwnProperty("buf")
&& !this.hasOwnProperty("message_length")) {
const self = this as any;
this.buf = Buffer.alloc(0);
this.message_length = null;
// Setup persistent data
if (!this.hasOwnProperty("_buf")
&& !this.hasOwnProperty("_messageLength")) {
self._buf = Buffer.alloc(0);
self._messageLength = null;
}
// Append next chunk to buffer
this.buf = Buffer.concat([ this.buf, chunk ]);
self._buf = Buffer.concat([ self._buf, chunk ]);
while (true) {
if (this.message_length === null) {
if (this.buf.length >= 4) {
if (self._messageLength === null) {
if (self._buf.length >= 4) {
// Read message length
this.message_length = this.buf.readUInt32LE(0);
self._messageLength = self._buf.readUInt32LE(0);
// Offset buffer
this.buf = this.buf.slice(4);
self._buf = self._buf.slice(4);
continue;
}
} else {
if (this.buf.length >= this.message_length) {
const message = JSON.parse(this.buf.slice(
0, this.message_length));
if (self._buf.length >= self._messageLength) {
const message = JSON.parse(self._buf.slice(
0, self._messageLength));
this.push(message);
// Cleanup persistent data
this.buf = this.buf.slice(this.message_length);
this.message_length = null;
self._buf = self._buf.slice(self._messageLength);
self._messageLength = null;
// Parse next message
continue;
@@ -84,7 +90,7 @@ const decode = new Transform({
* Takes input, encodes the message length and content and
* outputs the encoded result.
*/
const encode = new Transform({
export const encode = new Transform({
writableObjectMode: true
, transform (chunk, encoding, callback) {
@@ -98,10 +104,3 @@ const encode = new Transform({
callback(null, Buffer.concat([message_length, message]));
}
});
export {
response
, decode
, encode
};

11
app/src/types.ts Normal file
View File

@@ -0,0 +1,11 @@
"use strict";
export interface Message {
subject: string;
data: any;
_id?: string;
}
export interface SendMessageCallback {
(message: Message): void
}