372 lines
No EOL
13 KiB
TypeScript
372 lines
No EOL
13 KiB
TypeScript
import * as electron from 'electron';
|
|
import * as os from 'os';
|
|
import {resolve} from 'path';
|
|
import * as CiderReceiver from '../base/castreceiver';
|
|
|
|
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 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
|
|
// if (app.cfg.get("audio.enableDLNA")) {
|
|
// let ssdpBrowser2 = new Client();
|
|
// ssdpBrowser2.on('response', (headers, statusCode, rinfo) => {
|
|
// 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 {
|
|
// 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: 'Apple Music Electron',
|
|
// 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://' + getIp() + ':' + server.address().port + '/a.wav', options, function (err, _result) {
|
|
// if (err) throw err;
|
|
// console.log('playing ...');
|
|
// });
|
|
|
|
// } 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) => {
|
|
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;
|
|
});
|
|
}
|
|
|
|
} |