import dnssd from "dnssd"; import events from "events"; import fs from "fs"; import http from "http"; import mime from "mime-types"; import path from "path"; import Media from "./Media"; import Session from "./Session"; import StatusListener from "./StatusListener"; import * as transforms from "./transforms"; import { ReceiverStatus, MediaStatus } from "./castTypes"; import { Message } from "./types"; import { __applicationName , __applicationVersion } from "../package.json"; // Increase listener limit events.EventEmitter.defaultMaxListeners = 50; const browser = new dnssd.Browser(dnssd.tcp("googlecast")); // Local media server let httpServer: http.Server; process.on("SIGTERM", () => { if (httpServer) { httpServer.close(); } }); // 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: object) { try { transforms.encode.write(message); } catch (err) { console.error("Failed to encode message"); } } interface InitializeOptions { shouldWatchStatus?: boolean; } // Existing counterpart Media/Session objects const existingSessions: Map = new Map(); const existingMedia: Map = new Map(); /** * 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: Message) { if (message.subject.startsWith("bridge:/media/")) { 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:/session/")) { 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 , message._id , sendMessage)); } } return; } switch (message.subject) { case "bridge:/getInfo": { const extensionVersion = message.data; return __applicationVersion; } case "bridge:/initialize": { const options: InitializeOptions = message.data; initialize(options); break; } case "bridge:/startHttpServer": { const { filePath, port } = message.data; httpServer = http.createServer((req, res) => { const { size: fileSize } = fs.statSync(filePath); const { range } = req.headers; const contentType = mime.lookup(filePath) || "video/mp4"; // Partial content HTTP 206 if (range) { const bounds = range.substring(6).split("-"); const start = parseInt(bounds[0]); const end = bounds[1] ? parseInt(bounds[1]) : fileSize - 1; const chunkSize = (end - start) + 1; res.writeHead(206, { "Accept-Ranges": "bytes" , "Content-Range": `bytes ${start}-${end}/${fileSize}` , "Content-Length": chunkSize , "Content-Type": contentType }); fs.createReadStream(filePath, { start, end }).pipe(res); } else { res.writeHead(200, { "Content-Length": fileSize , "Content-Type": contentType }); fs.createReadStream(filePath).pipe(res); } }); httpServer.listen(port, () => { sendMessage({ subject: "mediaCast:/httpServerStarted" }); }); break; } case "bridge:/stopHttpServer": { if (httpServer) { httpServer.close(); } break; } } } function initialize (options: InitializeOptions) { const statusListeners = new Map(); browser.on("serviceUp", (service: dnssd.Service) => { const host = service.addresses[0]; const port = service.port; const id = service.txt.id; if (options.shouldWatchStatus) { const listener = new StatusListener(host, port); listener.on("receiverStatus", (status: ReceiverStatus) => { const receiverStatusMessage: any = { subject: "receiverStatus" , data: { id , status: { volume: { level: status.volume.level , muted: status.volume.muted } } } }; if ("applications" in status) { const application = status.applications[0]; receiverStatusMessage.data.status.application = { displayName: application.displayName , isIdleScreen: application.isIdleScreen , statusText: application.statusText }; } sendMessage(receiverStatusMessage); }); statusListeners.set(id, listener); } sendMessage({ subject: "shim:/serviceUp" , data: { host, port, id , friendlyName: service.txt.fn } }); }); browser.on("serviceDown", (service: dnssd.Service) => { const id = service.txt.id; // De-register status listener if (options.shouldWatchStatus && statusListeners.has(id)) { statusListeners.get(id).deregister(); } sendMessage({ subject: "shim:/serviceDown" , data: { id } }); }); browser.start(); }