353 lines
12 KiB
TypeScript
353 lines
12 KiB
TypeScript
import * as electron from "electron";
|
|
import * as os from "os";
|
|
import { resolve } from "path";
|
|
import * as CiderReceiver from "../base/castreceiver";
|
|
const MediaRendererClient = require("upnp-mediarenderer-client");
|
|
|
|
export default class ChromecastPlugin {
|
|
/**
|
|
* Private variables for interaction in plugins
|
|
*/
|
|
private _win: any;
|
|
private _app: any;
|
|
private _lastfm: any;
|
|
private _store: any;
|
|
private _timer: any;
|
|
private audioClient = require("castv2-client").Client;
|
|
private mdns = require("mdns-js");
|
|
|
|
private devices: any = [];
|
|
private castDevices: any = [];
|
|
|
|
// private GCRunning = false;
|
|
// private GCBuffer: any;
|
|
// private expectedConnections = 0;
|
|
// private currentConnections = 0;
|
|
private activeConnections: any = [];
|
|
// private requests = [];
|
|
// private GCstream = new Stream.PassThrough(),
|
|
private connectedHosts: any = {};
|
|
private connectedPlayer: any;
|
|
private ciderPort: any = 9000;
|
|
private scanCount: any = 0;
|
|
// private server = false;
|
|
// private bufcount = 0;
|
|
// private bufcount2 = 0;
|
|
// private headerSent = false;
|
|
|
|
private searchForGCDevices() {
|
|
try {
|
|
let browser = this.mdns.createBrowser(this.mdns.tcp("googlecast"));
|
|
browser.on("ready", browser.discover);
|
|
|
|
browser.on("update", (service: any) => {
|
|
if (service.addresses && service.fullname && service.fullname.includes("_googlecast._tcp")) {
|
|
let a = service.txt.filter((u: any) => String(u).startsWith("fn="));
|
|
let name = (a[0] ?? "").substring(3) != "" ? (a[0] ?? "").substring(3) : service.fullname.substring(0, service.fullname.indexOf("._googlecast"));
|
|
this.ondeviceup(service.addresses[0], name + " (" + (service.type[0]?.description ?? "") + ")", "", "googlecast");
|
|
}
|
|
});
|
|
const Client = require("node-ssdp").Client;
|
|
// also do a SSDP/UPnP search
|
|
let ssdpBrowser = new Client();
|
|
ssdpBrowser.on("response", (headers: any, statusCode: any, rinfo: any) => {
|
|
var location = getLocation(headers);
|
|
if (location != null) {
|
|
this.getServiceDescription(location, rinfo.address);
|
|
}
|
|
});
|
|
|
|
function getLocation(headers: any) {
|
|
let location = null;
|
|
if (headers["LOCATION"] != null) {
|
|
location = headers["LOCATION"];
|
|
} else if (headers["Location"] != null) {
|
|
location = headers["Location"];
|
|
}
|
|
return location;
|
|
}
|
|
|
|
ssdpBrowser.search("urn:dial-multiscreen-org:device:dial:1");
|
|
|
|
// actual upnp devices
|
|
let ssdpBrowser2 = new Client();
|
|
ssdpBrowser2.on("response", (headers: any, statusCode: any, rinfo: any) => {
|
|
var location = getLocation(headers);
|
|
if (location != null) {
|
|
this.getServiceDescription(location, rinfo.address);
|
|
}
|
|
});
|
|
ssdpBrowser2.search("urn:schemas-upnp-org:device:MediaRenderer:1");
|
|
} catch (e) {
|
|
console.log("Search GC err", e);
|
|
}
|
|
}
|
|
|
|
private getServiceDescription(url: any, address: any) {
|
|
const request = require("request");
|
|
request.get(url, (error: any, response: any, body: any) => {
|
|
if (!error && response.statusCode === 200) {
|
|
this.parseServiceDescription(body, address, url);
|
|
}
|
|
});
|
|
}
|
|
|
|
private ondeviceup(host: any, name: any, location: any, type: any) {
|
|
if (this.castDevices.findIndex((item: any) => item.host === host && item.name === name && item.location === location && item.type === type) === -1) {
|
|
this.castDevices.push({
|
|
name: name,
|
|
host: host,
|
|
location: location,
|
|
type: type,
|
|
});
|
|
if (this.devices.indexOf(host) === -1) {
|
|
this.devices.push(host);
|
|
}
|
|
if (name) {
|
|
this._win.webContents.executeJavaScript(`console.log('deviceFound','ip: ${host} name:${name}')`).catch((err: any) => console.error(err));
|
|
console.log("deviceFound", host, name);
|
|
}
|
|
} else {
|
|
this._win.webContents.executeJavaScript(`console.log('deviceFound (added)','ip: ${host} name:${name}')`).catch((err: any) => console.error(err));
|
|
console.log("deviceFound (added)", host, name);
|
|
}
|
|
}
|
|
|
|
private parseServiceDescription(body: any, address: any, url: any) {
|
|
const parseString = require("xml2js").parseString;
|
|
parseString(body, (err: any, result: any) => {
|
|
if (!err && result && result.root && result.root.device) {
|
|
const device = result.root.device[0];
|
|
console.log("device", device);
|
|
let devicetype = "googlecast";
|
|
console.log();
|
|
if (device.deviceType && device.deviceType.toString() === "urn:schemas-upnp-org:device:MediaRenderer:1") {
|
|
devicetype = "upnp";
|
|
}
|
|
this.ondeviceup(address, device.friendlyName.toString(), url, devicetype);
|
|
}
|
|
});
|
|
}
|
|
|
|
private loadMedia(client: any, song: any, artist: any, album: any, albumart: any, cb?: any) {
|
|
// const u = 'http://' + this.getIp() + ':' + server.address().port + '/';
|
|
// const DefaultMediaReceiver : any = require('castv2-client').DefaultMediaReceiver;
|
|
client.launch(CiderReceiver, (err: any, player: any) => {
|
|
if (err) {
|
|
console.log(err);
|
|
return;
|
|
}
|
|
let media = {
|
|
// Here you can plug an URL to any mp4, webm, mp3 or jpg file with the proper contentType.
|
|
contentId: "http://" + this.getIp() + ":" + this.ciderPort + "/audio.wav",
|
|
contentType: "audio/wav",
|
|
streamType: "LIVE", // or LIVE
|
|
|
|
// Title and cover displayed while buffering
|
|
metadata: {
|
|
type: 0,
|
|
metadataType: 3,
|
|
title: song ?? "",
|
|
albumName: album ?? "",
|
|
artist: artist ?? "",
|
|
images: [{ url: albumart ?? "" }],
|
|
},
|
|
};
|
|
|
|
player.on("status", (status: any) => {
|
|
console.log("status broadcast playerState=%s", status);
|
|
});
|
|
|
|
console.log('app "%s" launched, loading media %s ...', player, media);
|
|
|
|
player.load(
|
|
media,
|
|
{
|
|
autoplay: true,
|
|
},
|
|
(err: any, status: any) => {
|
|
console.log("media loaded playerState=%s", status);
|
|
}
|
|
);
|
|
|
|
client.getStatus((x: any, status: any) => {
|
|
if (status && status.volume) {
|
|
client.volume = status.volume.level;
|
|
client.muted = status.volume.muted;
|
|
client.stepInterval = status.volume.stepInterval;
|
|
}
|
|
});
|
|
|
|
// send websocket ip
|
|
|
|
player.sendIp("ws://" + this.getIp() + ":26369");
|
|
electron.ipcMain.on("stopGCast", (_event) => {
|
|
player.kill();
|
|
});
|
|
electron.app.on("before-quit", (_event) => {
|
|
player.kill();
|
|
});
|
|
});
|
|
}
|
|
|
|
private getIp() {
|
|
let ip: string = "";
|
|
let ip2: any = [];
|
|
let alias = 0;
|
|
const ifaces: any = os.networkInterfaces();
|
|
for (let dev in ifaces) {
|
|
ifaces[dev].forEach((details: any) => {
|
|
if (details.family === "IPv4" && !details.internal) {
|
|
if (!/(loopback|vmware|internal|hamachi|vboxnet|virtualbox)/gi.test(dev + (alias ? ":" + alias : ""))) {
|
|
if (details.address.substring(0, 8) === "192.168." || details.address.substring(0, 7) === "172.16." || details.address.substring(0, 3) === "10.") {
|
|
if (!ip.startsWith("192.168.") || (ip2.startsWith("192.168.") && !ip.startsWith("192.168.") && ip2.startsWith("172.16.") && !ip.startsWith("192.168.") && !ip.startsWith("172.16.")) || (ip2.startsWith("10.") && !ip.startsWith("192.168.") && !ip.startsWith("172.16.") && !ip.startsWith("10."))) {
|
|
ip = details.address;
|
|
}
|
|
++alias;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return ip;
|
|
}
|
|
|
|
private stream(device: any, song: any, artist: any, album: any, albumart: any) {
|
|
let castMode = "googlecast";
|
|
let UPNPDesc = "";
|
|
castMode = device.type;
|
|
UPNPDesc = device.location;
|
|
|
|
let client;
|
|
if (castMode === "googlecast") {
|
|
let client = new this.audioClient();
|
|
client.volume = 100;
|
|
client.stepInterval = 0.5;
|
|
client.muted = false;
|
|
|
|
client.connect(device.host, () => {
|
|
// console.log('connected, launching app ...', 'http://' + this.getIp() + ':' + server.address().port + '/');
|
|
if (!this.connectedHosts[device.host]) {
|
|
this.connectedHosts[device.host] = client;
|
|
this.activeConnections.push(client);
|
|
}
|
|
this.loadMedia(client, song, artist, album, albumart);
|
|
});
|
|
|
|
client.on("close", () => {
|
|
console.info("Client Closed");
|
|
for (let i = this.activeConnections.length - 1; i >= 0; i--) {
|
|
if (this.activeConnections[i] === client) {
|
|
this.activeConnections.splice(i, 1);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
client.on("error", (err: any) => {
|
|
console.log("Error: %s", err.message);
|
|
client.close();
|
|
delete this.connectedHosts[device.host];
|
|
});
|
|
} else {
|
|
// upnp devices
|
|
try {
|
|
let client = new MediaRendererClient(UPNPDesc);
|
|
const options = {
|
|
autoplay: true,
|
|
contentType: "audio/x-wav",
|
|
dlnaFeatures: "DLNA.ORG_PN=-;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000",
|
|
metadata: {
|
|
title: "Cider",
|
|
creator: "Streaming ...",
|
|
type: "audio", // can be 'video', 'audio' or 'image'
|
|
// url: 'http://' + getIp() + ':' + server.address().port + '/',
|
|
// protocolInfo: 'DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000;
|
|
},
|
|
};
|
|
client.load("http://" + this.getIp() + ":" + this.ciderPort + "/audio.wav", options, function (err: any, _result: any) {
|
|
if (err) throw err;
|
|
console.log("playing ...");
|
|
});
|
|
if (!this.connectedHosts[device.host]) {
|
|
this.connectedHosts[device.host] = client;
|
|
this.activeConnections.push(client);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
private async setupGCServer() {
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Base Plugin Details (Eventually implemented into a GUI in settings)
|
|
*/
|
|
public name: string = "Chromecast";
|
|
public description: string = "LastFM plugin for Cider";
|
|
public version: string = "0.0.1";
|
|
public author: string = "vapormusic / Cider Collective";
|
|
|
|
/**
|
|
* Runs on plugin load (Currently run on application start)
|
|
*/
|
|
constructor(utils: { getApp: () => any; getStore: () => any }) {
|
|
this._app = utils.getApp();
|
|
this._store = utils.getStore();
|
|
}
|
|
|
|
/**
|
|
* Runs on app ready
|
|
*/
|
|
onReady(win: any): void {
|
|
this._win = win;
|
|
electron.ipcMain.on("getKnownCastDevices", (event) => {
|
|
event.returnValue = this.castDevices;
|
|
});
|
|
|
|
electron.ipcMain.on("performGCCast", (event, device, song, artist, album, albumart) => {
|
|
// this.setupGCServer().then( () => {
|
|
this._win.webContents.setAudioMuted(true);
|
|
console.log(device);
|
|
this.stream(device, song, artist, album, albumart);
|
|
// })
|
|
});
|
|
|
|
electron.ipcMain.on("getChromeCastDevices", (_event, _data) => {
|
|
if (this.scanCount++ == 2) {
|
|
this.scanCount = 0;
|
|
this.castDevices = [];
|
|
}
|
|
this.searchForGCDevices();
|
|
});
|
|
|
|
electron.ipcMain.on("stopGCast", (_event) => {
|
|
this._win.webContents.setAudioMuted(false);
|
|
this.activeConnections.forEach((client: any) => {
|
|
try {
|
|
client.stop();
|
|
} catch (e) {}
|
|
});
|
|
this.activeConnections = [];
|
|
this.connectedHosts = {};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Runs on app stop
|
|
*/
|
|
onBeforeQuit(): void {}
|
|
|
|
/**
|
|
* Runs on song change
|
|
* @param attributes Music Attributes
|
|
*/
|
|
onNowPlayingItemDidChange(attributes: any): void {}
|
|
|
|
onRendererReady(): void {
|
|
this._win.webContents.executeJavaScript(`ipcRenderer.sendSync('get-port')`).then((result: any) => {
|
|
this.ciderPort = result;
|
|
});
|
|
}
|
|
}
|