diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 4b44af6..f41eb9b 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -46,7 +46,7 @@ Cast SDK API calls are translated into Chromecast protocol messages and sent via | No. | Subject | Origin | Destination | Description | | --: | --------------------------------------------- | ---------- | ----------- | ----------- | | 1 | `shim:/initialized` | background | shim | Sent once bridge has been created. | -| 2 | `bridge:/startDiscovery` | shim | bridge | Starts network discovery. | +| 2 | `bridge:/initialize` | shim | bridge | Starts network discovery. | | 3 | `shim:/serviceUp` | bridge | shim | Sent once a receiver device has been found. | | 4 | `shim:/serviceDown` | bridge | shim | Sent once a receiver device has been lost. | | 5 | `main:/openPopup` | shim | background | Opens the receiver selection popup. | diff --git a/app/src/main.ts b/app/src/main.ts index 45c4dc4..350a9ae 100755 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -10,12 +10,19 @@ import Media from "./Media"; import Session from "./Session"; import * as transforms from "./transforms"; +import { Channel, Client } from "castv2"; + import { Message } from "./types"; import { __applicationName , __applicationVersion } from "../package.json"; +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"; + + // Increase listener limit events.EventEmitter.defaultMaxListeners = 50; @@ -50,6 +57,10 @@ function sendMessage (message: object) { } +interface InitializeOptions { + shouldWatchStatus?: boolean; +} + // Existing counterpart Media/Session objects const existingSessions: Map = new Map(); const existingMedia: Map = new Map(); @@ -111,11 +122,14 @@ async function handleMessage (message: Message) { return __applicationVersion; } - case "bridge:/startDiscovery": { - browser.start(); + case "bridge:/initialize": { + const options: InitializeOptions = message.data; + initialize(options); + break; } + case "bridge:/startHttpServer": { const { filePath, port } = message.data; @@ -173,25 +187,128 @@ async function handleMessage (message: Message) { } } +function initialize (options: InitializeOptions) { + browser.on("serviceUp", (service: dnssd.Service) => { + const address = service.addresses[0]; + const port = service.port; + const id = service.txt.id; -browser.on("serviceUp", (service: dnssd.Service) => { - transforms.encode.write({ - subject: "shim:/serviceUp" - , data: { - address: service.addresses[0] - , port: service.port - , id: service.txt.id - , friendlyName: service.txt.fn - , currentApp: service.txt.rs + if (options.shouldWatchStatus) { + registerStatusListener(address, port, id); } - }); -}); -browser.on("serviceDown", (service: dnssd.Service) => { - transforms.encode.write({ - subject: "shim:/serviceDown" - , data: { - id: service.txt.id - } + transforms.encode.write({ + subject: "shim:/serviceUp" + , data: { + address, port, id + , friendlyName: service.txt.fn + , currentApp: service.txt.rs + } + }); }); -}); + + browser.on("serviceDown", (service: dnssd.Service) => { + const id = service.txt.id; + + if (options.shouldWatchStatus) { + deregisterStatusListener(id); + } + + transforms.encode.write({ + subject: "shim:/serviceDown" + , data: { id } + }); + }); + + browser.start(); +} + + +interface StatusListener { + client: Client; + clientReceiver: Channel; +} + +// Map of client connections +const statusListeners = new Map(); + +/** + * Creates a connection to a receiver device and forwards + * RECEIVER_STATUS updates to the extension. + */ +function registerStatusListener ( + host: string + , port: number + , id: string) { + + const client = new Client(); + + const sourceId = "sender-0"; + const destinationId = "receiver-0"; + + let heartbeatIntervalId: number; + + client.connect({ host, port }, () => { + const clientConnection = client.createChannel( + sourceId, destinationId, NS_CONNECTION, "JSON"); + const clientHeartbeat = client.createChannel( + sourceId, destinationId, NS_HEARTBEAT, "JSON"); + const clientReceiver = client.createChannel( + sourceId, destinationId, NS_RECEIVER, "JSON"); + + + clientReceiver.on("message", data => { + switch (data.type) { + case "CLOSE": { + client.close(); + break; + } + + case "RECEIVER_STATUS": { + // Send update message + transforms.encode.write({ + subject: "main:/receiverStatusUpdate" + , data: { + id + , status: data.status + } + }); + + break; + } + } + }); + + clientConnection.send({ type: "CONNECT" }); + clientHeartbeat.send({ type: "PING" }); + clientReceiver.send({ type: "GET_STATUS", requestId: 1 }); + + heartbeatIntervalId = setInterval(() => { + clientHeartbeat.send({ type: "PING" }); + }); + + statusListeners.set(id, { + client + , clientReceiver + }); + }); + + client.on("close", () => { + clearInterval(heartbeatIntervalId); + }); +} + +/** + * Closes status listener connection for a given receiver + * device. + */ +function deregisterStatusListener (id: string) { + const { client, clientReceiver } = statusListeners.get(id); + + // Cleanup + clientReceiver.send({ type: "CLOSE" }); + client.close(); + + // Remove from map + statusListeners.delete(id); +} diff --git a/diagram.svg b/diagram.svg index ff370ab..0f14954 100644 --- a/diagram.svg +++ b/diagram.svg @@ -1 +1 @@ -backgroundshimbridgepopupruntime.connectNative { application: “fx_cast_bridge” }runtime.connect { name: “shim” }runtime.connect { name: “popup” }tabs.connect { name: “popup” }INCOMING 8) popup:/populateReceiverList 10) popup:/closeOUTGOING 9) shim:/selectReceiver 7) shim:/popupReadyINCOMING 1) shim:/initializedOUTGOING 5) main:/openPopupINCOMING 6) popup:/assignShimSession (shim)Media (shim)SESSION (SHIM) 13) shim:/session/connected 14) shim:/session/updateStatus 22) shim:/session/impl_addMessageListener 23) shim:/session/impl_sendMessage 24) shim:/session/impl_setReceiverMuted 25) shim:/session/impl_setReceiverVolumeLevel 26) shim:/session/impl_stopBRIDGE 2) bridge:/startDiscovery 11) bridge:/session/initialize 12) bridge:/session/close 15) bridge:/session/impl_addMessageListener 16) bridge:/session/impl_sendMessage 17) bridge:/session/impl_setReceiverMuted 18) bridge:/session/impl_setReceiverVolumeLevel 19) bridge:/session/impl_stop 20) bridge:/media/initialize 21) bridge:/media/sendMediaMessage 29) bridge:/startHttpServer 30) bridge:/stopHttpServerMEDIA (SHIM) 27) shim:/media/sendMediaMessageResponse 28) shim:/media/updateSHIM 3) shim:/serviceUp 4) shim:/serviceDownmediaCastmirrorCastMEDIACAST 31) mediaCast:/httpServerStartedoptionsupdaterruntime.connect { name: “updater” }INCOMING 32) updater:/updateDataProxied Messages \ No newline at end of file +backgroundshimbridgepopupruntime.connectNative { application: “fx_cast_bridge” }runtime.connect { name: “shim” }runtime.connect { name: “popup” }tabs.connect { name: “popup” }INCOMING 8) popup:/populateReceiverList 10) popup:/closeOUTGOING 9) shim:/selectReceiver 7) shim:/popupReadyINCOMING 1) shim:/initializedOUTGOING 5) main:/openPopupINCOMING 6) popup:/assignShimSession (shim)Media (shim)SESSION (SHIM) 13) shim:/session/connected 14) shim:/session/updateStatus 22) shim:/session/impl_addMessageListener 23) shim:/session/impl_sendMessage 24) shim:/session/impl_setReceiverMuted 25) shim:/session/impl_setReceiverVolumeLevel 26) shim:/session/impl_stopBRIDGE 2) bridge:/initialize 11) bridge:/session/initialize 12) bridge:/session/close 15) bridge:/session/impl_addMessageListener 16) bridge:/session/impl_sendMessage 17) bridge:/session/impl_setReceiverMuted 18) bridge:/session/impl_setReceiverVolumeLevel 19) bridge:/session/impl_stop 20) bridge:/media/initialize 21) bridge:/media/sendMediaMessage 29) bridge:/startHttpServer 30) bridge:/stopHttpServerMEDIA (SHIM) 27) shim:/media/sendMediaMessageResponse 28) shim:/media/updateSHIM 3) shim:/serviceUp 4) shim:/serviceDownmediaCastmirrorCastMEDIACAST 31) mediaCast:/httpServerStartedoptionsupdaterruntime.connect { name: “updater” }INCOMING 32) updater:/updateDataProxied Messages diff --git a/ext/src/main.ts b/ext/src/main.ts index 74ef7e2..b4a70bc 100755 --- a/ext/src/main.ts +++ b/ext/src/main.ts @@ -539,6 +539,28 @@ browser.runtime.onConnect.addListener(port => { }); +const statusBridge = browser.runtime.connectNative(APPLICATION_NAME); +const receiverStatusMap = new Map(); + +statusBridge.onMessage.addListener((message: Message) => { + switch (message.subject) { + case "main:/receiverStatusUpdate": { + const { id, status } = message.data; + receiverStatusMap.set(id, status); + + break; + } + } +}); + +statusBridge.postMessage({ + subject: "bridge:/initialize" + , data: { + shouldWatchStatus: true + } +}); + + messageRouter.register("mirrorCast", message => { browser.tabs.sendMessage(mirrorCastTabId, message , { frameId: mirrorCastFrameId });