Initial commit

This commit is contained in:
hensm
2018-06-08 04:56:02 +01:00
commit d815fb7af0
70 changed files with 8370 additions and 0 deletions

7
app/caster_bridge.json Executable file
View File

@@ -0,0 +1,7 @@
{
"name": "caster_bridge"
, "description": ""
, "path": ""
, "type": "stdio"
, "allowed_extensions": [ "caster@matt.tf" ]
}

3
app/install.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
# TODO

114
app/package-lock.json generated Normal file
View File

@@ -0,0 +1,114 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"ascli": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/ascli/-/ascli-0.3.0.tgz",
"integrity": "sha1-XmYjDlIZ/j6JUqTvtPIPrllqgTo=",
"requires": {
"colour": "^0.7.1",
"optjs": "^3.2.2"
}
},
"bufferview": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bufferview/-/bufferview-1.0.1.tgz",
"integrity": "sha1-ev10pF+Tf6QiodM4wIu/3HbNcl0="
},
"bytebuffer": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-3.5.5.tgz",
"integrity": "sha1-em+vGhNRSwg/H8+VQcTJv75+f9M=",
"requires": {
"bufferview": "~1",
"long": "~2 >=2.2.3"
}
},
"castv2": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/castv2/-/castv2-0.1.9.tgz",
"integrity": "sha1-0LD6sf0GsNnMpjaIZxbsEpOlkFo=",
"requires": {
"debug": "^2.2.0",
"protobufjs": "^3.2.2"
}
},
"colour": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz",
"integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"dns-js": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/dns-js/-/dns-js-0.2.1.tgz",
"integrity": "sha1-XWZimzwOal6w4U8K5wHQX26kZnM=",
"requires": {
"debug": "^2.1.0",
"qap": "^3.1.2"
}
},
"long": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz",
"integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8="
},
"mdns-js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdns-js/-/mdns-js-1.0.1.tgz",
"integrity": "sha512-dwEtMzmoZCQcGlr004J4m2+W6dCMpCoGQ5kYIEY+7rMPdMM7ztT+1qD9ExmottvLGgbqAVsjllhwU8PyusecPg==",
"requires": {
"debug": "^3.1.0",
"dns-js": "~0.2.1",
"semver": "^5.4.1"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"optjs": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz",
"integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4="
},
"protobufjs": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-3.8.2.tgz",
"integrity": "sha1-vIJuNMOvRpfo0K96Zp5NYSrtzRc=",
"requires": {
"ascli": "~0.3",
"bytebuffer": "~3 >=3.5"
}
},
"qap": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/qap/-/qap-3.3.1.tgz",
"integrity": "sha1-Efno+oiQ/ny5khDA9E0GE7c3LKw="
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
}
}
}

6
app/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"dependencies": {
"castv2": "^0.1.9",
"mdns-js": "^1.0.1"
}
}

2
app/src/launcher.bat Executable file
View File

@@ -0,0 +1,2 @@
@echo off
node main.js

4
app/src/launcher.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
AVAHI_COMPAT_NOWARN=1 node main.js

463
app/src/main.js Executable file
View File

