From bf6bf8a67ca10572f0950f17753dfa4291b6fd25 Mon Sep 17 00:00:00 2001 From: angelnu Date: Mon, 19 Dec 2016 22:29:09 +0100 Subject: [PATCH] bugfix - not playing when another chromecast playing same url --- README.md | 17 +- chromecast.js | 4 +- io-package.json | 2 +- lib/chromecastDevice.js | 1869 ++++++++++++++------------- lib/mediaInformation.js | 492 +++---- package.json | 2 +- widgets/chromecast.html | 2 +- widgets/chromecast/js/chromecast.js | 4 +- 8 files changed, 1210 insertions(+), 1182 deletions(-) diff --git a/README.md b/README.md index 62cfcf5..304ebc6 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,29 @@ Features What is missing? ---------------- -* use semaphores to avoid race conditions * add state machine to track states: detected ->connected -> player loader -> playing * add retries: sometimes the Chromecast fails to respond to a request * more testing +How to build +------------ + +1. Checkout from git +2. Install grunt with `npm install -g grunt-cli` +3. Install node.js dependencies: `npm install` +4. Make changes and test them +5. Change version in package.json +6. Check changes with `grunt` +7. git commit & push +8. npm publish Changelog --------- + +### 1.1.1 +* (Vegetto) bugfix - not playing when another chromecast playing same url +* (Vegetto) added additional logs + ### 1.1.0 * (Vegetto) **Added support for playlist m3u, asx and pls files - play first entry** diff --git a/chromecast.js b/chromecast.js index fecb73b..ebbdcb7 100644 --- a/chromecast.js +++ b/chromecast.js @@ -30,7 +30,7 @@ var adapter = utils.adapter('chromecast'); //Own libraries var chromecastScanner = require('./lib/chromecastScanner'); -var ChromecastDevice = require('./lib/chromecastDevice'); +var ChromecastDevice = require('./lib/chromecastDevice')(adapter); // is called when adapter shuts down - callback has to be called under any circumstances! adapter.on('unload', function (callback) { @@ -77,7 +77,7 @@ function main() { var chromecastDevices = {}; chromecastScanner(adapter.config.useSSDP, function (name, address, port) { - chromecastDevices[name] = new ChromecastDevice(adapter, name, address, port); + chromecastDevices[name] = new ChromecastDevice(name, address, port); }, SCAN_INTERVAL, function (name, address, port) { chromecastDevices[name].updateAddress(address, port); diff --git a/io-package.json b/io-package.json index 3b9e289..5247129 100644 --- a/io-package.json +++ b/io-package.json @@ -2,7 +2,7 @@ "common": { "name": "chromecast", "title": "Chromecast Adapter", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "authors": ["Vegetto "], "desc": { diff --git a/lib/chromecastDevice.js b/lib/chromecastDevice.js index 3641f2b..a439730 100755 --- a/lib/chromecastDevice.js +++ b/lib/chromecastDevice.js @@ -2,1015 +2,1016 @@ * ChromecastDevice class */ -var MediaInformation = require('./mediaInformation'); -var Client = require('castv2-client').Client; -var DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver; -var fs = require('fs'); -//if (process.env.NODE_ENV !== 'production'){ -// require('longjohn'); -//} - -var STATUS_QUERY_TIME = 30000; // 30 seconds - -//NOTE: the retries are implemented in this way: -// the first retry will be tried after 1*DELAY_CONNECTION_RETY ms -// the second retriy will be tried after 2*DELAY_CONNECTION_RETY -// -// Therefore it will give up after -// MAX_CONECTIONS_RETRIES * (MAX_CONECTIONS_RETRIES +1) * DELAY_CONNECTION_RETY /2 -// 100 * 101 * 30000 /2 = 42 hours -var DELAY_CONNECTION_RETY = 1000; // 30 seconds -var MAX_CONECTIONS_RETRIES = 100; - -var ChromecastDevice = function (_adapter, _name, _address, _port) { - - var that = this; - - var adapter = _adapter; - var address = _address; - var name = _name.replace(/[.\s]+/g, '_'); - var port = _port; - - adapter.log.info(name + " - Found (Address:" + address + " Port:" + port + ")"); - - //Internal variables - var player; - var currentApplicationObject; - - //Internal status - var connectedClient = false; - var connectingPlayer = false; - var connectedPlayer = false; - - //Information retrieved via ICY - var titleViaIcy = false; - - //Some constants - var NAMESPACE = adapter.namespace + "." + name; - - //Create ioBroker objects - var states = createObjects(); - - //reset status of player states - updatePlayerStatus({}); - - //Create client - var client = new Client(); - - //Connect client - connectClient(); - - adapter.on('stateChange', stateChange); - - //end of constructor - - //Need to be called when the address/port change - that.updateAddress = function (n_address, n_port) { +module.exports = function (_adapter) { + + var adapter = _adapter; + var MediaInformation = require('./mediaInformation')(adapter); + var Client = require('castv2-client').Client; + var DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver; + var fs = require('fs'); + //if (process.env.NODE_ENV !== 'production'){ + // require('longjohn'); + //} + + var STATUS_QUERY_TIME = 30000; // 30 seconds + + //NOTE: the retries are implemented in this way: + // the first retry will be tried after 1*DELAY_CONNECTION_RETY ms + // the second retriy will be tried after 2*DELAY_CONNECTION_RETY + // + // Therefore it will give up after + // MAX_CONECTIONS_RETRIES * (MAX_CONECTIONS_RETRIES +1) * DELAY_CONNECTION_RETY /2 + // 100 * 101 * 30000 /2 = 42 hours + var DELAY_CONNECTION_RETY = 1000; // 30 seconds + var MAX_CONECTIONS_RETRIES = 100; + + var ChromecastDevice = function (_name, _address, _port) { + + var that = this; + + var address = _address; + var name = _name.replace(/[.\s]+/g, '_'); + var port = _port; + + adapter.log.info(name + " - Found (Address:" + address + " Port:" + port + ")"); + + //Internal variables + var player; + var currentApplicationObject; - adapter.log.info(name + " - Updating address: " + n_address + ":" + n_port); + //Internal status + var connectedClient = false; + var connectingPlayer = false; + var connectedPlayer = false; - address = n_address; - port = n_port; - adapter.setState(states.address.name, {val: address, ack: true}); - adapter.setState(states.port.name, {val: port, ack: true}); - - if (connectedClient) - reconnectClient(); - }; + //Information retrieved via ICY + var titleViaIcy = false; - function createObjects() { - - //Create a device object - adapter.setObject(name, { - type: 'device', - common: { - name: name - }, - native: {} - }); - - var CHANNEL_STATUS = name + '.status'; - var channels = { - 'status': { - name: name + '.status', - desc: 'Status channel for Chromecast device' - }, - 'player': { - name: name + '.player', - desc: 'Player channel for Chromecast device' - }, - 'media': { - name: name + '.media', - desc: 'Media channel for Chromecast device' - }, - 'metadata': { - name: name + '.metadata', - desc: 'Metadata channel for Chromecast device' - }, - 'exportedMedia': { - name: name + '.exportedMedia', - desc: 'Media exported via ioBroker web server' - } + //Some constants + var NAMESPACE = adapter.namespace + "." + name; + + //Create ioBroker objects + var states = createObjects(); + + //reset status of player states + updatePlayerStatus({}); + + //Create client + var client = new Client(); + + //Connect client + connectClient(); + + adapter.on('stateChange', stateChange); + + //end of constructor + + //Need to be called when the address/port change + that.updateAddress = function (n_address, n_port) { + + adapter.log.info(name + " - Updating address: " + n_address + ":" + n_port); + + address = n_address; + port = n_port; + adapter.setState(states.address.name, {val: address, ack: true}); + adapter.setState(states.port.name, {val: port, ack: true}); + + if (connectedClient) + reconnectClient(); }; - //Create/update all channel definitions - var k; - for (k in channels) { - adapter.setObject(channels[k].name, { - type: 'channel', - common: channels[k], + function createObjects() { + + //Create a device object + adapter.setObject(name, { + type: 'device', + common: { + name: name + }, native: {} }); - } - var states = { - //Top level - 'address': { - name: name + '.address', - def: address, - type: 'string', - read: true, - write: false, - role: 'address', - desc: 'Address of the Chromecast' - }, - 'port': { - name: name + '.port', - def: address, - type: 'string', - read: true, - write: false, - role: 'port', - desc: 'Port of the Chromecast' - }, - //Status channel - 'connected': { - name: channels.status.name + '.connected', - def: false, - type: 'boolean', - read: true, - write: true, - role: 'status', - desc: 'ioBroker adapter connected to Chromecast. Writing to this state will trigger a disconnect followed by a connect (that might fail).' - }, - 'playing': { - name: channels.status.name + '.playing', - def: false, - type: 'boolean', - read: true, - write: true, - role: 'status', - desc: 'Player loaded. Setting to false stops play.' - }, - 'volume': { - name: channels.status.name + '.volume', - def: 1, - type: 'number', - read: true, - write: true, - role: 'status', - desc: 'volume in %', - min: 0, - max: 100 - }, - 'muted': { - name: channels.status.name + '.muted', - def: false, - type: 'boolean', - read: true, - write: true, - role: 'status', - desc: 'is muted?' - }, - 'isActiveInput': { - name: channels.status.name + '.isActiveInput', - def: true, - type: 'boolean', - read: true, - write: false, - role: 'status', - desc: '(HDMI only) TV is set to use Chromecast as input' - }, - 'isStandBy': { - name: channels.status.name + '.isStandBy', - def: false, - type: 'boolean', - read: true, - write: false, - role: 'status', - desc: '(HDMI only) TV is standby' - }, - 'displayName': { - name: channels.status.name + '.displayName', - def: "", - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'Chromecast player display name' - }, - 'statusText': { - name: channels.status.name + '.text', - def: "", - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'Chromecast player status as text' - }, - //Player channel - 'url2play': { - name: channels.player.name + '.url2play', - def: '', - type: 'string', - read: true, - write: true, - role: 'command', - desc: 'URL that the chomecast should play from' - }, - 'playerState': { - name: channels.player.name + '.playerState', - def: '', - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'Player status' - }, - 'paused': { - name: channels.player.name + '.paused', - def: false, - type: 'boolean', - read: true, - write: true, - role: 'status', - desc: 'is paused?' - }, - 'currentTime': { - name: channels.player.name + '.currentTime', - def: 0, - type: 'number', - read: true, - write: false, - role: 'status', - desc: 'Playing time?', - unit: 's' - }, - 'repeat': { - name: channels.player.name + '.repeatMode', - def: false, - type: 'boolean', - read: true, - write: true, - role: 'status', - desc: 'repeat playing media?' - }, - 'playerVolume': { - name: channels.player.name + '.volume', - def: 1, - type: 'number', - read: true, - write: true, - role: 'status', - min: 0, - max: 100, - desc: 'Player volume in %' - }, - 'playerMuted': { - name: channels.player.name + '.muted', - def: false, - type: 'boolean', - read: true, - write: true, - role: 'status', - desc: 'Player is muted?' - }, - //Media channel - 'streamType': { - name: channels.media.name + '.streamType', - def: '', - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'Type of stream being played - LIVE or BUFFERED' - }, - 'duration': { - name: channels.media.name + '.duration', - def: -1, - type: 'number', - read: true, - write: false, - role: 'status', - unit: 's', - desc: 'Duration of media being played' - }, - 'contentType': { - name: channels.media.name + '.contentType', - def: '', - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'Type of media being played such as audio/mp3' - }, - 'contentId': { - name: channels.media.name + '.contentId', - def: '', - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'id of content being played. Usally the URL.' - }, - //Metadata channel - 'title': { - name: channels.metadata.name + '.title', - def: '', - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'Title' - }, - 'album': { - name: channels.metadata.name + '.album', - def: '', - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'Album' - }, - 'artist': { - name: channels.metadata.name + '.artist', - def: '', - type: 'string', - read: true, - write: false, - role: 'status', - desc: 'Artist' - }, - //Exported media - 'exportedMedia': { - name: channels.exportedMedia.name + '.mp3', - type: 'object', - read: true, - write: false, - role: 'web', - desc: 'Can be accessed from web server under http://ip:8082/state/chromecast.0..exportedMedia.mp3' - } - }; + var CHANNEL_STATUS = name + '.status'; + var channels = { + 'status': { + name: name + '.status', + desc: 'Status channel for Chromecast device' + }, + 'player': { + name: name + '.player', + desc: 'Player channel for Chromecast device' + }, + 'media': { + name: name + '.media', + desc: 'Media channel for Chromecast device' + }, + 'metadata': { + name: name + '.metadata', + desc: 'Metadata channel for Chromecast device' + }, + 'exportedMedia': { + name: name + '.exportedMedia', + desc: 'Media exported via ioBroker web server' + } + }; + + //Create/update all channel definitions + var k; + for (k in channels) { + adapter.setObject(channels[k].name, { + type: 'channel', + common: channels[k], + native: {} + }); + } - //Create/update all state definitions - for (k in states) { - adapter.setObject(states[k].name, { - type: 'state', - common: states[k], - native: {} + var states = { + //Top level + 'address': { + name: name + '.address', + def: address, + type: 'string', + read: true, + write: false, + role: 'address', + desc: 'Address of the Chromecast' + }, + 'port': { + name: name + '.port', + def: address, + type: 'string', + read: true, + write: false, + role: 'port', + desc: 'Port of the Chromecast' + }, + //Status channel + 'connected': { + name: channels.status.name + '.connected', + def: false, + type: 'boolean', + read: true, + write: true, + role: 'status', + desc: 'ioBroker adapter connected to Chromecast. Writing to this state will trigger a disconnect followed by a connect (that might fail).' + }, + 'playing': { + name: channels.status.name + '.playing', + def: false, + type: 'boolean', + read: true, + write: true, + role: 'status', + desc: 'Player loaded. Setting to false stops play.' + }, + 'volume': { + name: channels.status.name + '.volume', + def: 1, + type: 'number', + read: true, + write: true, + role: 'status', + desc: 'volume in %', + min: 0, + max: 100 + }, + 'muted': { + name: channels.status.name + '.muted', + def: false, + type: 'boolean', + read: true, + write: true, + role: 'status', + desc: 'is muted?' + }, + 'isActiveInput': { + name: channels.status.name + '.isActiveInput', + def: true, + type: 'boolean', + read: true, + write: false, + role: 'status', + desc: '(HDMI only) TV is set to use Chromecast as input' + }, + 'isStandBy': { + name: channels.status.name + '.isStandBy', + def: false, + type: 'boolean', + read: true, + write: false, + role: 'status', + desc: '(HDMI only) TV is standby' + }, + 'displayName': { + name: channels.status.name + '.displayName', + def: "", + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'Chromecast player display name' + }, + 'statusText': { + name: channels.status.name + '.text', + def: "", + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'Chromecast player status as text' + }, + //Player channel + 'url2play': { + name: channels.player.name + '.url2play', + def: '', + type: 'string', + read: true, + write: true, + role: 'command', + desc: 'URL that the chomecast should play from' + }, + 'playerState': { + name: channels.player.name + '.playerState', + def: '', + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'Player status' + }, + 'paused': { + name: channels.player.name + '.paused', + def: false, + type: 'boolean', + read: true, + write: true, + role: 'status', + desc: 'is paused?' + }, + 'currentTime': { + name: channels.player.name + '.currentTime', + def: 0, + type: 'number', + read: true, + write: false, + role: 'status', + desc: 'Playing time?', + unit: 's' + }, + 'repeat': { + name: channels.player.name + '.repeatMode', + def: false, + type: 'boolean', + read: true, + write: true, + role: 'status', + desc: 'repeat playing media?' + }, + 'playerVolume': { + name: channels.player.name + '.volume', + def: 1, + type: 'number', + read: true, + write: true, + role: 'status', + min: 0, + max: 100, + desc: 'Player volume in %' + }, + 'playerMuted': { + name: channels.player.name + '.muted', + def: false, + type: 'boolean', + read: true, + write: true, + role: 'status', + desc: 'Player is muted?' + }, + //Media channel + 'streamType': { + name: channels.media.name + '.streamType', + def: '', + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'Type of stream being played - LIVE or BUFFERED' + }, + 'duration': { + name: channels.media.name + '.duration', + def: -1, + type: 'number', + read: true, + write: false, + role: 'status', + unit: 's', + desc: 'Duration of media being played' + }, + 'contentType': { + name: channels.media.name + '.contentType', + def: '', + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'Type of media being played such as audio/mp3' + }, + 'contentId': { + name: channels.media.name + '.contentId', + def: '', + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'id of content being played. Usally the URL.' + }, + //Metadata channel + 'title': { + name: channels.metadata.name + '.title', + def: '', + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'Title' + }, + 'album': { + name: channels.metadata.name + '.album', + def: '', + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'Album' + }, + 'artist': { + name: channels.metadata.name + '.artist', + def: '', + type: 'string', + read: true, + write: false, + role: 'status', + desc: 'Artist' + }, + //Exported media + 'exportedMedia': { + name: channels.exportedMedia.name + '.mp3', + type: 'object', + read: true, + write: false, + role: 'web', + desc: 'Can be accessed from web server under http://ip:8082/state/chromecast.0..exportedMedia.mp3' + } + }; + + //Create/update all state definitions + for (k in states) { + adapter.setObject(states[k].name, { + type: 'state', + common: states[k], + native: {} + }); + } + + //Set some objects + adapter.setState(states.address.name, {val: address, ack: true}); + adapter.setState(states.port.name, {val: port, ack: true}); + adapter.setState(states.connected.name, {val: false, ack: true}); + adapter.setState(states.playing.name, {val: false, ack: true}); + + //Set url2play only if not set already + adapter.getState(states.url2play.name, function (err, state) { + if (!state) { + adapter.setState(states.url2play.name, {val: "http:/example.org/playme.mp3", ack: true}); + } }); - } - //Set some objects - adapter.setState(states.address.name, {val: address, ack: true}); - adapter.setState(states.port.name, {val: port, ack: true}); - adapter.setState(states.connected.name, {val: false, ack: true}); - adapter.setState(states.playing.name, {val: false, ack: true}); + //return States + return states; + + } - //Set url2play only if not set already - adapter.getState(states.url2play.name, function (err, state) { - if (!state) { - adapter.setState(states.url2play.name, {val: "http:/example.org/playme.mp3", ack: true}); - } - }); - - //return States - return states; - - } - - - - /* - * Client methods - */ - - var connectionRetries = 0; - function reconnectClient() { - if (client) { - if (player) { + + /* + * Client methods + */ + + var connectionRetries = 0; + function reconnectClient() { + + if (client) { + if (player) { + try { + detachPlayer(); + } catch (e) { + adapter.log.error(name + " - error detaching player: " + e); + } + } try { - detachPlayer(); + client.removeListener('status', updateStatus); + if (connectedClient) client.close(); } catch (e) { - adapter.log.error(name + " - error detaching player: " + e); + adapter.log.error(name + " - error closing client: " + e); } + client = undefined; + connectedClient = false; } - try { - client.removeListener('status', updateStatus); - if (connectedClient) client.close(); - } catch (e) { - adapter.log.error(name + " - error closing client: " + e); - } - client = undefined; - connectedClient = false; - } - //Set playing and connected status to false - adapter.setState(states.playing.name, {val: false, ack: true}); - adapter.setState(states.connected.name, {val: false, ack: true}); - - //Try to re-connect - with a threshold - connectionRetries++; - if (connectionRetries > MAX_CONECTIONS_RETRIES) { - adapter.log.warn(name + " - Max amount of reconnects reached - stay offline"); - } else { - //Try to reconnect after 5 seconds - setTimeout(function () { - - client = new Client(); - connectClient(); - }, connectionRetries * DELAY_CONNECTION_RETY); + //Set playing and connected status to false + adapter.setState(states.playing.name, {val: false, ack: true}); + adapter.setState(states.connected.name, {val: false, ack: true}); + + //Try to re-connect - with a threshold + connectionRetries++; + if (connectionRetries > MAX_CONECTIONS_RETRIES) { + adapter.log.warn(name + " - Max amount of reconnects reached - stay offline"); + } else { + //Try to reconnect after 5 seconds + setTimeout(function () { + + client = new Client(); + connectClient(); + }, connectionRetries * DELAY_CONNECTION_RETY); + } } - } - - function connectClient() { - - //Register for status updates - client.on('status', updateStatus); - //Register for errors - client.once('close', function (err) { - adapter.log.error(name + " - Client closed: " + JSON.stringify(err)); - //Try to re-connect - reconnectClient(); - }); - - //Register for errors - client.once('error', function (err) { - adapter.log.warn(name + " - Client error: " + JSON.stringify(err)); - //Try to re-connect - reconnectClient(); - }); - - //Connect client - client.connect({host:address, port:port}, function () { - adapter.log.info(name + " - Connected"); - connectionRetries = 0; - connectedClient = true; - adapter.setState(states.connected.name, {val: true, ack: true}); - + function connectClient() { + //Register for status updates - client.getStatus(function (err, status) { - updateStatus(status); + client.on('status', updateStatus); + + //Register for errors + client.once('close', function (err) { + adapter.log.error(name + " - Client closed: " + JSON.stringify(err)); + //Try to re-connect + reconnectClient(); }); - }); - - } - - function updateStatus(status) { - /* - * Example for Chromecast audio (plex) - * {"applications":[{"appId":"9AC194DC", - * "displayName":"Plex", - * "namespaces":[{"name":"urn:x-cast:com.google.cast.media"}, - * {"name":"urn:x-cast:plex"}], - * "sessionId":"EB5AB303-F876-48E7-BF4A-5653A00031EA", - * "statusText":"Plex", - * "transportId":"web-283"}], - * "volume":{"level":0.007843137718737125, - * "muted":false}} - * - * - * Example for video - * {"applications":[{"appId":"E8C28D3C", - * "displayName":"Backdrop", - * "namespaces":[{"name":"urn:x-cast:com.google.cast.sse"}], - * "sessionId":"89967E57-7F4E-4449-A5F0-62A2F4C7AB73", - * "statusText":"","transportId":"web-58"}], - * "isActiveInput":false, - * "isStandBy":false, - * "volume":{"level":1, - * "muted":false}} - * - */ - - adapter.log.debug(name + ' currentApplicationObject ' + JSON.stringify(status)); - - //volume object seems to always be there - adapter.setState(states.volume.name, {val: Math.round(status.volume.level * 100), ack: true}); - adapter.setState(states.muted.name, {val: status.volume.muted, ack: true}); - - //Video Chromecast-only - adapter.setState(states.isActiveInput.name, {val: ("isActiveInput" in status ? status.isActiveInput: true), ack: true}); - adapter.setState(states.isStandBy.name, {val: ("isStandBy" in status ? status.isStandBy : false), ack: true}); - - //if the Chromecast has an application running then try to attach DefaultMediaReceiver - //NOTE: this might fail in case the Chromecast is running a weird player - // It works fine with the TuneIn and Plex applications - if ("applications" in status) { - currentApplicationObject = status.applications[0]; - - //display name and status - adapter.setState(states.displayName.name, {val: ("displayName" in currentApplicationObject ? currentApplicationObject.displayName: ""), ack: true}); - adapter.setState(states.statusText.name, {val: ("statusText" in currentApplicationObject ? currentApplicationObject.statusText: ""), ack: true}); + //Register for errors + client.once('error', function (err) { + adapter.log.warn(name + " - Client error: " + JSON.stringify(err)); + //Try to re-connect + reconnectClient(); + }); + + //Connect client + client.connect({host:address, port:port}, function () { + adapter.log.info(name + " - Connected"); + connectionRetries = 0; + connectedClient = true; + adapter.setState(states.connected.name, {val: true, ack: true}); + + //Register for status updates + client.getStatus(function (err, status) { + updateStatus(status); + }); + + }); + + } - //set playing state to true - adapter.setState(states.playing.name, {val: true, ack: true}); + function updateStatus(status) { + /* + * Example for Chromecast audio (plex) + * {"applications":[{"appId":"9AC194DC", + * "displayName":"Plex", + * "namespaces":[{"name":"urn:x-cast:com.google.cast.media"}, + * {"name":"urn:x-cast:plex"}], + * "sessionId":"EB5AB303-F876-48E7-BF4A-5653A00031EA", + * "statusText":"Plex", + * "transportId":"web-283"}], + * "volume":{"level":0.007843137718737125, + * "muted":false}} + * + * + * Example for video + * {"applications":[{"appId":"E8C28D3C", + * "displayName":"Backdrop", + * "namespaces":[{"name":"urn:x-cast:com.google.cast.sse"}], + * "sessionId":"89967E57-7F4E-4449-A5F0-62A2F4C7AB73", + * "statusText":"","transportId":"web-58"}], + * "isActiveInput":false, + * "isStandBy":false, + * "volume":{"level":1, + * "muted":false}} + * + */ + adapter.log.debug(name + ' currentApplicationObject ' + JSON.stringify(status)); - if (currentApplicationObject.appId == "MultizoneLeader") { - //We cannot connect to the MultizoneLeader since it does not have namespaces nor transportId - //adapter.log.info(name + ' currentApplicationObject ' + JSON.stringify(status)); + //volume object seems to always be there + adapter.setState(states.volume.name, {val: Math.round(status.volume.level * 100), ack: true}); + adapter.setState(states.muted.name, {val: status.volume.muted, ack: true}); + + //Video Chromecast-only + adapter.setState(states.isActiveInput.name, {val: ("isActiveInput" in status ? status.isActiveInput: true), ack: true}); + adapter.setState(states.isStandBy.name, {val: ("isStandBy" in status ? status.isStandBy : false), ack: true}); + + //if the Chromecast has an application running then try to attach DefaultMediaReceiver + //NOTE: this might fail in case the Chromecast is running a weird player + // It works fine with the TuneIn and Plex applications + if ("applications" in status) { + currentApplicationObject = status.applications[0]; + + //display name and status + adapter.setState(states.displayName.name, {val: ("displayName" in currentApplicationObject ? currentApplicationObject.displayName: ""), ack: true}); + adapter.setState(states.statusText.name, {val: ("statusText" in currentApplicationObject ? currentApplicationObject.statusText: ""), ack: true}); + + //set playing state to true + adapter.setState(states.playing.name, {val: true, ack: true}); + - //{'applications':[{'appId':'MultizoneLeader', - // 'displayName':'Default Media Receiver', - // 'isIdleScreen':false, - // 'sessionId':'63533C2D-D0DC-4F9F-BE21-51D09A60F50B', - // 'statusText':'Now Casting: http://192.168.2.3/musica/test.mp3'} - // ], - // 'volume':{'controlType':'attenuation', - // 'level':0.1764705926179886, - // 'muted':false, - // 'stepInterval':0.05000000074505806} - //} - var dummy; + if (currentApplicationObject.appId == "MultizoneLeader") { + //We cannot connect to the MultizoneLeader since it does not have namespaces nor transportId + //adapter.log.info(name + ' currentApplicationObject ' + JSON.stringify(status)); + + //{'applications':[{'appId':'MultizoneLeader', + // 'displayName':'Default Media Receiver', + // 'isIdleScreen':false, + // 'sessionId':'63533C2D-D0DC-4F9F-BE21-51D09A60F50B', + // 'statusText':'Now Casting: http://192.168.2.3/musica/test.mp3'} + // ], + // 'volume':{'controlType':'attenuation', + // 'level':0.1764705926179886, + // 'muted':false, + // 'stepInterval':0.05000000074505806} + //} + var dummy; + } else { + if (!connectedPlayer && !connectingPlayer) joinPlayer(); + } } else { - if (!connectedPlayer && !connectingPlayer) joinPlayer(); + currentApplicationObject = undefined; + detachPlayer(); } - } else { - currentApplicationObject = undefined; - detachPlayer(); } - } - - - - /* - * Player methods - */ - - function joinPlayer() { - if (!connectedClient) { - adapter.log.error(name + " - Cannot join player: client not connected!"); - } else if (connectedPlayer) { - adapter.log.error(name + " - Cannot join player: player already connected!"); - } else if (connectingPlayer) { - adapter.log.error(name + " - Cannot join player: player already connecting!"); - } else { - //We do not have a player object yet - connectingPlayer = true; - client.join(currentApplicationObject, - DefaultMediaReceiver, - function (err, p) { + + + + /* + * Player methods + */ + + function joinPlayer() { + if (!connectedClient) { + adapter.log.error(name + " - Cannot join player: client not connected!"); + } else if (connectedPlayer) { + adapter.log.error(name + " - Cannot join player: player already connected!"); + } else if (connectingPlayer) { + adapter.log.error(name + " - Cannot join player: player already connecting!"); + } else { + //We do not have a player object yet + connectingPlayer = true; + client.join(currentApplicationObject, + DefaultMediaReceiver, + function (err, p) { + connectingPlayer = false; + if (err) { + adapter.log.error(name + ' failed to attach player: ' + err); + } else { + adapter.log.info(name + " - Attached player"); + //We attached fine -> remember player object + player = p; + + //set playing state to true + connectedPlayer = true; + + //Register for close events + player.on("close", detachPlayer); + + //Register for close events + player.on("error", function (err) { + adapter.log.error(name + " - Player - " + err); + detachPlayer(); + }); + + //Register for player status updates + player.on('status', updatePlayerStatus); + player.getStatus(function (err, pStatus) { + if (err) { + adapter.log.error(name + " - Player - " + err); + } else { + updatePlayerStatus(pStatus); + } + }); + } + }); + } + } + + function detachPlayer() { + //Remove player listener if there was one + if (player) { + connectedPlayer = false; connectingPlayer = false; + + //Stop getting media info + MediaInformation.closeListener(name); + + //try to unregister/close player -> this might fail if the player + //was already destroyed + try { + player.removeListener('status', updatePlayerStatus); + player.close(); + + player = undefined; + currentApplicationObject = undefined; + } catch (e) {} + + adapter.log.info(name + " - Detached player"); + + //reset status of player states + updatePlayerStatus({}); + } + adapter.setState(states.playing.name, {val: false, ack: true}); + } + + function launchPlayer(callback) { + if (connectedClient) + adapter.log.debug(name + " - Launching player"); + else { + adapter.log.error(name + " - Cannot launchPlayer: no connection to client"); + return; + } + if (connectingPlayer || connectedPlayer) { + if (DefaultMediaReceiver.APP_ID == currentApplicationObject.appId) { + adapter.log.info(name + " - own player was already loaded"); + callback(); + return; //Our player is already loaded + } + + detachPlayer(); + } + + connectingPlayer = true; + client.launch(DefaultMediaReceiver, function (err, p) { if (err) { - adapter.log.error(name + ' failed to attach player: ' + err); + adapter.log.info(name + ' failed to launch player: ' + err); } else { - adapter.log.info(name + " - Attached player"); - //We attached fine -> remember player object - player = p; - - //set playing state to true + adapter.log.info(name + " - Launched player"); connectedPlayer = true; - - //Register for close events - player.on("close", detachPlayer); - - //Register for close events - player.on("error", function (err) { - adapter.log.error(name + " - Player - " + err); - detachPlayer(); - }); - + //We launched fine -> remember player object + player = p; //Register for player status updates player.on('status', updatePlayerStatus); player.getStatus(function (err, pStatus) { - if (err) { - adapter.log.error(name + " - Player - " + err); - } else { - updatePlayerStatus(pStatus); - } - }); - } + if (err) adapter.log.error(name + " - " + err); + updatePlayerStatus(pStatus); + }); + //set playing state to true + adapter.setState(states.playing.name, {val: true, ack: true}); + callback(); + } }); } - } - function detachPlayer() { - //Remove player listener if there was one - if (player) { - connectedPlayer = false; - connectingPlayer = false; - - //Stop getting media info - MediaInformation.closeListener(name); + var getStatusTimeout; + var cachedPlayerStatus; + function updatePlayerStatus(pStatus) { + /* + * {"mediaSessionId":2, + * "playbackRate":1, + * "playerState":"PLAYING", + * "currentTime":51.304, + * "supportedMediaCommands":15, + * "volume":{"level":1, + * "muted":false}, + * "media":{"contentId":"/library/metadata/8574", + * "streamType":"BUFFERED", + * "contentType":"music", + * "customData":{...}, + * "duration":180.271, + * "metadata":{"metadataType":3, + * "albumName":"Yellow Submarine", + * "title":"Sea Of Time", + * "albumArtist":"The Beatles", + * "artist":"The Beatles", + * "trackNumber":8, + * "discNumber":1}}, + * "currentItemId":2, + * "items":[{"itemId":2, + * "media":{"contentId":"/library/metadata/8574", + * "streamType":"BUFFERED", + * "contentType":"music", + * "customData":{...}, + * "duration":180.271, + * "metadata":{"metadataType":3, + * "albumName":"Yellow Submarine", + * "title":"Sea Of Time", + * "albumArtist":"The Beatles", + * "artist":"The Beatles", + * "trackNumber":8, + * "discNumber":1} + * }, + * "autoplay":true}], + * "repeatMode":"REPEAT_OFF", + * "customData":{...}, + * "idleReason":null} + */ + //adapter.log.info(name + ' - Player status: ' + JSON.stringify(pStatus)); + + //Player channel status + var status = pStatus; + var cachedStatus = cachedPlayerStatus; + if (!status) status = {}; + if (!cachedStatus) cachedStatus = {}; + var playerState = status.playerState ? status.playerState : "STOP"; + var cachedPlayerState = cachedStatus.playerState ? cachedStatus.playerState : "STOP"; + setStateIfChanged(states.playerState.name, + {val: playerState, ack: true}, + cachedPlayerState); + setStateIfChanged(states.currentTime.name, + {val: Math.floor(status.currentTime), ack: true}, + Math.floor(cachedPlayerState.currentTime)); + setStateIfChanged(states.paused.name, + {val: (status.playerState == "PAUSED"), ack: true}, + (cachedStatus.playerState == "PAUSED")); + setStateIfChanged(states.repeat.name, + {val: (status.repeatMode == "REPEAT_ON"), ack: true}, + (cachedStatus.repeatMode == "REPEAT_ON")); + setStateIfChanged(states.playerVolume.name, + {val: Math.round(("volume" in status ? status.volume.level : 1) * 100), ack: true}, + Math.round(("volume" in cachedStatus ? cachedStatus.volume.level : 1) * 100)); + setStateIfChanged(states.playerMuted.name, + {val: ("volume" in status ? status.volume.muted : false), ack: true}, + ("volume" in cachedStatus ? cachedStatus.volume.muted : false)); + + //Media channel status + if (!status.media) status.media = {}; + var media = status.media; + if (!cachedStatus.media) cachedStatus.media = {}; + var cachedMedia = cachedStatus.media; + setStateIfChanged(states.streamType.name, + {val: (media.streamType ? media.streamType : "Unknown"), ack: true}, + (cachedMedia.streamType ? cachedMedia.streamType : "Unknown")); + setStateIfChanged(states.duration.name, + {val: (media.duration ? media.duration : "Unknown"), ack: true}, + (cachedMedia.duration ? cachedMedia.duration : "Unknown")); + setStateIfChanged(states.contentType.name, + {val: (media.contentType ? media.contentType : "Unknown"), ack: true}, + (cachedMedia.contentType ? media.contentType : "Unknown")); + var contentId = (media.contentId ? media.contentId : "Unknown"); + var cachedContentId = (cachedMedia.contentId ? media.contentId : "Unknown"); + setStateIfChanged(states.contentId.name, + {val: contentId, ack: true}, + cachedContentId); - //try to unregister/close player -> this might fail if the player - //was already destroyed - try { - player.removeListener('status', updatePlayerStatus); - player.close(); - - player = undefined; - currentApplicationObject = undefined; - } catch (e) {} + //If contentId starts with http try to get media info + if (playerState != "STOP" && contentId.indexOf("http") === 0) + getMediaInfo(contentId, contentId, "LIVE", function () {}); - adapter.log.info(name + " - Detached player"); + //Metadata channel status + if (!media.metadata) media.metadata = {}; + var metadata = media.metadata; + if (!cachedMedia.metadata) cachedMedia.metadata = {}; + var cachedMetadata = cachedMedia.metadata; + if (!titleViaIcy) + setStateIfChanged(states.title.name, + {val: (metadata.title ? metadata.title : "Unknown"), ack: true}, + (cachedMetadata.title ? cachedMetadata.title : "Unknown")); + setStateIfChanged(states.album.name, + {val: (metadata.albumName ? metadata.albumName : "Unknown"), ack: true}, + (cachedMetadata.albumName ? cachedMetadata.albumName : "Unknown")); + setStateIfChanged(states.artist.name, + {val: (metadata.artist ? metadata.artist : "Unknown"), ack: true}, + (cachedMetadata.artist ? cachedMetadata.artist : "Unknown")); - //reset status of player states - updatePlayerStatus({}); - } - adapter.setState(states.playing.name, {val: false, ack: true}); - } - - function launchPlayer(callback) { - if (connectedClient) - adapter.log.debug(name + " - Launching player"); - else { - adapter.log.error(name + " - Cannot launchPlayer: no connection to client"); - return; - } - if (connectingPlayer || connectedPlayer) { - if (DefaultMediaReceiver.APP_ID == currentApplicationObject.appId) { - adapter.log.info(name + " - own player was already loaded"); - callback(); - return; //Our player is already loaded - } + //Remember last status + cachedPlayerStatus = status; - detachPlayer(); + //Query status if not queried in the last STATUS_QUERY_TIME mseconds + if (getStatusTimeout) clearTimeout(getStatusTimeout); + getStatusTimeout = setTimeout(function () { + if (connectingPlayer || connectedPlayer) { + player.getStatus(function (err, pStatus) { + if (err) { + adapter.log.error(name + " - " + err); + } else { + updatePlayerStatus(pStatus); + } + }); + } + }, STATUS_QUERY_TIME); } - connectingPlayer = true; - client.launch(DefaultMediaReceiver, function (err, p) { - if (err) { - adapter.log.info(name + ' failed to launch player: ' + err); - } else { - adapter.log.info(name + " - Launched player"); - connectedPlayer = true; - //We launched fine -> remember player object - player = p; - //Register for player status updates - player.on('status', updatePlayerStatus); - player.getStatus(function (err, pStatus) { - if (err) adapter.log.error(name + " - " + err); - updatePlayerStatus(pStatus); - }); - //set playing state to true - adapter.setState(states.playing.name, {val: true, ack: true}); - callback(); - } - }); - } + function setStateIfChanged(id, val, oldVal) { + if (oldVal == val.val) + //same value + return; + + adapter.getState(id, function (err, state) { + if (err) { + adapter.log.error(name + ' - Could not get ' + id + ':' + err); + } else { + if (!state) { + adapter.setState(id, val); + } else if (val != state.val) { + adapter.setState(id, val); + } else if ((val.val != state.val) || + (val.ack != state.ack)) { + adapter.setState(id, val); + } else { + adapter.log.debug(name + ' - ' + id + ' value unchanged -> SKIP'); + } + } - var getStatusTimeout; - var cachedPlayerStatus; - function updatePlayerStatus(pStatus) { + }); + } + /* - * {"mediaSessionId":2, - * "playbackRate":1, - * "playerState":"PLAYING", - * "currentTime":51.304, - * "supportedMediaCommands":15, - * "volume":{"level":1, - * "muted":false}, - * "media":{"contentId":"/library/metadata/8574", - * "streamType":"BUFFERED", - * "contentType":"music", - * "customData":{...}, - * "duration":180.271, - * "metadata":{"metadataType":3, - * "albumName":"Yellow Submarine", - * "title":"Sea Of Time", - * "albumArtist":"The Beatles", - * "artist":"The Beatles", - * "trackNumber":8, - * "discNumber":1}}, - * "currentItemId":2, - * "items":[{"itemId":2, - * "media":{"contentId":"/library/metadata/8574", - * "streamType":"BUFFERED", - * "contentType":"music", - * "customData":{...}, - * "duration":180.271, - * "metadata":{"metadataType":3, - * "albumName":"Yellow Submarine", - * "title":"Sea Of Time", - * "albumArtist":"The Beatles", - * "artist":"The Beatles", - * "trackNumber":8, - * "discNumber":1} - * }, - * "autoplay":true}], - * "repeatMode":"REPEAT_OFF", - * "customData":{...}, - * "idleReason":null} + * getMediaInfo functions + * + * Allow to retrieve meta data of servers supporting icecast + * Also find out what type of media is being streamed (audio, video, etc) */ - //adapter.log.info(name + ' - Player status: ' + JSON.stringify(pStatus)); - - //Player channel status - var status = pStatus; - var cachedStatus = cachedPlayerStatus; - if (!status) status = {}; - if (!cachedStatus) cachedStatus = {}; - var playerState = status.playerState ? status.playerState : "STOP"; - var cachedPlayerState = cachedStatus.playerState ? cachedStatus.playerState : "STOP"; - setStateIfChanged(states.playerState.name, - {val: playerState, ack: true}, - cachedPlayerState); - setStateIfChanged(states.currentTime.name, - {val: Math.floor(status.currentTime), ack: true}, - Math.floor(cachedPlayerState.currentTime)); - setStateIfChanged(states.paused.name, - {val: (status.playerState == "PAUSED"), ack: true}, - (cachedStatus.playerState == "PAUSED")); - setStateIfChanged(states.repeat.name, - {val: (status.repeatMode == "REPEAT_ON"), ack: true}, - (cachedStatus.repeatMode == "REPEAT_ON")); - setStateIfChanged(states.playerVolume.name, - {val: Math.round(("volume" in status ? status.volume.level : 1) * 100), ack: true}, - Math.round(("volume" in cachedStatus ? cachedStatus.volume.level : 1) * 100)); - setStateIfChanged(states.playerMuted.name, - {val: ("volume" in status ? status.volume.muted : false), ack: true}, - ("volume" in cachedStatus ? cachedStatus.volume.muted : false)); - - //Media channel status - if (!status.media) status.media = {}; - var media = status.media; - if (!cachedStatus.media) cachedStatus.media = {}; - var cachedMedia = cachedStatus.media; - setStateIfChanged(states.streamType.name, - {val: (media.streamType ? media.streamType : "Unknown"), ack: true}, - (cachedMedia.streamType ? cachedMedia.streamType : "Unknown")); - setStateIfChanged(states.duration.name, - {val: (media.duration ? media.duration : "Unknown"), ack: true}, - (cachedMedia.duration ? cachedMedia.duration : "Unknown")); - setStateIfChanged(states.contentType.name, - {val: (media.contentType ? media.contentType : "Unknown"), ack: true}, - (cachedMedia.contentType ? media.contentType : "Unknown")); - var contentId = (media.contentId ? media.contentId : "Unknown"); - var cachedContentId = (cachedMedia.contentId ? media.contentId : "Unknown"); - setStateIfChanged(states.contentId.name, - {val: contentId, ack: true}, - cachedContentId); - //If contentId starts with http try to get media info - if (playerState != "STOP" && contentId.indexOf("http") === 0) - getMediaInfo(contentId, contentId, "LIVE", function () {}); + function getMediaInfo(url2play, org_url2play, streamType, callback) { + //get connection + connection = MediaInformation.getListener(name, url2play, org_url2play, streamType, callback); + + //For all media updates update iobroker state + connection.removeListener("media", getMediaInfoUpdate); + connection.on("media", getMediaInfoUpdate); + } - //Metadata channel status - if (!media.metadata) media.metadata = {}; - var metadata = media.metadata; - if (!cachedMedia.metadata) cachedMedia.metadata = {}; - var cachedMetadata = cachedMedia.metadata; - if (!titleViaIcy) - setStateIfChanged(states.title.name, - {val: (metadata.title ? metadata.title : "Unknown"), ack: true}, - (cachedMetadata.title ? cachedMetadata.title : "Unknown")); - setStateIfChanged(states.album.name, - {val: (metadata.albumName ? metadata.albumName : "Unknown"), ack: true}, - (cachedMetadata.albumName ? cachedMetadata.albumName : "Unknown")); - setStateIfChanged(states.artist.name, - {val: (metadata.artist ? metadata.artist : "Unknown"), ack: true}, - (cachedMetadata.artist ? cachedMetadata.artist : "Unknown")); + function getMediaInfoUpdate(media) { + //TBD: I would like to send the new metadata to the Chromecast + //but I do not know how without a new load which then interrupts + //the play... + if (media.metadata.title) { + titleViaIcy = true; + setStateIfChanged(states.title.name, {val: media.metadata.title, ack: true}); + } + } - //Remember last status - cachedPlayerStatus = status; - - //Query status if not queried in the last STATUS_QUERY_TIME mseconds - if (getStatusTimeout) clearTimeout(getStatusTimeout); - getStatusTimeout = setTimeout(function () { - if (connectingPlayer || connectedPlayer) { - player.getStatus(function (err, pStatus) { - if (err) { - adapter.log.error(name + " - " + err); - } else { - updatePlayerStatus(pStatus); - } - }); - } - }, STATUS_QUERY_TIME); - } - - function setStateIfChanged(id, val, oldVal) { - if (oldVal == val.val) - //same value - return; - adapter.getState(id, function (err, state) { - if (err) { - adapter.log.error(name + ' - Could not get ' + id + ':' + err); - } else { - if (!state) { - adapter.setState(id, val); - } else if (val != state.val) { - adapter.setState(id, val); - } else if ((val.val != state.val) || - (val.ack != state.ack)) { - adapter.setState(id, val); - } else { - adapter.log.debug(name + ' - ' + id + ' value unchanged -> SKIP'); - } - } - - }); - } - - /* - * getMediaInfo functions - * - * Allow to retrieve meta data of servers supporting icecast - * Also find out what type of media is being streamed (audio, video, etc) - */ - - function getMediaInfo(url2play, org_url2play, streamType, callback) { - //get connection - connection = MediaInformation.getListener(name, url2play, org_url2play, streamType, callback); - - //For all media updates update iobroker state - connection.removeListener("media", getMediaInfoUpdate); - connection.on("media", getMediaInfoUpdate); - } - - function getMediaInfoUpdate(media) { - //TBD: I would like to send the new metadata to the Chromecast - //but I do not know how without a new load which then interrupts - //the play... - if (media.metadata.title) { - titleViaIcy = true; - setStateIfChanged(states.title.name, {val: media.metadata.title, ack: true}); - } - } - - - - function playURL(url2play, org_url2play, streamType) { - if (connectedClient) { - if (org_url2play === undefined) - org_url2play = url2play; - - //Assume live stream by default - if (streamType === undefined) - streamType = 'LIVE'; - - if (url2play.indexOf("http") !== 0) { - //Not an http(s) URL -> assume local file - adapter.log.info("Not a http(s) URL -> asume local file"); + + function playURL(url2play, org_url2play, streamType) { + if (connectedClient) { + if (org_url2play === undefined) + org_url2play = url2play; - //Check that the webserver has been configured - if (adapter.config.webServer === "") { - adapter.log.error(name + '- Sorry, cannot play file "' + url2play + '"'); - adapter.log.error(name + '- Please configure webserver settings first!'); + //Assume live stream by default + if (streamType === undefined) + streamType = 'LIVE'; + + if (url2play.indexOf("http") !== 0) { + //Not an http(s) URL -> assume local file + adapter.log.info("Not a http(s) URL -> asume local file"); + + //Check that the webserver has been configured + if (adapter.config.webServer === "") { + adapter.log.error(name + '- Sorry, cannot play file "' + url2play + '"'); + adapter.log.error(name + '- Please configure webserver settings first!'); + return; + } + + var exported_file_state = adapter.namespace + "." + states.exportedMedia.name; + //Try to load in a local state + try { + adapter.setBinaryState(exported_file_state, fs.readFileSync(url2play), function (err) { + if (err) { + adapter.log.error(name + ' - Cannot store file "' + url2play + ' into ' + exported_file_state + '": ' + e.toString()); + return; + } + + //Calculate the exported URL + url2play = 'http://' + adapter.config.webServer + ':8082/state/' + exported_file_state; + adapter.log.info("Exported as " + url2play); + playURL(url2play, org_url2play, 'BUFFERED'); + }); + } catch (e) { + adapter.log.error(name + ' - Cannot play file "' + url2play + '": ' + e.toString()); + } return; } - var exported_file_state = adapter.namespace + "." + states.exportedMedia.name; - //Try to load in a local state - try { - adapter.setBinaryState(exported_file_state, fs.readFileSync(url2play), function (err) { - if (err) { - adapter.log.error(name + ' - Cannot store file "' + url2play + ' into ' + exported_file_state + '": ' + e.toString()); - return; - } - - //Calculate the exported URL - url2play = 'http://' + adapter.config.webServer + ':8082/state/' + exported_file_state; - adapter.log.info("Exported as " + url2play); - playURL(url2play, org_url2play, 'BUFFERED'); - }); - } catch (e) { - adapter.log.error(name + ' - Cannot play file "' + url2play + '": ' + e.toString()); - } - return; + //get media info + getMediaInfo(url2play, org_url2play, streamType, function (mediaInfo) { + //launch player + launchPlayer(function () { + //load media + loadMedia(mediaInfo); + }); + }); + + } else { + adapter.log.error(name + ' - cannot play URL: disconnected from Chromecast'); } - //get media info - getMediaInfo(url2play, org_url2play, streamType, function (mediaInfo) { - //launch player - launchPlayer(function () { - //load media - loadMedia(mediaInfo); - }); - }); - } else { - adapter.log.error(name + ' - cannot play URL: disconnected from Chromecast'); - } - - - function loadMedia(media) { - player.load(media, {autoplay: true}, function (err, status) { - if (err) { - adapter.log.error(name + ' - media loadMedia err: ' + err.toString()); - adapter.log.error(JSON.stringify(media)); - detachPlayer(); - } else { - adapter.log.info(name + " - Playing " + org_url2play); - //ACK after we successfully started playing - adapter.setState(states.url2play.name, {val: org_url2play, ack: true}); - } - }); - } - } - - // is called if a subscribed state changes - function stateChange(id, state) { - if ((id.indexOf(NAMESPACE) === 0) && - state && - (state.from.indexOf(adapter.namespace) < 0) - ) { - // Warning, state can be null if it was deleted - adapter.log.debug(name + ' - device stateChange ' + id + ' ' + JSON.stringify(state)); - - // you can use the ack flag to detect if it is status (true) or command (false) - if (state && !state.ack) { - - //Is connected? - if (id.indexOf(adapter.namespace + "." + states.connected.name) === 0) { - adapter.log.warn(name + ' - reconnecting as requested by ' + state.from); - connectionRetries = 0; - reconnectClient(); - } - //Is volume? - else if (id.indexOf(adapter.namespace + "." + states.volume.name) === 0) { - if (connectedClient) { - client.setVolume({level: (state.val / 100)}, function (err, volume) { - if (err) adapter.log.error(name + " - " + err); - //ACK written when status update sent by Chromecast - }); + function loadMedia(media) { + player.load(media, {autoplay: true}, function (err, status) { + if (err) { + adapter.log.error(name + ' - media loadMedia err: ' + err.toString()); + adapter.log.error(JSON.stringify(media)); + detachPlayer(); } else { - adapter.log.error(name + ' - cannot set volume: disconnected from Chromecast'); + adapter.log.info(name + " - Playing " + org_url2play); + //ACK after we successfully started playing + adapter.setState(states.url2play.name, {val: org_url2play, ack: true}); } - } - //Is muted? - else if (id.indexOf(adapter.namespace + "." + states.muted.name) === 0) { - if (connectedClient) { - client.setVolume({muted: state.val}, function (err, volume) { + }); + } + } - if (err) adapter.log.error(name + " - " + err); - //ACK written when status update sent by Chromecast - }); - } else { - adapter.log.error(name + ' - cannot (un)mute: disconnected from Chromecast'); + // is called if a subscribed state changes + function stateChange(id, state) { + if ((id.indexOf(NAMESPACE) === 0) && + state && + (state.from.indexOf(adapter.namespace) < 0) + ) { + // Warning, state can be null if it was deleted + adapter.log.debug(name + ' - device stateChange ' + id + ' ' + JSON.stringify(state)); + + // you can use the ack flag to detect if it is status (true) or command (false) + if (state && !state.ack) { + + //Is connected? + if (id.indexOf(adapter.namespace + "." + states.connected.name) === 0) { + adapter.log.warn(name + ' - reconnecting as requested by ' + state.from); + connectionRetries = 0; + reconnectClient(); } - } - //Is playing? - else if (id.indexOf(adapter.namespace + "." + states.playing.name) === 0) { - if (connectedClient) { - if (state.val) { - //Try to play last contentID - adapter.getState(states.contentId.name, function (err, state) { - if (state && state.val && state.val.startsWith("http")) { - playURL(state.val); - } else { - //Try to play last url2play - adapter.getState(states.url2play.name, function (err, state) { - if (state && state.val && state.val.startsWith("http")) { - playURL(state.val); - } else { - //Could not find a valid link to play -> set to false again - adapter.setState(id, false); - } - }); - } + //Is volume? + else if (id.indexOf(adapter.namespace + "." + states.volume.name) === 0) { + if (connectedClient) { + client.setVolume({level: (state.val / 100)}, function (err, volume) { + if (err) adapter.log.error(name + " - " + err); + //ACK written when status update sent by Chromecast }); - } else if (connectedPlayer) { - //Disconnect client - client.stop(player, function (err) { + } else { + adapter.log.error(name + ' - cannot set volume: disconnected from Chromecast'); + } + } + //Is muted? + else if (id.indexOf(adapter.namespace + "." + states.muted.name) === 0) { + if (connectedClient) { + client.setVolume({muted: state.val}, function (err, volume) { + if (err) adapter.log.error(name + " - " + err); //ACK written when status update sent by Chromecast }); + } else { + adapter.log.error(name + ' - cannot (un)mute: disconnected from Chromecast'); } - } else { - adapter.log.error(name + ' - cannot play/stop: disconnected from Chromecast'); } - } - //Is paused? - else if (id.indexOf(adapter.namespace + "." + states.paused.name) === 0) { - if (connectedPlayer) { - if (state.val) { - player.pause(function () {}); + //Is playing? + else if (id.indexOf(adapter.namespace + "." + states.playing.name) === 0) { + if (connectedClient) { + if (state.val) { + //Try to play last contentID + adapter.getState(states.contentId.name, function (err, state) { + if (state && state.val && state.val.startsWith("http")) { + playURL(state.val); + } else { + //Try to play last url2play + adapter.getState(states.url2play.name, function (err, state) { + if (state && state.val && state.val.startsWith("http")) { + playURL(state.val); + } else { + //Could not find a valid link to play -> set to false again + adapter.setState(id, false); + } + }); + } + }); + } else if (connectedPlayer) { + //Disconnect client + client.stop(player, function (err) { + if (err) adapter.log.error(name + " - " + err); + //ACK written when status update sent by Chromecast + }); + } + } else { + adapter.log.error(name + ' - cannot play/stop: disconnected from Chromecast'); + } + } + //Is paused? + else if (id.indexOf(adapter.namespace + "." + states.paused.name) === 0) { + if (connectedPlayer) { + if (state.val) { + player.pause(function () {}); + } else { + player.play(function () {}); + } + //ACK written when status update sent by Chromecast } else { - player.play(function () {}); + adapter.log.error(name + ' - cannot pause: Chromecast not playing'); + adapter.setState(id, false); } - //ACK written when status update sent by Chromecast + } + //Is url2play? + else if (id.indexOf(adapter.namespace + "." + states.url2play.name) === 0) { + playURL(state.val); } else { - adapter.log.error(name + ' - cannot pause: Chromecast not playing'); - adapter.setState(id, false); + adapter.log.error(name + ' - Sorry, update for ' + id + ' not supported!'); } } - //Is url2play? - else if (id.indexOf(adapter.namespace + "." + states.url2play.name) === 0) { - playURL(state.val); - } else { - adapter.log.error(name + ' - Sorry, update for ' + id + ' not supported!'); - } } } - } -}; - - + }; -module.exports = ChromecastDevice; + return ChromecastDevice; +}; diff --git a/lib/mediaInformation.js b/lib/mediaInformation.js index dc985ca..d9190be 100755 --- a/lib/mediaInformation.js +++ b/lib/mediaInformation.js @@ -5,267 +5,279 @@ * Also find out what type of media is being streamed (audio, video, etc) */ -var EventEmitter = require('events').EventEmitter; -var util = require('util'); -var icy = require('icy'); -var devnull = require('dev-null'); -var playlist_parsers = require("playlist-parser"); -var request_lib = require('request'); -//if (process.env.NODE_ENV !== 'production'){ -// require('longjohn'); -//} - -var DISCOVER_ICY_METADATA = true; - -var Connection = function (url2play, org_url2play, streamType, callback) { - that = this; - EventEmitter.call(that); - that._url2play = url2play; - that._callback = callback; - that._media = { - // Here you can plug an URL to any mp4, webm, mp3 or jpg file with the proper contentType. - contentId: url2play, - streamType: streamType, // LIVE or BUFFERED - - // Title and cover displayed while buffering - metadata: { - type: 0, - metadataType: 0, - title: org_url2play - } +module.exports = function (_adapter) { + var adapter = _adapter; + + var EventEmitter = require('events').EventEmitter; + var util = require('util'); + var icy = require('icy'); + var devnull = require('dev-null'); + var playlist_parsers = require("playlist-parser"); + var request_lib = require('request'); + //if (process.env.NODE_ENV !== 'production'){ + // require('longjohn'); + //} + + var DISCOVER_ICY_METADATA = true; + + var Connection = function (url2play, org_url2play, streamType, callback) { + that = this; + EventEmitter.call(that); + that._url2play = url2play; + that._callback = callback; + that._media = { + // Here you can plug an URL to any mp4, webm, mp3 or jpg file with the proper contentType. + contentId: url2play, + streamType: streamType, // LIVE or BUFFERED + + // Title and cover displayed while buffering + metadata: { + type: 0, + metadataType: 0, + title: org_url2play + } + }; + + //See if we have to run in dummy mode + if (!DISCOVER_ICY_METADATA) { + callback(that._media); + return; + } + + that._pipeOn = false; + + try { + that._requestObject = icy.get(url2play, that._connected.bind(that)); + that._requestObject.on('error', function (e) { + adapter.log.error("MediaInformation - ICY connection error for " + that._url2play + ": " + e); + }); + } catch (e) { + adapter.log.error("MediaInformation - Cannot connect to " + that._url2play + ": " + e); + } }; - - //See if we have to run in dummy mode - if (!DISCOVER_ICY_METADATA) { - callback(that._media); - return; - } - - that._pipeOn = false; - - try { - that._requestObject = icy.get(url2play, that._connected.bind(that)); - that._requestObject.on('error', function (e) { - console.log("MediaInformation - ICY connection error for " + that._url2play + ": " + e); - }); - } catch (e) { - console.log("MediaInformation - Cannot connect to " + that._url2play + ": " + e); - } -}; -util.inherits(Connection, EventEmitter); - -Connection.prototype.getMedia = function () { - return that._media; -}; - -Connection.prototype.close = function () { - if (that._requestObject) { - that._requestObject.abort(); - } -}; + util.inherits(Connection, EventEmitter); + Connection.prototype.getMedia = function () { + return that._media; + }; -Connection.prototype._connected = function (res) { - console.log("MediaInformation - Connected to " + that._url2play); - /* - * Example from http://edge.live.mp3.mdn.newmedia.nacamar.net/ps-dieneue_rock/livestream_hi.mp3 - * - * {'accept-ranges': 'none', - * 'content-type': 'audio/mpeg', - * 'icy-br': '128', - * 'ice-audio-info': 'ice-samplerate=44100;ice-bitrate=128;ice-channels=2', - * 'icy-description': 'BESTER ROCK UND POP', - * 'icy-genre': 'Rock', - * 'icy-name': 'DIE NEUE 107.7', - * 'icy-pub': '1', - * 'icy-url': 'http://www.dieneue1077.de', - * server: 'Icecast 2.3.3-kh11', - * 'cache-control': 'no-cache, no-store', - * pragma: 'no-cache', - * 'access-control-allow-origin': '*', - * 'access-control-allow-headers': 'Origin, Accept, X-Requested-With, Content-Type', - * 'access-control-allow-methods': 'GET, OPTIONS, HEAD', - * connection: 'close', - * expires: 'Mon, 26 Jul 1997 05:00:00 GMT', - * 'icy-metaint': '16000' } - */ - // log the HTTP response headers - console.log(res.headers); - - //Set content-type - if (res.headers && res.headers["content-type"]) { - - var contentType = res.headers["content-type"]; - - var parser; - if (contentType.indexOf("audio/x-mpegurl") >= 0) { - //This is a M3U playlist - parser = playlist_parsers.M3U; - } else if (contentType.indexOf("audio/x-scpls") >= 0) { - //This is a PLS playlist - parser = playlist_parsers.PLS; - } else if ((contentType.indexOf("video/x-ms-asf") >= 0) || - (contentType.indexOf("video/x-ms-asx") >= 0)) { - //This is a ASX playlist - parser = playlist_parsers.ASX; + Connection.prototype.close = function () { + if (that._requestObject) { + that._requestObject.abort(); } + }; - if (parser) { - //This is a playlist. Close the stream and get the playlist file - that.close(); - - request_lib.get(that._url2play, function (error, response, body) { - if (!error && response.statusCode == 200) { - var playlist = parser.parse(body); - that._media.dump_body = body; - that._media.dump_playlist = playlist; - - if (playlist.length > 0) { - - //Play the first element on the list - that._url2play = playlist[0].file; - - //Set the default metadata again - that._media.contentId = that._url2play; - that._media.metadata.title = playlist[0].title ? playlist[0].title : playlist[0].file; - that._media.metadata.artist = playlist[0].artist; - that._media.metadata.length = playlist[0].lenght; - - try { - that._requestObject = icy.get(that._url2play, that._connected.bind(that)); - that._requestObject.on('error', function (e) { - that._media.ERROR = "MediaInformation - ICY connection error for " + that._url2play + ": " + e; - that._callback(that._media); - }); - - //Everything went fine -> let us return and wait as done on the constructor - return; - } catch (e) { - that._media.ERROR = "MediaInformation - Cannot connect to " + that._url2play + ": " + e; - } + Connection.prototype._connected = function (res) { + adapter.log.info("MediaInformation - Connected to " + that._url2play); + /* + * Example from http://edge.live.mp3.mdn.newmedia.nacamar.net/ps-dieneue_rock/livestream_hi.mp3 + * + * {'accept-ranges': 'none', + * 'content-type': 'audio/mpeg', + * 'icy-br': '128', + * 'ice-audio-info': 'ice-samplerate=44100;ice-bitrate=128;ice-channels=2', + * 'icy-description': 'BESTER ROCK UND POP', + * 'icy-genre': 'Rock', + * 'icy-name': 'DIE NEUE 107.7', + * 'icy-pub': '1', + * 'icy-url': 'http://www.dieneue1077.de', + * server: 'Icecast 2.3.3-kh11', + * 'cache-control': 'no-cache, no-store', + * pragma: 'no-cache', + * 'access-control-allow-origin': '*', + * 'access-control-allow-headers': 'Origin, Accept, X-Requested-With, Content-Type', + * 'access-control-allow-methods': 'GET, OPTIONS, HEAD', + * connection: 'close', + * expires: 'Mon, 26 Jul 1997 05:00:00 GMT', + * 'icy-metaint': '16000' } + */ + // log the HTTP response headers + adapter.log.debug(JSON.stringify(res.headers)); + + //Set content-type + if (res.headers && res.headers["content-type"]) { + + var contentType = res.headers["content-type"]; + + var parser; + if (contentType.indexOf("audio/x-mpegurl") >= 0) { + //This is a M3U playlist + parser = playlist_parsers.M3U; + } else if (contentType.indexOf("audio/x-scpls") >= 0) { + //This is a PLS playlist + parser = playlist_parsers.PLS; + } else if ((contentType.indexOf("video/x-ms-asf") >= 0) || + (contentType.indexOf("video/x-ms-asx") >= 0)) { + //This is a ASX playlist + parser = playlist_parsers.ASX; + } + + + if (parser) { + //This is a playlist. Close the stream and get the playlist file + that.close(); + + request_lib.get(that._url2play, function (error, response, body) { + if (!error && response.statusCode == 200) { + var playlist = parser.parse(body); + that._media.dump_body = body; + that._media.dump_playlist = playlist; + + if (playlist.length > 0) { + + //Play the first element on the list + that._url2play = playlist[0].file; + + //Set the default metadata again + that._media.contentId = that._url2play; + that._media.metadata.title = playlist[0].title ? playlist[0].title : playlist[0].file; + that._media.metadata.artist = playlist[0].artist; + that._media.metadata.length = playlist[0].lenght; + + try { + that._requestObject = icy.get(that._url2play, that._connected.bind(that)); + that._requestObject.on('error', function (e) { + that._media.ERROR = "MediaInformation - ICY connection error for " + that._url2play + ": " + e; + that._callback(that._media); + }); + + //Everything went fine -> let us return and wait as done on the constructor + return; + } catch (e) { + that._media.ERROR = "MediaInformation - Cannot connect to " + that._url2play + ": " + e; + } + } } - } - //Something went wrong -> call callback with what we have - that._callback(that._media); - }); - return; + //Something went wrong -> call callback with what we have + that._callback(that._media); + }); + return; + } + + if ((contentType.indexOf("audio") >= 0) || + (contentType.indexOf("video") >= 0)) + that._media.contentType = contentType; } - if ((contentType.indexOf("audio") >= 0) || - (contentType.indexOf("video") >= 0)) - that._media.contentType = contentType; - } - - //console.log(that._media); - that._media.debugHeaderDump = res.headers; - - if ("icy-name" in res.headers) { - //As backup call the callback if we do not get - //first metadata in less than 1 second - that._timeoutHandler = setTimeout(function () { - that._callback(that._media); - }, 1000); + //console.log(that._media); + that._media.debugHeaderDump = res.headers; - // log any "metadata" events that happen - res.on('metadata', that._gotMetadata.bind(that, res)); - } else { - //Not ICY -> call callback already - that._callback(that._media); - } - -}; + if ("icy-name" in res.headers) { + //As backup call the callback if we do not get + //first metadata in less than 1 second + that._timeoutHandler = setTimeout(function () { + that._callback(that._media); + }, 1000); + + // log any "metadata" events that happen + res.on('metadata', that._gotMetadata.bind(that, res)); + } else { + //Not ICY -> call callback already + that._callback(that._media); + } + + }; -Connection.prototype._gotMetadata = function (res, metadata) { - /* - * { StreamTitle: 'BILLY IDOL - WHITE WEDDING', - StreamUrl: '&artist=BILLY%20IDOL&title=WHITE%20WEDDING&album=&duration=&songtype=S&overlay=&buycd=&website=&picture' } - */ - var parsed = icy.parse(metadata); - console.log(parsed); + Connection.prototype._gotMetadata = function (res, metadata) { + /* + * { StreamTitle: 'BILLY IDOL - WHITE WEDDING', + StreamUrl: '&artist=BILLY%20IDOL&title=WHITE%20WEDDING&album=&duration=&songtype=S&overlay=&buycd=&website=&picture' } + */ + var parsed = icy.parse(metadata); + adapter.log.debug(JSON.stringify(parsed)); - //Get title - that._media.metadata.title = parsed.StreamTitle; - - //If we got media info then call callback already - if (that._timeoutHandler) { - clearTimeout(that._timeoutHandler); - that._timeoutHandler = undefined; - that._callback(that._media); - } - - //Notify that media has been updated - that.emit("media", that._media); - - if (!that._pipeOn) { - //We need to keep reading in order to receive additional metadata - //We do it here to avoid reading data for sources that do not send - //any metadata - res.pipe(devnull()); - that._pipeOn = true; - } -}; + //Get title + that._media.metadata.title = parsed.StreamTitle; + + //If we got media info then call callback already + if (that._timeoutHandler) { + clearTimeout(that._timeoutHandler); + that._timeoutHandler = undefined; + that._callback(that._media); + } + + //Notify that media has been updated + that.emit("media", that._media); + + if (!that._pipeOn) { + //We need to keep reading in order to receive additional metadata + //We do it here to avoid reading data for sources that do not send + //any metadata + res.pipe(devnull()); + that._pipeOn = true; + } + }; -var activeConnections = {}; + var activeConnections = {}; -function getMediaInfoListerner(callerId, url2play, org_url2play, streamType, callback) { - - if (url2play in activeConnections && - callerId in activeConnections[url2play].callerIDs) { + function getMediaInfoListerner(callerId, url2play, org_url2play, streamType, callback) { + + adapter.log.debug("Get media info for " + url2play); + if (url2play in activeConnections && + callerId in activeConnections[url2play].callerIDs) { + + //Call callback + callback(activeConnections[url2play].connection.getMedia()); + + //This callerID is already listing the URL + return activeConnections[url2play].connection; + } + + //Close in case this caller was already listing -> only one connection/callerID + closeMediaInfoListerner(callerId); + + //If this is the first listener for this URL then create object + if (!(url2play in activeConnections)) { + var connection = new Connection(url2play, org_url2play, streamType, callback); + activeConnections[url2play] = { + callerIDs: {}, + connection: connection + }; + } else { + //Call callback + callback(activeConnections[url2play].connection.getMedia()); + } - //Call callback - callback(activeConnections[url2play].connection.getMedia()); + //Remember callerId + activeConnections[url2play].callerIDs[callerId] = true; - //This callerID is already listing the URL + //Return connection return activeConnections[url2play].connection; } - - //Close in case this caller was already listing -> only one connection/callerID - closeMediaInfoListerner(callerId); - - //If this is the first listener for this URL then create object - if (!(url2play in activeConnections)) { - var connection = new Connection(url2play, org_url2play, streamType, callback); - activeConnections[url2play] = { - callerIDs: {}, - connection: connection - }; - } - - //Remember callerId - activeConnections[url2play].callerIDs[callerId] = true; - - //Return connection - return activeConnections[url2play].connection; -} - -function closeMediaInfoListerner(callerId) { - for (var url2play in activeConnections) { - - //Set this callerId as inactive - activeConnections[url2play].callerIDs[callerId] = false; - - //Any active listeners for this URL? - var anyListenerActive = false; - for (var i in activeConnections[url2play].callerIDs) { - anyListenerActive |= activeConnections[url2play].callerIDs[i]; - } - - if (!anyListenerActive) { - //None is listening to this URL -> destroy connection - activeConnections[url2play].connection.close(); - activeConnections[url2play].callerIDs = undefined; - console.log("Disconnect " + url2play); - console.log("Last listener was " + callerId); - delete activeConnections[url2play]; - } - } -} -module.exports.getListener = getMediaInfoListerner; -module.exports.closeListener = closeMediaInfoListerner; + function closeMediaInfoListerner(callerId) { + for (var url2play in activeConnections) { + + //Set this callerId as inactive + activeConnections[url2play].callerIDs[callerId] = false; + + //Any active listeners for this URL? + var anyListenerActive = false; + adapter.log.debug(callerId + " closing for " + url2play + " - " + JSON.stringify(activeConnections[url2play].callerIDs)); + for (var i in activeConnections[url2play].callerIDs) { + anyListenerActive |= activeConnections[url2play].callerIDs[i]; + } + + if (!anyListenerActive) { + //None is listening to this URL -> destroy connection + activeConnections[url2play].connection.close(); + activeConnections[url2play].callerIDs = undefined; + adapter.log.info("Disconnect " + url2play); + adapter.log.debug("Last listener was " + callerId); + delete activeConnections[url2play]; + } + } + } + + //Define module exports + module = {}; + module.getListener = getMediaInfoListerner; + module.closeListener = closeMediaInfoListerner; + return module; +}; diff --git a/package.json b/package.json index 12ae704..97c4b68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iobroker.chromecast", - "version": "1.1.0", + "version": "1.1.1", "description": "ioBroker chromecast Adapter", "author": { "name": "Vegetto", diff --git a/widgets/chromecast.html b/widgets/chromecast.html index 8a4000f..e71cc1a 100644 --- a/widgets/chromecast.html +++ b/widgets/chromecast.html @@ -1,7 +1,7 @@ diff --git a/widgets/chromecast/js/chromecast.js b/widgets/chromecast/js/chromecast.js index 05bb3ee..dfac191 100644 --- a/widgets/chromecast/js/chromecast.js +++ b/widgets/chromecast/js/chromecast.js @@ -1,7 +1,7 @@ /* ioBroker.chromecast Widget-Set - version: "1.1.0" + version: "1.1.1" Copyright 10.2015-2016 Vegetto @@ -142,7 +142,7 @@ function registerForDeviceUpdates($widget, ioBrokerState){ // this code can be placed directly in chromecast.html vis.binds.chromecast = { - version: "1.1.0", + version: "1.1.1", showVersion: function () { if (vis.binds.chromecast.version) { console.log('Version chromecast: ' + vis.binds.chromecast.version);