diff --git a/app/src/Media.js b/app/src/Media.js new file mode 100644 index 0000000..44eff0b --- /dev/null +++ b/app/src/Media.js @@ -0,0 +1,82 @@ +export default class Media { + messageHandler (message) { + switch (message.subject) { + case "bridge:bridgemedia/sendMediaMessage": { + let error = false; + try { + this.channel.send(message.data.message); + } catch (err) { + error = true; + } + + this.sendMessage("shim:media/sendMediaMessageResponse", { + messageId: message.data.messageId + , error + }); + + break; + }; + } + } + + 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 = {}) { + this._sendMessage({ + subject + , data + , _id: this._id + }); + } +} diff --git a/app/src/Session.js b/app/src/Session.js new file mode 100644 index 0000000..d07c9a8 --- /dev/null +++ b/app/src/Session.js @@ -0,0 +1,255 @@ +import { Client } from "castv2"; + +export default class Session { + messageHandler (message) { + switch (message.subject) { + case "bridge:bridgesession/close": + this.close(); + break; + + case "bridge:bridgesession/impl_addMessageListener": + this._impl_addMessageListener(message.data.namespace); + break; + + case "bridge:bridgesession/impl_sendMessage": + this._impl_sendMessage( + message.data.namespace + , message.data.message + , message.data.messageId) + break; + + case "bridge:bridgesession/impl_setReceiverMuted": + this._impl_setReceiverMuted( + message.data.muted + , message.data.volumeId); + break; + + case "bridge:bridgesession/impl_setReceiverVolumeLevel": + this._impl_setReceiverVolumeLevel( + message.data.newLevel + , message.data.volumeId); + break; + + case "bridge:bridgesession/impl_stop": + this._impl_stop(message.data.stopId); + break; + } + } + + 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 = {}) { + this._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 + }); + } +} diff --git a/app/src/main.js b/app/src/main.js index 4c15e0f..9bac810 100755 --- a/app/src/main.js +++ b/app/src/main.js @@ -1,4 +1,3 @@ -import { Client } from "castv2"; import { createBrowser, tcp } from "mdns-js"; import http from "http"; @@ -6,6 +5,9 @@ import fs from "fs"; import path from "path"; import * as transforms from "./transforms"; +import Media from "./Media"; +import Session from "./Session"; + const browser = createBrowser(tcp("googlecast")); @@ -35,20 +37,62 @@ function sendMessage (message) { } catch (err) {} } + +// Existing counterpart Media/Session objects +const existingSessions = new Map(); +const existingMedia = new Map(); + /** - * Handle incoming messages from the extension and forward them to the - * appropriate handlers. + * Handle incoming messages from the extension and forward + * them to the appropriate handlers. + * + * Initializes the counterpart objects and is responsible + * for managing existing ones. */ async function handleMessage (message) { if (message.subject.startsWith("bridge:bridgemedia/")) { - Media.messageHandler(message); + if (existingMedia.has(message._id)) { + // Forward message to instance message handler + existingMedia.get(message._id).messageHandler(message); + } else { + if (message.subject.endsWith("/initialize")) { + // Get Session object media belongs to + const parentSession = existingSessions.get( + message.data._internalSessionId); + + // Create Media + existingMedia.set(message._id, new Media( + message.data.sessionId + , message.data.mediaSessionId + , message._id + , parentSession + , sendMessage)); + } + } + return; } + if (message.subject.startsWith("bridge:bridgesession/")) { - Session.messageHandler(message); + if (existingSessions.has(message._id)) { + // Forward message to instance message handler + existingSessions.get(message._id).messageHandler(message); + } else { + if (message.subject.endsWith("/initialize")) { + // Create Session + existingSessions.set(message._id, new Session( + message.data.address + , message.data.port + , message.data.appId + , message.data.sessionId + , sendMessage)); + } + } + return; } + switch (message.subject) { case "bridge:discover": browser.discover(); @@ -151,372 +195,3 @@ browser.on("serviceDown", service => { } }); })*/ - - -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 - }); - } -}