@@ -0,0 +1,463 @@
"use strict";
const { Client } = require("castv2");
const mdns = require("mdns-js");
const transforms = require("./transforms");
const browser = mdns.createBrowser(mdns.tcp("googlecast"));
// Increase listener limit
require('events').EventEmitter.defaultMaxListeners = 50;
// stdin -> stdout
process.stdin
.pipe(transforms.decode)
.pipe(transforms.response(handleMessage))
.pipe(transforms.encode)
.pipe(process.stdout);
/**
* Encode and send a message to the extension.
*/
function sendMessage (message) {
try {
transforms.encode.write(message);
} catch (err) {}
}
/**
* Handle incoming messages from the extension and forward them to the
* appropriate handlers.
*/
async function handleMessage (message) {
if (message.subject.startsWith("bridge:bridgemedia/")) {
Media.messageHandler(message);
return;
}
if (message.subject.startsWith("bridge:bridgesession/")) {
Session.messageHandler(message);
return;
}
switch (message.subject) {
case "bridge:discover":
browser.discover();
break;
}
}
browser.on("update", service => {
if (!service.txt) return;
const txt = service.txt
.reduce((prev, current) => {
const [ key, value ] = current.split("=");
prev[key] = value;
return prev;
}, {});
sendMessage({
subject: "shim:serviceUp"
, data: {
address: service.addresses[0]
, port: service.port
, id: txt.id
, friendlyName: txt.fn
}
})
});
/*
browser.on("serviceUp", service => {
transforms.encode.write({
subject: "shim:serviceUp"
, data: {
address: service.addresses[0]
, port: service.port
, id: service.txtRecord.id
, friendlyName: service.txtRecord.fn
}
});
});
browser.on("serviceDown", service => {
transforms.encode.write({
subject:"shim:serviceDown"
, data: {
address: service.addresses[0]
, port: service.port
, id: service.txtRecord.id
, friendlyName: service.txtRecord.fn
}
});
})*/
const sessionMap = new Map();
class Session {
static messageHandler (message) {
const { _id } = message;
let session;
if (sessionMap.has(_id)) {
session = sessionMap.get(_id);
}
switch (message.subject) {
case "bridge:bridgesession/initialize":
sessionMap.set(_id, new Session(
message.data.address
, message.data.port
, message.data.appId
, message.data.sessionId));
break;
case "bridge:bridgesession/close":
session.close();
break;
case "bridge:bridgesession/impl_addMessageListener":
session._impl_addMessageListener(message.data.namespace);
break;
case "bridge:bridgesession/impl_sendMessage":
session._impl_sendMessage(
message.data.namespace
, message.data.message
, message.data.messageId)
break;
case "bridge:bridgesession/impl_setReceiverMuted":
session._impl_setReceiverMuted(
message.data.muted
, message.data.volumeId);
break;
case "bridge:bridgesession/impl_setReceiverVolumeLevel":
session._impl_setReceiverVolumeLevel(
message.data.newLevel
, message.data.volumeId);
break;
case "bridge:bridgesession/impl_stop":
session._impl_stop(message.data.stopId);
break;
}
}
constructor (host, port, appId, sessionId) {
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
, data
, _id: this._id
});
}
createChannel (namespace) {
if (!this.channelMap.has(namespace)) {
this.channelMap.set(namespace
, this.client.createChannel(
this.clientId, this.transport, namespace, "JSON"));
}
}
close () {
this.clientConnection.send({ type: "CLOSE" });
if (this.transportConnect) {
this.transportConnect.send({ type: "CLOSE" });
}
}
_impl_addMessageListener (namespace) {
this.createChannel(namespace);
this.channelMap.get(namespace).on("message", data => {
this.sendMessage("shim:session/impl_addMessageListener", {
namespace: namespace
, data: JSON.stringify(data)
});
})
}
_impl_sendMessage (namespace, message, messageId) {
let error = false;
try {
this.createChannel(namespace);
this.channelMap.get(namespace).send(message);
} catch (err) {
error = true;
}
this.sendMessage("shim:session/impl_sendMessage", {
messageId
, error
});
}
_impl_setReceiverMuted (muted, volumeId) {
let error = false;
try {
this.clientReceiver.send({
type: "SET_VOLUME"
, volume: { muted }
, requestId: 0
});
} catch (err) {
error = true;
}
this.sendMessage("shim:session/impl_setReceiverMuted", {
volumeId
, error
});
}
_impl_setReceiverVolumeLevel (newLevel, volumeId) {
let error = false;
try {
this.clientReceiver.send({
type: "SET_VOLUME"
, volume: { level: newLevel }
, requestId: 0
})
} catch (err) {
error = true;
}
this.sendMessage("shim:session/impl_setReceiverVolumeLevel", {
volumeId
, error
});
}
_impl_stop (stopId) {
let error = false;
try {
this.clientReceiver.send({
type: "STOP"
, sessionId: this.sessionId
, requestId: 0
});
} catch (err) {
error = true;
}
this.client.close();
clearInterval(this.clientHeartbeatInterval);
this.sendMessage("shim:session/impl_stop", {
stopId
, error
});
}
}
const mediaMap = new Map();
class Media {
static messageHandler (message) {
const { _id } = message;
let media;
if (mediaMap.has(_id)) {
media = mediaMap.get(_id);
}
switch (message.subject) {
case "bridge:bridgemedia/initialize":
mediaMap.set(_id
, new Media(
message.data.sessionId
, message.data.mediaSessionId
, _id
, message.data._internalSessionId));
break;
case "bridge:bridgemedia/sendMediaMessage":
const { messageId } = message.data;
let error = false;
try {
media.channel.send(message.data.message);
} catch (err) {
error = true;
}
media.sendMessage("shim:media/sendMediaMessageResponse", {
messageId
, error
});
break;
default:
return;
}
}
constructor (sessionId, mediaSessionId, _id, _internalSessionId) {
this._id = _id;
this.sessionId = sessionId;
this.mediaSessionId = mediaSessionId;
const namespace = "urn:x-cast:com.google.cast.media";
this.session = sessionMap.get(_internalSessionId);
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
, data
, _id: this._id
});
}
}

107
app/src/transforms.js Executable file
View File

@@ -0,0 +1,107 @@
"use strict";
const { Transform } = require("stream");
/**
* Takes a handler function that implements the transform
* and calls the transform callback.
*/
const response = (handler) => new Transform({
readableObjectMode: true
, writableObjectMode: true
, transform (chunk, encoding, callback) {
Promise.resolve(handler(chunk, callback))
.then(response => {
if (response) {
callback(null, response);
} else {
callback(null);
}
});
}
});
/**
* Takes input, decodes the message string, parses as JSON
* and outputs the parsed result.
*/
const decode = new Transform({
readableObjectMode: true
, transform (chunk, encoding, callback) {
// Setup persistent data
if (!this.hasOwnProperty("buf")
&& !this.hasOwnProperty("message_length")) {
this.buf = new Buffer(0);
this.message_length = null;
}
// Append next chunk to buffer
this.buf = Buffer.concat([ this.buf, chunk ]);
while (true) {
if (this.message_length === null) {
if (this.buf.length >= 4) {
// Read message length
this.message_length = this.buf.readUInt32LE(0);
// Offset buffer
this.buf = this.buf.slice(4);
continue;
}
} else {
if (this.buf.length >= this.message_length) {
const message = JSON.parse(this.buf.slice(
0, this.message_length));
this.push(message);
// Cleanup persistent data
this.buf = this.buf.slice(this.message_length);
this.message_length = null;
// Parse next message
continue;
}
}
// No complete messages left
callback();
break;
}
}
});
/**
* Takes input, encodes the message length and content and
* outputs the encoded result.
*/
const encode = new Transform({
writableObjectMode: true
, transform (chunk, encoding, callback) {
const message_length = new Buffer(4);
const message = new Buffer(JSON.stringify(chunk));
// Write message length
message_length.writeUInt32LE(message.length, 0);
// Output joined message length and content
callback(null, Buffer.concat([message_length, message]));
}
});
module.exports = {
response
, decode
, encode
};

3
app/uninstall.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
# TODO