diff --git a/package.json b/package.json
index 0d447ef4..dbe9342b 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.17.4",
"adm-zip": "^0.5.9",
+ "castv2-client": "^1.2.0",
"discord-rpc": "^4.0.1",
"ejs": "^3.1.6",
"electron-fetch": "^1.7.4",
@@ -54,6 +55,7 @@
"mpris-service": "^2.1.2",
"music-metadata": "^7.11.4",
"node-gyp": "^8.4.1",
+ "node-ssdp": "^4.0.1",
"qrcode": "^1.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
diff --git a/src/main/base/browserwindow.ts b/src/main/base/browserwindow.ts
index b704a6b4..a237e7ef 100644
--- a/src/main/base/browserwindow.ts
+++ b/src/main/base/browserwindow.ts
@@ -78,6 +78,7 @@ export class BrowserWindow {
"components/lyrics-view",
"components/fullscreen",
"components/miniplayer",
+ "components/castmenu"
],
appRoutes: [
{
diff --git a/src/main/plugins/chromecast.ts b/src/main/plugins/chromecast.ts
new file mode 100644
index 00000000..2441f0e0
--- /dev/null
+++ b/src/main/plugins/chromecast.ts
@@ -0,0 +1,337 @@
+import * as electron from 'electron';
+import * as os from 'os';
+import {resolve} from 'path';
+
+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 port = false;
+ // 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) {
+ this.ondeviceup(service.addresses[0], service.fullname.substring(0, service.fullname.indexOf("._googlecast")) + " " + (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(DefaultMediaReceiver, (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() + ':9000/audio.webm',
+ contentType: 'audio/webm',
+ 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;
+ }
+ })
+
+ });
+ }
+
+ private getIp() {
+ let ip = false;
+ let alias = 0;
+ let ifaces: any = os.networkInterfaces();
+ for (var dev in ifaces) {
+ ifaces[dev].forEach((details:any) => {
+ if (details.family === 'IPv4') {
+ 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.'
+ ) {
+ 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(app: any, store: any) {
+ this._app = app;
+ this._store = store
+
+ }
+
+ /**
+ * 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 = [];
+ this.connectedHosts = {};
+
+ })
+ }
+
+ /**
+ * Runs on app stop
+ */
+ onBeforeQuit(): void {
+
+ }
+
+ /**
+ * Runs on song change
+ * @param attributes Music Attributes
+ */
+ onNowPlayingItemDidChange(attributes: any): void {
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/renderer/audio/audio.js b/src/renderer/audio/audio.js
index 322d776e..99cc43c3 100644
--- a/src/renderer/audio/audio.js
+++ b/src/renderer/audio/audio.js
@@ -9,11 +9,13 @@ var CiderAudio = {
vibrantbassNode: null,
llpw: null,
llpwEnabled: null,
- analogWarmth: null
+ analogWarmth: null,
},
+ ccON: false,
+ mediaRecorder: null,
init: function (cb = function () { }) {
//AudioOutputs.fInit = true;
- searchInt = setInterval(function () {
+ let searchInt = setInterval(function () {
if (document.getElementById("apple-music-player")) {
//AudioOutputs.eqReady = true;
document.getElementById("apple-music-player").crossOrigin = "anonymous";
@@ -138,19 +140,29 @@ var CiderAudio = {
CiderAudio.hierarchical_loading();
},
sendAudio: function (){
- var options = {
- mimeType : 'audio/webm; codecs=opus'
- };
- var destnode = CiderAudio.context.createMediaStreamDestination();
- CiderAudio.audioNodes.gainNode.connect(destnode)
- var mediaRecorder = new MediaRecorder(destnode.stream,options);
- mediaRecorder.start(1);
- mediaRecorder.ondataavailable = function(e) {
- e.data.arrayBuffer().then(buffer => {
- ipcRenderer.send('writeAudio',buffer)
- }
- );
+ if (!CiderAudio.ccON) {
+ CiderAudio.ccON = true
+ let searchInt = setInterval(function () {
+ if (CiderAudio.context != null && CiderAudio.audioNodes.gainNode != null) {
+ var options = {
+ mimeType: 'audio/webm; codecs=opus'
+ };
+ var destnode = CiderAudio.context.createMediaStreamDestination();
+ CiderAudio.audioNodes.gainNode.connect(destnode)
+ var mediaRecorder = new MediaRecorder(destnode.stream, options);
+ mediaRecorder.start(1);
+ mediaRecorder.ondataavailable = function (e) {
+ e.data.arrayBuffer().then(buffer => {
+ ipcRenderer.send('writeAudio', buffer)
+ }
+ );
+ }
+
+ clearInterval(searchInt);
+ }
+ }, 1000);
}
+
},
analogWarmth_h2_3: function (status, hierarchy){
if (status === true) { // 23 Band Adjustment
diff --git a/src/renderer/index.js b/src/renderer/index.js
index d55e9925..65206670 100644
--- a/src/renderer/index.js
+++ b/src/renderer/index.js
@@ -253,6 +253,7 @@ const app = new Vue({
pluginMenu: false,
audioControls: false,
showPlaylist: false,
+ castMenu: false
},
socialBadges: {
badgeMap: {},
@@ -269,7 +270,8 @@ const app = new Vue({
headerItems: {}
}
},
- pauseButtonTimer: null
+ pauseButtonTimer: null,
+ activeCasts: []
},
watch: {
cfg: {
diff --git a/src/renderer/views/app/panels.ejs b/src/renderer/views/app/panels.ejs
index 65768149..78751e87 100644
--- a/src/renderer/views/app/panels.ejs
+++ b/src/renderer/views/app/panels.ejs
@@ -22,6 +22,9 @@
+
+
+
diff --git a/src/renderer/views/app/sidebar.ejs b/src/renderer/views/app/sidebar.ejs
index de511b78..69b66b07 100644
--- a/src/renderer/views/app/sidebar.ejs
+++ b/src/renderer/views/app/sidebar.ejs
@@ -96,6 +96,10 @@
+