├── .vscode
├── settings.json
└── launch.json
├── lib
├── lib.manager.base.js
├── lib.request.js
├── lib.device.base.js
├── setUriMetadata.template
├── lib.base.manager.js
├── lib.tools.js
├── lib.managerDisposer.js
├── lib.logger.js
├── lib.queueController.js
├── lib.device.base.upnp.js
├── lib.queueController.nativePlaylist.js
├── lib.base.js
├── lib.device.upnp.mediaServer.js
├── lib.discoverHostDevice.js
├── lib.device.upnp.mediaServer.raumfeld.js
├── lib.device.upnp.mediaRenderer.raumfeld.js
├── lib.manager.triggerManager.js
├── lib.manager.infodataManager.js
├── lib.queueController.zonePlaylist.js
├── lib.mediaDataConverter.js
├── lib.raumkernel.js
├── lib.manager.mediaListManager.js
├── lib.device.upnp.mediaRenderer.js
└── lib.external.upnp-device-client.js
├── .gitignore
├── package.json
├── index.js
├── LICENSE
├── sample_contentBrowser.js
├── README.md
└── test.js
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | }
--------------------------------------------------------------------------------
/lib/lib.manager.base.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var BaseManager = require('./lib.base.manager');
3 |
4 | module.exports = class ManagerBase extends BaseManager
5 | {
6 | constructor()
7 | {
8 | super();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lib/lib.request.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Use old GOT version because of ES6 imports
4 | //import got from 'got';
5 | //var got = require("got");
6 | // npm install got@7.1.0
7 |
8 | // use node-fetch old version --> version 2!!!! later switch to nodejs version if nodejs 18 is used in iobroker
9 |
10 | module.exports = function Request(_requestOptions, _callback)
11 | {
12 | let options = {};
13 | let request = got.get(_requestOptions.url, options);
14 | request.then((_response) => {
15 | _callback(false, _response, _response.body)
16 | }).catch((_error) => {
17 | _callback(true, err.response, err.response.body)
18 | });
19 |
20 | // response.statusCode
21 | // response.headers
22 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
--------------------------------------------------------------------------------
/lib/lib.device.base.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var BaseManager = require('./lib.base.manager');
3 |
4 | /**
5 | * this is the base class to use for child's which are devices
6 | */
7 | module.exports = class Device extends BaseManager
8 | {
9 | constructor()
10 | {
11 | super();
12 | }
13 |
14 | /**
15 | * used to return a readable name for a device
16 | * @return {String} a readable name for the device
17 | */
18 | name()
19 | {
20 | return "";
21 | }
22 |
23 | /**
24 | * used to return a unique Id for a device
25 | * @return {String} a unique id for the device
26 | */
27 | id()
28 | {
29 | return "";
30 | }
31 | }
--------------------------------------------------------------------------------
/lib/setUriMetadata.template:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 | Station
5 | object.item.audioItem.audioBroadcast.radio
6 | External
7 | 120
8 |
9 | 99
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "engines": {
3 | "node": ">=7.6.0"
4 | },
5 | "name": "node-raumkernel",
6 | "version": "1.2.23",
7 | "description": "Library to control the raumfeld multiroom system",
8 | "main": "index.js",
9 | "scripts": {
10 | "start": "node index.js"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/ChriD/node-raumkernel.git"
15 | },
16 | "keywords": [
17 | "raumserver",
18 | "raumkernel",
19 | "raumfeld"
20 | ],
21 | "dependencies": {
22 | "bonjour": "^3.5.0",
23 | "node-fetch": "^3.3.0",
24 | "node-ssdp": "^3.2.0",
25 | "query-string": "^4.3.2",
26 | "request": "^2.79.0",
27 | "upnp-device-client": "^1.0.2",
28 | "xml2js": "^0.4.17"
29 | },
30 | "author": "Christian Dürnberger",
31 | "bugs": {
32 | "url": "https://github.com/ChriD/node-raumkernel/issues"
33 | },
34 | "homepage": "https://github.com/ChriD/node-raumkernel#readme"
35 | }
36 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = './lib/';
4 |
5 | module.exports = {
6 | Raumkernel : require(path + "lib.raumkernel"),
7 | Logger : require(path + "lib.logger"),
8 | Base : require(path + "lib.base"),
9 | BaseManager : require(path + "lib.base.manager"),
10 | MediaRenderer : require(path + "lib.device.upnp.mediaRenderer"),
11 | MediaRendererRaumfeld : require(path + "lib.device.upnp.mediaRenderer.raumfeld"),
12 | MediaRendererRaumfeldVirtual : require(path + "lib.device.upnp.mediaRenderer.raumfeldVirtual"),
13 | MediaServer : require(path + "lib.device.upnp.mediaServer"),
14 | MediaServerRaumfeld : require(path + "lib.device.upnp.mediaServer.raumfeld"),
15 | ManagerBase : require(path + "lib.manager.base"),
16 | DeviceManager : require(path + "lib.manager.deviceManager"),
17 | ZoneManager : require(path + "lib.manager.zoneManager"),
18 | MediaListManager : require(path + "lib.manager.mediaListManager"),
19 | ManagerDisposer : require(path + "lib.managerDisposer"),
20 | PackageJSON : require("./package.json")
21 | }
22 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "program": "${workspaceRoot}/test.js"
12 | },
13 | {
14 | "type": "node",
15 | "request": "launch",
16 | "name": "Programm starten",
17 | "program": "${workspaceRoot}\\test.js"
18 | },
19 | {
20 | "type": "node",
21 | "request": "launch",
22 | "name": "Content Browse test starten",
23 | "program": "${workspaceRoot}\\sample_contentBrowser.js"
24 | },
25 | {
26 | "type": "node",
27 | "request": "attach",
28 | "name": "An den Prozess anfügen",
29 | "address": "localhost",
30 | "port": 5858
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Christian Dürnberger
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/lib.base.manager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Logger = require('./lib.logger');
3 | var Base = require('./lib.base');
4 |
5 | /**
6 | * this is the base class to use for child's which have to have access to the manager instances
7 | */
8 | module.exports = class BaseManager extends Base
9 | {
10 | constructor()
11 | {
12 | super();
13 | this.managerDisposer = null;
14 | }
15 |
16 | /**
17 | * used to set or to get the manager disposer object
18 | * @param {Object} the instanced of the manager disposer
19 | * @return {Object} the instanced of the manager disposer
20 | */
21 | parmManagerDisposer(_managerDisposer = this.managerDisposer)
22 | {
23 | this.managerDisposer = _managerDisposer;
24 | return this.managerDisposer;
25 | }
26 |
27 | /**
28 | * used to return the settings from the raumkernel instance
29 | * @return {Object} the settings of the kernel
30 | */
31 | getSettings()
32 | {
33 | if(!this.managerDisposer || !this.managerDisposer.raumkernel)
34 | return {};
35 | return this.managerDisposer.raumkernel.settings;
36 | }
37 | }
--------------------------------------------------------------------------------
/lib/lib.tools.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * use this to encode a string for the content directory
5 | * @param {String} the string which should be encoded
6 | * @param {String} encoded string
7 | */
8 | var encodeString = exports.encodeString=function(_string)
9 | {
10 | return encodeURIComponent(_string)/*.replace(/\-/g, "%2D").replace(/\_/g, "%5F")*/.replace(/\./g, "%2E").replace(/\!/g, "%21").replace(/\~/g, "%7E").replace(/\*/g, "%2A").replace(/\'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29");
11 | }
12 |
13 |
14 | /**
15 | * use this to decode a string for the content directory
16 | * @param {String} the string which should be decoded
17 | * @param {String} decoded string
18 | */
19 | var decodeString = exports.decodeString=function(_string)
20 | {
21 | return decodeURIComponent(_string/*.replace(/\%2D/g, "-").replace(/\%5F/g, "_")*/.replace(/\%2E/g, ".").replace(/\%21/g, "!").replace(/\%7E/g, "~").replace(/\%2A/g, "*").replace(/\%27/g, "'").replace(/\%28/g, "(").replace(/\%29/g, ")"));
22 | }
23 |
24 |
25 | var isUrl = exports.isUrl=function(_string)
26 | {
27 | var regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
28 | return regexp.test(_string);
29 | }
30 |
31 |
32 | exports.createAVTransportUriForContainer=function(_mediaServerUdn, _containerId, _trackIndex = -1)
33 | {
34 | // a valid transport uri looks like this!
35 | //dlna-playcontainer://uuid%3Aed3bd3db-17b1-4dbe-82df-5201c78e632c?sid=urn%3Aupnp-org%3AserviceId%3AContentDirectory&cid=0%2FPlaylists%2FMyPlaylists%2FTest&md=0
36 | var uri = "";
37 | uri = encodeString(_mediaServerUdn) + "?sid=" + encodeString("urn:upnp-org:serviceId:ContentDirectory") + "&cid=" + encodeString(_containerId) + "&md=0";
38 | if(_trackIndex >= 0)
39 | uri += "&fii=" + encodeString(_trackIndex.toString());
40 | uri = "dlna-playcontainer://" + uri;
41 | return uri;
42 | }
43 |
44 | exports.createAVTransportUriForSingle=function(_mediaServerUdn, _singleId)
45 | {
46 | // a valid transport uri looks like this!
47 | //dlna-playsingle://uuid%3Aed3bd3db-17b1-4dbe-82df-5201c78e632c?sid=urn%3Aupnp-org%3AserviceId%3AContentDirectory&iid=0%2FRadioTime%2FLocalRadio%2Fs-s68932
48 | var uri = "";
49 | uri = encodeString(_mediaServerUdn) + "?sid=" + encodeString("urn:upnp-org:serviceId:ContentDirectory") + "&iid=" + encodeString(_singleId);
50 | uri = "dlna-playsingle://" + uri;
51 | return uri;
52 | }
--------------------------------------------------------------------------------
/lib/lib.managerDisposer.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | var Logger = require('./lib.logger')
3 | var Base = require('./lib.base')
4 | var DeviceManager = require('./lib.manager.deviceManager')
5 | var ZoneManager = require('./lib.manager.zoneManager')
6 | var MediaListManager = require('./lib.manager.mediaListManager')
7 | var TriggerManager = require('./lib.manager.triggerManager')
8 | var InfodataManager = require('./lib.manager.infodataManager')
9 |
10 | /**
11 | * this is the manager disposer class.
12 | * it holds all available managers
13 | */
14 | module.exports = class ManagerDisposer extends Base
15 | {
16 | constructor()
17 | {
18 | super()
19 | this.raumkernel = null
20 | this.deviceManager = null
21 | this.zoneManager = null
22 | this.mediaListManager = null;
23 | this.triggerManager = null
24 | this.infodataManager = null
25 | }
26 |
27 | parmRaumkernel(_raumkernel = this.raumkernel)
28 | {
29 | this.raumkernel = _raumkernel
30 | return this.raumkernel
31 | }
32 |
33 | additionalLogIdentifier()
34 | {
35 | return "ManagerDisposer"
36 | }
37 |
38 | createManagers()
39 | {
40 | this.createDeviceManager()
41 | this.createZoneManager()
42 | this.createMediaListManager()
43 | this.createTriggerManager()
44 | this.createInfodataManager()
45 | }
46 |
47 | createDeviceManager()
48 | {
49 | this.logVerbose("Creating device manager")
50 | this.deviceManager = new DeviceManager()
51 | this.deviceManager.parmLogger(this.parmLogger())
52 | this.deviceManager.parmManagerDisposer(this)
53 | }
54 |
55 | createZoneManager()
56 | {
57 | this.logVerbose("Creating zone manager")
58 | this.zoneManager = new ZoneManager()
59 | this.zoneManager.parmLogger(this.parmLogger())
60 | this.zoneManager.parmManagerDisposer(this)
61 | }
62 |
63 | createMediaListManager()
64 | {
65 | this.logVerbose("Creating media list manager")
66 | this.mediaListManager = new MediaListManager()
67 | this.mediaListManager.parmLogger(this.parmLogger())
68 | this.mediaListManager.parmManagerDisposer(this)
69 | }
70 |
71 | createTriggerManager()
72 | {
73 | this.logVerbose("Creating trigger manager")
74 | this.triggerManager = new TriggerManager()
75 | this.triggerManager.parmLogger(this.parmLogger())
76 | this.triggerManager.parmManagerDisposer(this)
77 | }
78 |
79 | createInfodataManager()
80 | {
81 | this.logVerbose("Creating infodata manager")
82 | this.infodataManager = new InfodataManager()
83 | this.infodataManager.parmLogger(this.parmLogger())
84 | this.infodataManager.parmManagerDisposer(this)
85 | }
86 |
87 | }
--------------------------------------------------------------------------------
/lib/lib.logger.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var EventEmitter = require("events").EventEmitter;
3 |
4 |
5 | /**
6 | * A simple logger class that will be used throughout the raumkernel lib
7 | * It emits an log event so any kind of logger can be attached
8 | */
9 | module.exports = class Logger extends EventEmitter
10 | {
11 | /**
12 | * Constructor of the logger class
13 | * @param {Number} the maximum log level which should be logged
14 | * @return {Object} The logger object
15 | */
16 | constructor(_logLevel)
17 | {
18 | super()
19 | // this is the maximum log level that will be logged
20 | this.logLevel = _logLevel;
21 | }
22 |
23 | /**
24 | * log a text to the activated outputs
25 | * @param {Number} the log type
26 | * @param {String} the log text
27 | * @param {Object} some additional meta data object
28 | */
29 | log(_logType, _log, _metadata = null)
30 | {
31 | if(_logType <= this.logLevel)
32 | this.emit("log", { "logType" : _logType, "log" : _log, "metadata" : _metadata });
33 | }
34 |
35 | /**
36 | * log a error
37 | * @param {String} the log text
38 | * @param {Object} some additional meta data object
39 | */
40 | logError(_log, _metadata = null)
41 | {
42 | this.log(0, _log, _metadata);
43 | }
44 |
45 | /**
46 | * log a warning
47 | * @param {String} the log text
48 | * @param {Object} some additional meta data object
49 | */
50 | logWarning(_log, _metadata = null)
51 | {
52 | this.log(1, _log, _metadata);
53 | }
54 |
55 | /**
56 | * log a info
57 | * @param {String} the log text
58 | * @param {Object} some additional meta data object
59 | */
60 | logInfo(_log, _metadata = null)
61 | {
62 | this.log(2, _log, _metadata);
63 | }
64 |
65 | /**
66 | * log a verbose
67 | * @param {String} the log text
68 | * @param {Object} some additional meta data object
69 | */
70 | logVerbose(_log, _metadata = null)
71 | {
72 | this.log(3, _log, _metadata);
73 | }
74 |
75 | /**
76 | * log a debug
77 | * @param {String} the log text
78 | * @param {Object} some additional meta data object
79 | */
80 | logDebug(_log, _metadata = null)
81 | {
82 | this.log(4, _log, _metadata);
83 | }
84 |
85 | /**
86 | * log a silly
87 | * @param {String} the log text
88 | * @param {Object} some additional meta data object
89 | */
90 | logSilly(_log, _metadata = null)
91 | {
92 | this.log(5, _log, _metadata);
93 | }
94 |
95 |
96 |
97 | }
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/lib/lib.queueController.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Logger = require('./lib.logger');
3 | var BaseManager = require('./lib.base.manager');
4 | var ManagerDisposer = require('./lib.managerDisposer');
5 |
6 | module.exports = class QueueController extends BaseManager
7 | {
8 | constructor()
9 | {
10 | super();
11 | this.mediaServer = null;
12 | this.queueBaseContainerId = "";
13 | }
14 |
15 |
16 | init()
17 | {
18 | }
19 |
20 | additionalLogIdentifier()
21 | {
22 | return "QueueController";
23 | }
24 |
25 | parmQueueBaseContainerId(_queueBaseContainerId = this.queueBaseContainerId)
26 | {
27 | this.queueBaseContainerId = _queueBaseContainerId;
28 | return this.queueBaseContainerId;
29 | }
30 |
31 | parmMediaServer(_mediaServer = this.mediaServer)
32 | {
33 | this.mediaServer = _mediaServer;
34 | return this.mediaServer;
35 | }
36 |
37 | getQueueIdFromNameAndBase(_queueName, _baseContainerId = this.queueBaseContainerId)
38 | {
39 | return _baseContainerId + "/" + this.encodeString(_queueName);
40 | }
41 |
42 | createQueue(_queueName)
43 | {
44 | if(!this.mediaServer)
45 | return Promise.reject(new Error("MediaServer not ready!"));
46 | return this.mediaServer.createQueue(_queueName, this.queueBaseContainerId);
47 | }
48 |
49 | deleteQueue(_queueName)
50 | {
51 | if(!this.mediaServer)
52 | return Promise.reject(new Error("MediaServer not ready!"));
53 | return this.mediaServer.destroyObject(this.getQueueIdFromNameAndBase(_queueName));
54 | }
55 |
56 | renameQueue(_oldQueueName, _newQueueName)
57 | {
58 | if(!this.mediaServer)
59 | return Promise.reject(new Error("MediaServer not ready!"));
60 | return this.mediaServer.renameQueue(this.getQueueIdFromNameAndBase(_oldQueueName), _newQueueName);
61 | }
62 |
63 |
64 | addItemToQueue(_queueName, _mediaItemId, _position = 294967295, _isItemsContainer = false, _startIndex = 0, _endIndex = 294967295)
65 | {
66 | if(!this.mediaServer)
67 | return Promise.reject(new Error("MediaServer not ready!"));
68 |
69 | if(_isItemsContainer)
70 | {
71 | return this.mediaServer.addContainerToQueue(this.getQueueIdFromNameAndBase(_queueName), _mediaItemId, _mediaItemId, "*", "", _startIndex, _endIndex, _position);
72 | }
73 | else
74 | {
75 | return this.mediaServer.addItemToQueue(this.getQueueIdFromNameAndBase(_queueName), _mediaItemId, _position);
76 | }
77 | }
78 |
79 |
80 | removeItemsFromQueue(_queueName, _fromPosition, _toPosition)
81 | {
82 | if(!this.mediaServer)
83 | return Promise.reject(new Error("MediaServer not ready!"));
84 | return this.mediaServer.removeFromQueue(this.getQueueIdFromNameAndBase(_queueName), _fromPosition, _toPosition);
85 | }
86 |
87 |
88 | moveItemInQueue(_mediaItemId, _newPosition)
89 | {
90 | if(!this.mediaServer)
91 | return Promise.reject(new Error("MediaServer not ready!"));
92 | return this.mediaServer.moveInQueue(_mediaItemId, _newPosition);
93 | }
94 |
95 |
96 | }
--------------------------------------------------------------------------------
/lib/lib.device.base.upnp.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Url = require('url');
3 | var Device = require('./lib.device.base');
4 |
5 | /**
6 | * this is the base class to use for child's which are UPNP devices
7 | */
8 | module.exports = class UPNPDevice extends Device
9 | {
10 | constructor(_upnpClient)
11 | {
12 | super();
13 | this.upnpClient = _upnpClient;
14 | this.upnpClient.userAgent = "RaumfeldControl/3.10 RaumfeldProtocol/399";
15 | }
16 |
17 |
18 | close()
19 | {
20 | this.upnpClient.releaseEventingServer(true);
21 | }
22 |
23 | name()
24 | {
25 | return this.upnpClient.deviceDescription.friendlyName;
26 | }
27 |
28 | id()
29 | {
30 | // in fact USN/UDN should not be used as an ID as mentioned here: https://www.w3.org/2009/dap/track/issues/151
31 | return this.upnpClient.deviceDescription.UDN;
32 | }
33 |
34 | /**
35 | * used to return the host (IP) of the device
36 | * @return {String} the host (IP) of the device
37 | */
38 | host()
39 | {
40 | return Url.parse(this.upnpClient.url).hostname;
41 | }
42 |
43 | /**
44 | * used to return a the UDN of the device
45 | * @return {String} the UDN of the UPNP device
46 | */
47 | udn()
48 | {
49 | return this.upnpClient.deviceDescription.UDN;
50 | }
51 |
52 | /**
53 | * used to return a the modelNumber of the device
54 | * @return {String} the modelNumber of the UPNP device
55 | */
56 | modelNumber()
57 | {
58 | return this.upnpClient.deviceDescription.modelNumber;
59 | }
60 |
61 | /**
62 | * used to return a the manufacturer of the device
63 | * @return {String} the manufacturer of the UPNP device
64 | */
65 | manufacturer()
66 | {
67 | return this.upnpClient.deviceDescription.manufacturer;
68 | }
69 |
70 | /**
71 | * used to return a the friendlyName of the device
72 | * @return {String} the friendlyName of the UPNP device
73 | */
74 | friendlyName()
75 | {
76 | return this.upnpClient.deviceDescription.friendlyName;
77 | }
78 |
79 | /**
80 | * should be used to call an action on a service of the device
81 | * @return {Promise} a promise with the result as parameter
82 | */
83 | callAction(_service, _action, _params, _resultSetFunction = null)
84 | {
85 | var self = this;
86 | this.logDebug("Call " + _action + " from " + this.name());
87 |
88 | return new Promise(function(resolve, reject){
89 | self.upnpClient.callAction(_service, _action, _params, function (_err, _result) {
90 | if(!_err && _result)
91 | {
92 | self.logDebug("Result of " + _action + " for " + self.name() + " is " + JSON.stringify(_result));
93 | if(_resultSetFunction)
94 | resolve(_resultSetFunction(_result));
95 | else
96 | {
97 | resolve(_result);
98 | }
99 | }
100 | else
101 | {
102 | self.logError(_action + " on " + self.name() + " failed with params: " + JSON.stringify(_params), _err);
103 | reject(_err);
104 | }
105 | });
106 | })
107 | }
108 |
109 | /**
110 | * use this method to subscribe to services of the upnp device
111 | */
112 | subscribe()
113 | {
114 | }
115 |
116 | /**
117 | * use this method to unsubscribe to services of the upnp device
118 | */
119 | unsubscribe()
120 | {
121 | }
122 |
123 | }
--------------------------------------------------------------------------------
/lib/lib.queueController.nativePlaylist.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Logger = require('./lib.logger');
3 | var QueueController = require('./lib.queueController');
4 | var ManagerDisposer = require('./lib.managerDisposer');
5 |
6 | module.exports = class QueueControllerNativePlaylist extends QueueController
7 | {
8 | constructor()
9 | {
10 | super();
11 | // the container id for the native playlists
12 | this.parmQueueBaseContainerId("0/Playlists/MyPlaylists");
13 | }
14 |
15 |
16 | additionalLogIdentifier()
17 | {
18 | return "QueueControllerNativePlaylist";
19 | }
20 |
21 |
22 | createPlaylist(_playlistName)
23 | {
24 | var self = this;
25 | self.logVerbose("Create native playlist with name '" + _playlistName + "'");
26 | return new Promise(function(_resolve, _reject){
27 | self.createQueue(_playlistName).then(function(_data){
28 | self.logVerbose("Native playlist with name '" + _playlistName + "' created");
29 | _resolve(_data);
30 | }).catch(function(_data){
31 | self.logError("Native playlist with name '" + _playlistName + "' could not be created");
32 | _reject(_data);
33 | });
34 | });
35 | }
36 |
37 |
38 | deletePlaylist(_playlistName)
39 | {
40 | var self = this;
41 | self.logVerbose("Delete native playlist with name '" + _playlistName + "'");
42 | return new Promise(function(_resolve, _reject){
43 | self.deleteQueue(_playlistName).then(function(_data){
44 | self.logVerbose("Native playlist with name '" + _playlistName + "' deleted");
45 | _resolve(_data);
46 | }).catch(function(_data){
47 | self.logError("Native playlist with name '" + _playlistName + "' could not be deleted");
48 | _reject(_data);
49 | });
50 | });
51 | }
52 |
53 |
54 | renamePlaylist(_oldPlaylistName, _newPlaylistName)
55 | {
56 | var self = this;
57 | self.logVerbose("Rename native playlist with name '" + _oldPlaylistName + "' to '" + _newPlaylistName + "'");
58 | return new Promise(function(_resolve, _reject){
59 | self.renameQueue(_oldPlaylistName, _newPlaylistName).then(function(_data){
60 | self.logVerbose("Playlist with name '" + _oldPlaylistName + "' renamed to '" + _newPlaylistName + "'");
61 | _resolve(_data);
62 | }).catch(function(_data){
63 | self.logError("Rename native playlist with name '" + _oldPlaylistName + "' to '" + _newPlaylistName + "' had errors");
64 | _reject(_data);
65 | });
66 | });
67 | }
68 |
69 |
70 | addItemToPlaylist(_playlistName, _mediaItemId, _position = 294967295, _isItemsContainer = false, _startIndex = 0, _endIndex = 294967295)
71 | {
72 | var self = this;
73 | self.logVerbose("Add item '" + _mediaItemId + "' to '" + _playlistName + "'");
74 | return this.addItemToQueue(_playlistName, _mediaItemId, _position, _isItemsContainer, _startIndex, _endIndex);
75 | }
76 |
77 |
78 | removeItemsFromPlaylist(_playlistName, _fromPosition, _toPosition)
79 | {
80 | var self = this;
81 | self.logVerbose("Remove items '" + _fromPosition.toString() + "' to '" + _toPosition.toString() + "' on playlist '" + _playlistName + "'");
82 | return this.removeItemsFromQueue(_playlistName, _fromPosition, _toPosition);
83 | }
84 |
85 |
86 | moveItemInPlaylist(_playlistName, _mediaItemId, _newPosition)
87 | {
88 | var self = this;
89 | self.logVerbose("Move item '" + _mediaItemId + "' in playlist '" + _playlistName + "' to position " + _newPosition.toString());
90 | // the "_mediaItemId" has to consist of the id of the playlist item of course, so on fact "_playlistName" is not really needed here
91 | this.moveItemInQueue(_mediaItemId, _newPosition);
92 | }
93 |
94 | }
--------------------------------------------------------------------------------
/sample_contentBrowser.js:
--------------------------------------------------------------------------------
1 | const Readline = require('readline');
2 | var Raumkernel = require('./lib/lib.raumkernel');
3 |
4 | setWelcomeScreen();
5 |
6 | var idStack = [];
7 | var tp0;
8 | var tpDuration;
9 | var raumkernel = new Raumkernel();
10 | raumkernel.createLogger(1, "logs");
11 | raumkernel.init();
12 |
13 |
14 | function perfMeassure(_start) {
15 | if ( !_start ) return process.hrtime();
16 | var end = process.hrtime(_start);
17 | return Math.round((end[0]*1000) + (end[1]/1000000));
18 | }
19 |
20 |
21 | // set up a callback to show the "reading state" of a media list that is beeing read
22 | // of course this will be called on any browse wherer the "emit" parameter is active
23 | raumkernel.on("mediaListDataPackageReady", function(_id, _mediaListDataPkg, _pkgIdx, _pgkIdxEnd, _pkgDataCount)
24 | {
25 | console.log('\033[2J');
26 | console.log("--------------------------------");
27 | console.log("Reading Data: " + (_pgkIdxEnd+1).toString());
28 | console.log("--------------------------------");
29 | });
30 |
31 |
32 | // browse to root when system is ready
33 | raumkernel.on("systemReady", function(_ready){
34 | browse("0");
35 | });
36 |
37 |
38 | function browse(_id, _backwards = false, _addToStack = true)
39 | {
40 | setLoadingScreen(_id);
41 |
42 | if(_backwards)
43 | idStack.pop();
44 | else if(_addToStack)
45 | idStack.push(_id);
46 |
47 | tp0 = perfMeassure();
48 |
49 | raumkernel.managerDisposer.mediaListManager.getMediaList(_id, _id, "", true, true, 25).then(function(_data){
50 | tpDuration = perfMeassure(tp0);
51 | viewBrowseResult(_id, _data);
52 | }).catch(function(_data){
53 | viewError(_data);
54 | });
55 | }
56 |
57 |
58 | function viewBrowseResult(_id, _data)
59 | {
60 | console.log('\033[2J');
61 | console.log(JSON.stringify(idStack) + " --> " + _id);
62 | console.log("--------------------------------");
63 |
64 | if(_data && _data.length)
65 | {
66 | for(var i=0; i<_data.length; i++)
67 | {
68 | console.log(i.toString() + " - " + _data[i].title + " (" + _data[i].id + ")");
69 | }
70 | }
71 | else
72 | {
73 | console.log("# No data available");
74 | }
75 | console.log("--------------------------------");
76 | console.log("Loading time: " + (tpDuration) + " ms");
77 | console.log("--------------------------------");
78 | console.log("x BROWSE BACK");
79 | console.log("--------------------------------");
80 |
81 | var readLine1 = Readline.createInterface({input: process.stdin, output: process.stdout});
82 | readLine1.question('Choose ID: ', (_input) => {
83 | readLine1.close();
84 | if(_data && _data[_input])
85 | {
86 | browse(_data[_input].id);
87 | }
88 | else
89 | {
90 | if(_input.toLowerCase() == "x" && idStack.length > 1)
91 | {
92 | var parentId = idStack[idStack.length-2];
93 | browse(parentId, true);
94 | }
95 | else
96 | {
97 | browse(_id, false, false);
98 | }
99 | }
100 | });
101 | }
102 |
103 |
104 | function viewError(_data)
105 | {
106 | console.log('\033[2J');
107 | console.log('Error occured: ' + _data);
108 |
109 | var readLine1 = Readline.createInterface({input: process.stdin, output: process.stdout});
110 | readLine1.question("Press [ENTER] to start at root again!", (_input) => {
111 | readLine1.close();
112 | idStack = [];
113 | browse("0");
114 | });
115 | }
116 |
117 |
118 | function setLoadingScreen(_id)
119 | {
120 | console.log('\033[2J');
121 | console.log("Loading list for id :" + _id);
122 | console.log("Please wait! Certain id's can take a lot of time!");
123 | }
124 |
125 |
126 | function setWelcomeScreen()
127 | {
128 | console.log('\033[2J');
129 | console.log("Please wait till the raumfeld system is found!")
130 | }
131 |
132 |
133 | function execute(){
134 | }
135 |
136 |
137 | setInterval(execute,1000);
--------------------------------------------------------------------------------
/lib/lib.base.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Logger = require('./lib.logger');
3 | var EventEmitter = require("events").EventEmitter;
4 | var Tools = require('./lib.tools');
5 |
6 |
7 | /**
8 | * This is the base class all classes will be derived from.
9 | * It contains some basic functionality like log methods
10 | */
11 | module.exports = class Base extends EventEmitter
12 | {
13 |
14 | /**
15 | * Constructor of the base class
16 | * @return {Object} The base object
17 | */
18 | constructor()
19 | {
20 | super();
21 | //EventEmitter.call(this);
22 | this.logger = null;
23 | }
24 |
25 | /**
26 | * used to set or to get the logger object
27 | * @param {Object} the instanced of the logger
28 | * @return {Object} the instanced of the logger
29 | */
30 | parmLogger(_logger = this.logger)
31 | {
32 | this.logger = _logger;
33 | return this.logger;
34 | }
35 |
36 | /**
37 | * used to return an additional log identifier for specifying the log origin
38 | * @return {String} additional log identidier
39 | */
40 | additionalLogIdentifier()
41 | {
42 | return "";
43 | }
44 |
45 | /**
46 | * log to the logger object
47 | * @param {Number} the log type
48 | * @param {String} the log text
49 | * @param {Object} some additional meta data object
50 | */
51 | log(_logType, _log, _metadata = null)
52 | {
53 | if(this.logger && _log)
54 | {
55 | if(this.additionalLogIdentifier())
56 | //this.logger.log(_logType, "\x1b[90m[" + this.additionalLogIdentifier() + "]\x1b[0m" + " " + _log, _metadata);
57 | this.logger.log(_logType, "[" + this.additionalLogIdentifier() + "]" + " " + _log, _metadata);
58 | else
59 | this.logger.log(_logType, _log, _metadata);
60 | }
61 | }
62 |
63 | /**
64 | * log a error
65 | * @param {String} the log text
66 | * @param {Object} some additional meta data object
67 | */
68 | logError(_log, _metadata = null)
69 | {
70 | this.log(0, _log, _metadata);
71 | }
72 |
73 | /**
74 | * log a warning
75 | * @param {String} the log text
76 | * @param {Object} some additional meta data object
77 | */
78 | logWarning(_log, _metadata = null)
79 | {
80 | this.log(1, _log, _metadata);
81 | }
82 |
83 | /**
84 | * log a info
85 | * @param {String} the log text
86 | * @param {Object} some additional meta data object
87 | */
88 | logInfo(_log, _metadata = null)
89 | {
90 | this.log(2, _log, _metadata);
91 | }
92 |
93 | /**
94 | * log a verbose
95 | * @param {String} the log text
96 | * @param {Object} some additional meta data object
97 | */
98 | logVerbose(_log, _metadata = null)
99 | {
100 | this.log(3, _log, _metadata);
101 | }
102 |
103 | /**
104 | * log a debug
105 | * @param {String} the log text
106 | * @param {Object} some additional meta data object
107 | */
108 | logDebug(_log, _metadata = null)
109 | {
110 | this.log(4, _log, _metadata);
111 | }
112 |
113 | /**
114 | * log a silly
115 | * @param {String} the log text
116 | * @param {Object} some additional meta data object
117 | */
118 | logSilly(_log, _metadata = null)
119 | {
120 | this.log(5, _log, _metadata);
121 | }
122 |
123 |
124 | /**
125 | * use this to encode a string for the content directory
126 | * @param {String} the string which should be encoded
127 | * @param {String} encoded string
128 | */
129 | encodeString(_string)
130 | {
131 | try
132 | {
133 | return Tools.encodeString(_string)
134 | }
135 | catch (_exception)
136 | {
137 | this.logError("Error encoding string: " + _string);
138 | }
139 | return "";
140 | }
141 |
142 |
143 | /**
144 | * use this to decode a string for the content directory
145 | * @param {String} the string which should be decoded
146 | * @param {String} decoded string
147 | */
148 | decodeString(_string)
149 | {
150 | try
151 | {
152 | return Tools.decodeString(_string)
153 | }
154 | catch (_exception)
155 | {
156 | this.logError("Error decoding string: " + _string);
157 | }
158 | return "";
159 | }
160 | }
161 |
162 |
--------------------------------------------------------------------------------
/lib/lib.device.upnp.mediaServer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var UPNPDevice = require('./lib.device.base.upnp');
3 |
4 | /**
5 | * this is the base class to use for child's which are media server devices
6 | */
7 | module.exports = class UPNPMediaServer extends UPNPDevice
8 | {
9 | constructor(_upnpClient)
10 | {
11 | super(_upnpClient);
12 | }
13 |
14 |
15 | isRaumfeldServer()
16 | {
17 | return false;
18 | }
19 |
20 |
21 | subscribe()
22 | {
23 | var self = this;
24 | if(!this.upnpClient)
25 | {
26 | this.logError("Trying to subscribe to services on device '" + this.name() + "' without client object");
27 | return;
28 | }
29 |
30 | this.upnpClient.on("error", function(_err) {
31 | self.logError(_err);
32 | });
33 |
34 | this.logVerbose("Set up ContentDirectory subscription on device '" + this.name() + "'")
35 | this.upnpClient.subscribe('ContentDirectory', this.contentDirectorySubscriptionCallback(this));
36 | }
37 |
38 |
39 | unsubscribe()
40 | {
41 | if(!this.upnpClient)
42 | {
43 | this.logError("Trying to un-subscribe services on device '" + this.name() + "' without client object");
44 | return;
45 | }
46 |
47 | this.logVerbose("Remove service subscriptions for device '" + this.name() + "'");
48 | //this.upnpClient.unsubscribe("ContentDirectory", this.contentDirectorySubscriptionCallback(this));
49 | this.upnpClient.unsubscribeAll("ContentDirectory");
50 | }
51 |
52 |
53 | contentDirectorySubscriptionCallback(_self)
54 | {
55 | return function(_data)
56 | {
57 | _self.onContentDirectorySubscription(_data);
58 | }
59 | }
60 |
61 |
62 | /**
63 | * will be called when data of the ContentDirectory service was changed
64 | * @param {Object} a object with the changed data
65 | */
66 | onContentDirectorySubscription(_keyDataArray)
67 | {
68 | this.logDebug("ContentDirectory subscription callback triggered on device '" + this.name() + "'");
69 | this.managerDisposer.mediaListManager.loadMediaItemListsByContainerUpdateIds(this, _keyDataArray["ContainerUpdateIDs"]);
70 | }
71 |
72 |
73 | /**
74 | * enter the standby mode of a room
75 | * @return {Promise} a promise with a result data set
76 | */
77 | browse(_objectId, _browseFlag = "BrowseDirectChildren", _filter = "*", _startingIndex = 0, _requestedCount = 0, _sortCriteria = "")
78 | {
79 | return this.callAction("ContentDirectory", "Browse", { "ObjectID" : _objectId,
80 | "BrowseFlag" : _browseFlag,
81 | "Filter" : _filter,
82 | "StartingIndex" : _startingIndex,
83 | "RequestedCount" : _requestedCount,
84 | "SortCriteria" : _sortCriteria
85 | },
86 | function (_result){
87 | return _result.Result;
88 | }
89 | );
90 | }
91 |
92 |
93 | /**
94 | * enter the standby mode of a room
95 | * @return {Promise} a promise with a result data set
96 | */
97 | search(_objectId, _searchCriteria = "", _filter = "*", _startingIndex = 0, _requestedCount = 0, _sortCriteria = "")
98 | {
99 | return this.callAction("ContentDirectory", "Search", { "ContainerID" : _objectId,
100 | "SearchCriteria" : _searchCriteria,
101 | "Filter" : _filter,
102 | "StartingIndex" : _startingIndex,
103 | "RequestedCount" : _requestedCount,
104 | "SortCriteria" : _sortCriteria
105 | },
106 | function (_result){
107 | return _result.Result;
108 | }
109 | );
110 | }
111 | }
--------------------------------------------------------------------------------
/lib/lib.discoverHostDevice.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Logger = require('./lib.logger');
3 | var BaseManager = require('./lib.base.manager');
4 | var ManagerDisposer = require('./lib.managerDisposer');
5 |
6 | var SsdpClient = require("node-ssdp").Client;
7 |
8 |
9 | module.exports = class DiscoverHostDevice extends BaseManager
10 | {
11 | constructor()
12 | {
13 | super();
14 | this.bonjourClient = null
15 | this.bonjourBrowser = null
16 | this.ssdpClient = null
17 | this.deviceFoundSign = false
18 | this.deviceLostSign = false
19 | }
20 |
21 |
22 | init()
23 | {
24 | var self = this
25 |
26 | self.stopDiscover()
27 | self.createBonjourClient()
28 | self.createSSDPClient();
29 |
30 | }
31 |
32 | createBonjourClient()
33 | {
34 | if(this.bonjourClient)
35 | {
36 | this.bonjourClient.destroy()
37 | this.bonjourClient = null
38 | }
39 | this.bonjourClient = require('bonjour')()
40 | this.createBonjourBrowser()
41 | }
42 |
43 | createBonjourBrowser()
44 | {
45 | var self = this
46 |
47 | if(this.bonjourBrowser)
48 | this.bonjourBrowser.stop()
49 | this.bonjourBrowser = this.bonjourClient.find({})
50 |
51 | this.bonjourBrowser.on("up", function (_service) {
52 | if(_service.fqdn.startsWith("RaumfeldControl"))
53 | {
54 | self.deviceFound(_service.referer.address, _service.fqdn, _service, "BONJOUR")
55 | }
56 | })
57 |
58 | this.bonjourBrowser.on("down", function (_service) {
59 | if(_service.fqdn.startsWith("RaumfeldControl"))
60 | {
61 | self.deviceLost(_service.referer.address, _service.fqdn, _service, "BONJOUR")
62 | }
63 | })
64 | }
65 |
66 |
67 | createSSDPClient()
68 | {
69 | var self = this
70 |
71 | if(this.ssdpClient)
72 | this.ssdpClient.stop()
73 | this.ssdpClient = new SsdpClient({explicitSocketBind : true})
74 |
75 | this.ssdpClient.on('response', function (_headers, _statusCode, _rinfo) {
76 | if(_headers.ST && _headers.ST == "urn:schemas-raumfeld-com:device:ConfigDevice:1")
77 | self.deviceFound(_headers.LOCATION, "", _headers, "SSDP")
78 | });
79 |
80 | this.ssdpClient.on('advertise-alive', function (_headers) {
81 | if(_headers.ST && _headers.ST == "urn:schemas-raumfeld-com:device:ConfigDevice:1")
82 | self.deviceFound(_headers.LOCATION, "", _headers, "SSDP")
83 | });
84 |
85 | this.ssdpClient.on('advertise-bye', function (_headers) {
86 | if(_headers.ST && _headers.ST == "urn:schemas-raumfeld-com:device:ConfigDevice:1")
87 | self.deviceLost(_headers.LOCATION, "", _headers, "SSDP")
88 | });
89 |
90 | self.ssdpClient.search('urn:schemas-raumfeld-com:device:ConfigDevice:1');
91 | }
92 |
93 |
94 | deviceFound(_address, _name, _service, _type)
95 | {
96 | if(!this.deviceFoundSign)
97 | {
98 | this.deviceFoundSign = true
99 | this.emit("deviceFound", { "address" : _address, "name" : _name, "type" : _type, origService: _service })
100 | }
101 | }
102 |
103 | deviceLost(_address, _name, _service, _type)
104 | {
105 | if(!this.devicLostSign)
106 | {
107 | this.deviceLostSign = true
108 | this.emit("deviceLost", { "address" : _address, "name" : _name, "type" : _type, origService: _service })
109 | }
110 | }
111 |
112 |
113 | startDiscover()
114 | {
115 | this.logDebug("Start HOST discovering")
116 | // start discovering with both types of discover (bonjour and ssdp)
117 | // the one who will find the device firts is the winner
118 | if(this.bonjourBrowser)
119 | this.bonjourBrowser.start()
120 | if(this.ssdpClient)
121 | this.ssdpClient.start()
122 | }
123 |
124 |
125 | stopDiscover()
126 | {
127 | this.logDebug("Stop HOST discovering")
128 | if(this.bonjourBrowser)
129 | this.bonjourBrowser.stop()
130 | if(this.ssdpClient)
131 | this.ssdpClient.stop();
132 | }
133 |
134 |
135 | updateDiscover()
136 | {
137 | this.logDebug("Updateing HOST dicovery...")
138 |
139 | // clear found devices list for the update to catch again
140 | this.deviceFoundSign = false
141 | this.deviceLostSign = false
142 |
143 | // recreate bonjour and ssdp client to be sure it works well
144 | this.createBonjourBrowser()
145 | this.bonjourBrowser.start()
146 | this.createSSDPClient()
147 | this.ssdpClient.start()
148 | }
149 |
150 | }
--------------------------------------------------------------------------------
/lib/lib.device.upnp.mediaServer.raumfeld.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var UPNPMediaServer = require('./lib.device.upnp.mediaServer');
3 |
4 | /**
5 | * this is the class which should be used for the raumfeld media server
6 | */
7 | module.exports = class UPNPMediaServerRaumfeld extends UPNPMediaServer
8 | {
9 | constructor(_upnpClient)
10 | {
11 | super(_upnpClient);
12 | }
13 |
14 |
15 | isRaumfeldServer()
16 | {
17 | return true;
18 | }
19 |
20 | /**
21 | * create a queue in a container id
22 | * @param {String} the desired name of the queue
23 | * @param {String} the container id where the queue has to be created
24 | * @return {Promise} a promise with som result
25 | */
26 | createQueue(_desiredName, _containerId)
27 | {
28 | return this.callAction("ContentDirectory", "CreateQueue", { "DesiredName" : (_desiredName), "ContainerID" : (_containerId) });
29 | }
30 |
31 | /**
32 | * add a container or parts of containers into a queue
33 | * @param {String} the queue id
34 | * @param {String} the container id
35 | * @param {String} the source id
36 | * @return {Promise} a promise with some result
37 | */
38 | addContainerToQueue(_queueId, _containerId, _sourceId, _searchCriteria = "*", _sortCriteria = "", _startIndex = 0, _endIndex = 294967295, _position = 0)
39 | {
40 | return this.callAction("ContentDirectory", "AddContainerToQueue", { "QueueID" : (_queueId), "ContainerID" : (_containerId), "SourceID" : (_sourceId), "SearchCriteria" : _searchCriteria, "SortCriteria" : _sortCriteria, "StartIndex" : _startIndex, "EndIndex" : _endIndex, "Position" : _position });
41 | }
42 |
43 | /**
44 | * add one item into a queue
45 | * @param {String} the queue id
46 | * @param {String} the objectId which has to be inserted
47 | * @param {Integer} the position where the item has to be inserted
48 | * @return {Promise} a promise with some result
49 | */
50 | addItemToQueue(_queueId, _objectId, _position = 0)
51 | {
52 | return this.callAction("ContentDirectory", "AddItemToQueue", { "QueueID" : (_queueId), "ObjectID" : (_objectId), "Position" : _position });
53 | }
54 |
55 | /**
56 | * remove items from queue
57 | * @param {String} the queue id
58 | * @param {Integer} from position
59 | * @param {Integer} to position
60 | * @return {Promise} a promise with some result
61 | */
62 | removeFromQueue(_queueId, _fromPosition, _toPosition)
63 | {
64 | return this.callAction("ContentDirectory", "RemoveFromQueue", { "QueueID" : (_queueId), "FromPosition" : _fromPosition, "ToPosition" : _toPosition });
65 | }
66 |
67 | /**
68 | * rename queue
69 | * @param {String} the queue id
70 | * @param {String} the desired name
71 | * @return {Promise} a promise with some result
72 | */
73 | renameQueue(_queueId, _desiredName)
74 | {
75 | return this.callAction("ContentDirectory", "RenameQueue", { "QueueID" : (_queueId), "DesiredName" : _desiredName });
76 | }
77 |
78 |
79 | /**
80 | * move an object in a queue
81 | * @param {String} the object id (which includes the queue id)
82 | * @param {Integer} the new position of the object in the queue
83 | * @return {Promise} a promise with some result
84 | */
85 | moveInQueue(_objectID, _newPosition)
86 | {
87 | return this.callAction("ContentDirectory", "MoveInQueue", { "ObjectID" : (_objectID), "NewPosition" : _newPosition });
88 | }
89 |
90 | /**
91 | * destroys an object
92 | * @param {String} the object id to destroy
93 | * @return {Promise} a promise with some result
94 | */
95 | destroyObject(_objectID)
96 | {
97 | return this.callAction("ContentDirectory", "DestroyObject", { "ObjectID" : (_objectID)});
98 | }
99 |
100 |
101 | /**
102 | * creates a shuffle playlist
103 | * @param {String} container id where the shuffle should be generated from
104 | * @param {String} shuffle selection
105 | * @return {Promise} a promise with the playlist id and the playlist metadata
106 | */
107 | shuffle(_shuffleContainerId, _shuffleSelection)
108 | {
109 | return this.callAction("ContentDirectory", "Shuffle", { "ContainerID" : _shuffleContainerId, "Selection" : _shuffleSelection });
110 | }
111 |
112 |
113 | /**
114 | * assigns a station button for a renderer
115 | * @param {String} the renderer id for what the station button shoul be set
116 | * @param {Integer} the station button
117 | * @param {String} the object id to store
118 | * @return {Promise} a promise with some result
119 | */
120 | assignStationButton(_rendererId, _buttonNum, _objectId)
121 | {
122 | return this.callAction("ContentDirectory", "AssignStationButton", { "Renderer" : (_rendererId), "Button" : _buttonNum, "ObjectId" : _objectId});
123 | }
124 |
125 | /**
126 | * get the button assignement
127 | * @param {String} the renderer id for what the station button shoul be set
128 | * @param {Integer} the station button
129 | * @return {Promise} a promise with some result
130 | */
131 | getStationButtonAssignment(_rendererId, _buttonNum)
132 | {
133 | return this.callAction("ContentDirectory", "GetStationButtonAssignment", { "Renderer" : (_rendererId), "Button" : _buttonNum});
134 | }
135 |
136 |
137 | // TODO: @@@
138 | // RescanSource
139 | // QueryDatabaseState
140 | // GetSourceInfo
141 |
142 | }
--------------------------------------------------------------------------------
/lib/lib.device.upnp.mediaRenderer.raumfeld.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var UPNPMediaRenderer = require('./lib.device.upnp.mediaRenderer');
3 |
4 | /**
5 | * this is the class for a virtual media renderer
6 | */
7 | module.exports = class UPNPMediaRendererRaumfeld extends UPNPMediaRenderer
8 | {
9 | constructor(_upnpClient)
10 | {
11 | super(_upnpClient);
12 | }
13 |
14 |
15 | roomName()
16 | {
17 | // the room name is given in the zone configuration file, so every time we want to have the room name
18 | // we have to look up there. There is no other way of getting the room name
19 | return this.managerDisposer.zoneManager.getRoomNameForMediaRendererUDN(this.udn());
20 | }
21 |
22 | roomUdn()
23 | {
24 | return this.managerDisposer.zoneManager.getRoomUdnForMediaRendererUDN(this.udn());
25 | }
26 |
27 |
28 | isRaumfeldRenderer()
29 | {
30 | return true;
31 | }
32 |
33 | /**
34 | * starts a timer which will fade the renderer volume to a specific value
35 | * @return {Promise} a promise with nothing
36 | */
37 | async fadeToVolume(_desiredVolume, _duration = 2000)
38 | {
39 |
40 | try
41 | {
42 | var self = this;
43 |
44 | // await the current volume from the renderer
45 | var currentVolume = await this.getVolume();
46 |
47 | // create a new promise so that our caller can block and knows when we are finished
48 | return new Promise(function(_resolve, _reject){
49 |
50 | try
51 | {
52 | _desiredVolume = parseInt(_desiredVolume);
53 | _duration = parseInt(_duration);
54 | currentVolume = parseInt(currentVolume);
55 |
56 | var volDifference = (_desiredVolume - currentVolume);
57 | var currentFadeVolume = currentVolume;
58 |
59 | // if there is no difference, the return
60 | if(!volDifference)
61 | _resolve({});
62 |
63 | // calculate the time in MS for one volume step
64 | var timeForOneVolStep = _duration / Math.abs(volDifference);
65 |
66 | self.logDebug("Set fade to volume interval step to : " + timeForOneVolStep);
67 |
68 | // set an interval for the volume 1 step
69 | var volStepInterval = setInterval(function(){
70 | if(volDifference > 0)
71 | currentFadeVolume += 1
72 | if(volDifference < 0)
73 | currentFadeVolume -= 1
74 | self.setVolume(currentFadeVolume);
75 |
76 | // if the volume is reached stop the interval
77 | if(currentFadeVolume == _desiredVolume || currentFadeVolume <= 0 || currentFadeVolume >= 100)
78 | {
79 | clearInterval(volStepInterval);
80 | _resolve({});
81 | }
82 |
83 | }, timeForOneVolStep);
84 | }
85 | catch(exception)
86 | {
87 | self.logError("Error when fading volume on " + this.name());
88 | _reject(exception);
89 | }
90 | });
91 | }
92 | catch(_exception)
93 | {
94 | this.logError("Error when fading volume on " + this.name());
95 | throw (_exception);
96 | }
97 | }
98 |
99 |
100 | /**
101 | * enter the standby mode of a room
102 | * @param {String} the UDN of the room which should go into standby mode
103 | * @return {Promise} a promise with no result
104 | */
105 | enterManualStandby()
106 | {
107 | return this.callAction("AVTransport", "EnterManualStandby", {});
108 | }
109 |
110 |
111 | /**
112 | * enter the automatic standby mode of a room
113 | * @param {String} the UDN of the room which should go into standby mode
114 | * @return {Promise} a promise with no result
115 | */
116 | enterAutomaticStandby()
117 | {
118 | return this.callAction("AVTransport", "EnterAutomaticStandby", {});
119 | }
120 |
121 |
122 | /**
123 | * enter the standby mode of a room
124 | * @param {String} the UDN of the room which should go into standby mode
125 | * @return {Promise} a promise with no result
126 | */
127 | leaveStandby(_confirm = false)
128 | {
129 | //return this.callAction("AVTransport", "LeaveStandby", {});
130 | return this.callActionWithTriggerWait("AVTransport", "LeaveStandby", {}, null, { "key" : "PowerState", "values" : ["ACTIVE", "IDLE"] } , _confirm)
131 | }
132 |
133 |
134 | /**
135 | * change the current volume for the renderer (for the full zone)
136 | * @param {Integer} the desired volume
137 | * @return {Promise} a promise with no result
138 | */
139 | changeVolume(_amount)
140 | {
141 | return this.callAction("RenderingControl", "ChangeVolume", {"Amount": _amount});
142 | }
143 |
144 |
145 | /**
146 | * change the current volume for the renderer (for the full zone)
147 | * @param {String} the sound id (Failure, Success)
148 | * @return {Promise} a promise with no result
149 | */
150 | playSystemSound(_soundId)
151 | {
152 | return this.callAction("RenderingControl", "PlaySystemSound", {"Sound": _soundId});
153 | }
154 |
155 |
156 | /**
157 | * returns the lineIns Stream url
158 | * @return {Promise} a promise with url and mimetype
159 | */
160 | getLineInStream()
161 | {
162 | return this.callAction("RenderingControl", "GetLineInStreamURL", {});
163 | }
164 |
165 |
166 | /**
167 | * sets the equalizer values
168 | * @return {Promise} a promise with nothing
169 | */
170 | setFilter(_lowDB, _midDB, _highDB)
171 | {
172 | return this.callAction("RenderingControl", "SetFilter", {"LowDB" : _lowDB, "MidDB" : _midDB, "HighDB" : _highDB});
173 | }
174 |
175 |
176 | /**
177 | * get the equalizer values
178 | * @return {Promise} a promise with the equalizer data
179 | */
180 | getFilter()
181 | {
182 | return this.callAction("RenderingControl", "GetFilter", {});
183 | }
184 |
185 |
186 | /**
187 | * sets a device setting
188 | * @param {string} id of the setting
189 | * SounbarId's : "Source Select" --> LineIn, OpticalIn, TV_ARC, Raumfeld
190 | * "Audio Mode" --> Arena, Voice, Theater ???
191 | * "Subwoofer Playback Volume" --> ???
192 | * "Subwoofer X-Over" --> ???
193 | * and many more???
194 | * @param {string} the value for the id
195 | * @return {Promise} a promise with nothing
196 | */
197 | setDeviceSetting(_key, _value)
198 | {
199 | return this.callAction("RenderingControl", "SetDeviceSetting", {"Name" : _key, "Value" : _value });
200 | }
201 |
202 |
203 | /**
204 | * get a device setting
205 | * @return {Promise} a promise with nothing
206 | */
207 | getDeviceSetting(_key)
208 | {
209 | return this.callAction("RenderingControl", "GetDeviceSetting", {"Name" : _key });
210 | }
211 |
212 |
213 | // AvTransport -->
214 | // setNextStartTriggerTime
215 |
216 | // RenderingControl -->
217 | // Get Balance
218 | // SetBalance
219 | // QueryFilter
220 | // ToggleFilter
221 | // SetVolumeDB
222 | // setNextAvTransportUri
223 | }
--------------------------------------------------------------------------------
/lib/lib.manager.triggerManager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Url = require('url');
3 | var ManagerBase = require('./lib.manager.base');
4 |
5 |
6 | module.exports = class TriggerManager extends ManagerBase
7 | {
8 | constructor()
9 | {
10 | super()
11 | this.triggers = {}
12 | }
13 |
14 | additionalLogIdentifier()
15 | {
16 | return "TriggerManager";
17 | }
18 |
19 | /**
20 | * trigger a change in a state of the system (renderer, zones, ...)
21 | * @param {String} context of the trigger
22 | * @param {String} type of the trigger
23 | * @param {Object} dta for the trigger
24 | * @return {Promise} ???
25 | */
26 | trigger(_context, _type, _data)
27 | {
28 | var triggerFound = true;
29 | // search triggers for valid trigger and do a shot if found. If its a one shot trigger, then remove it from the array
30 | // i have to keep an eye on the performance here...
31 | var mapKey = this.createTriggerKey(_context, _type, _data)
32 | //while(triggerFound) // oh my goodness... endless loop :-)
33 | {
34 | if(this.triggers[mapKey] && this.triggers[mapKey].length)
35 | {
36 | var triggerArray = this.triggers[mapKey]
37 | var i = triggerArray.length
38 | while (i--)
39 | {
40 | var keyValueIsSet = false
41 | if(_context == "renderer" && _type == "rendererStateKeyValueChanged")
42 | {
43 | // we have to bes sure we trigger for the right renderer
44 | if(triggerArray[i].data.rendererUdn && triggerArray[i].data.rendererUdn != _data.rendererUdn)
45 | continue
46 |
47 | // we may have ranged a room in the trigger event, so check it
48 | if(triggerArray[i].data.roomUdn && triggerArray[i].data.roomUdn != _data.roomUdn)
49 | continue
50 |
51 | if(triggerArray[i].data.values)
52 | {
53 | for(var x=0; x 0)
235 | {
236 | var mediaRenderer = self.managerDisposer.deviceManager.getVirtualMediaRenderer(_rendererUdn);
237 | if (mediaRenderer)
238 | {
239 | // in this case we came here the rendere should have a queue (container) refereced so we can use this queue
240 | //mediaRenderer.loadContainer(mediaRenderer.mediaOriginData.containerId, "", foundTrackNumber, true);
241 | mediaRenderer.loadContainer(self.getQueueIdFromNameAndBase(self.getQueueNameFromRendererUdn(_rendererUdn)), "", foundTrackNumber, true).then(function(_data){
242 | _resolve(foundTrackNumber);
243 | }).catch(function(_error){
244 | _reject(_error);
245 | });
246 | }
247 | else
248 | {
249 | _reject(new Error("No media renderer with udn '" + _rendererUdn + "' found!"));
250 | }
251 | return;
252 | }
253 |
254 | // we came here if the track number was not found, that means we may have deleted it?
255 | // TODO: not really sure what we should do here, we may set the query again with the current track number (no binding)
256 | _resolve(foundTrackNumber);
257 |
258 | }).catch(function(_data){
259 | _reject(_data);
260 | });
261 | });
262 | }
263 |
264 | }
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Raumkernel = require('./lib/lib.raumkernel');
3 |
4 | var raumkernel = new Raumkernel();
5 |
6 | raumkernel.settings.raumfeldHost = "0.0.0.0"
7 |
8 | raumkernel.createLogger(5);
9 | raumkernel.logger.on('log', (_logData) =>{
10 | console.log(`${_logData.logType}: ${_logData.log}`);
11 | })
12 | raumkernel.init();
13 |
14 | /*
15 | raumkernel.on("zoneCreated", function(_zoneUDN) { raumkernel.logWarning("Zone created: " + _zoneUDN); });
16 | raumkernel.on("zoneRemoved", function(_zoneUDN) { raumkernel.logError("Zone removed: " + _zoneUDN); });
17 | raumkernel.on("roomAddedToZone", function(_zoneUDN, _roomUDN) { raumkernel.logWarning("Room: " + _roomUDN + " added to zone: " + _zoneUDN); });
18 | raumkernel.on("roomRemovedFromZone", function(_zoneUDN, _roomUDN) { raumkernel.logError("Room: " + _roomUDN + " removed from zone: " + _zoneUDN); });
19 | */
20 |
21 |
22 |
23 | raumkernel.on("systemReady", function(_ready){
24 | raumkernel.logInfo("System ready: " + _ready);
25 |
26 | //
27 |
28 |
29 |
30 | //raumkernel.logWarning("Try create playlist");
31 | //raumkernel.nativePlaylistController.createPlaylist("RAUMKERNELTEST").then(function(){
32 | //raumkernel.logWarning("Try rename playlist");
33 | //raumkernel.nativePlaylistController.renamePlaylist("RAUMKERNELTEST", "RAUMKERNELTEST X")
34 |
35 | //raumkernel.logWarning("Add a container item to playlist");
36 | //raumkernel.nativePlaylistController.addItemToPlaylist("RAUMKERNELTEST", "0/My Music/Artists/4%20Non%20Blondes/4%20Non%20Blondes+What%27s%20Up", 294967295, true);
37 |
38 | //raumkernel.logWarning("Add one item to playlist");
39 | //raumkernel.nativePlaylistController.addItemToPlaylist("RAUMKERNELTEST", "0/My Music/Artists/Dido/Dido+No%20Angel/c7e7ad4423927a75c5017b2640db6574");
40 |
41 | //raumkernel.logWarning("Mobe item in playlist");
42 | //raumkernel.nativePlaylistController.moveItemInPlaylist("RAUMKERNELTEST", "0/Playlists/MyPlaylists/RAUMKERNELTEST/31990", 1);
43 |
44 | //raumkernel.logWarning("remove items from playlist");
45 | //raumkernel.nativePlaylistController.removeItemsFromPlaylist("RAUMKERNELTEST", 1, 1);
46 |
47 | //var mediaRenderer = raumkernel.managerDisposer.deviceManager.getVirtualMediaRenderer("Küche")
48 |
49 | //var rendererUdns = mediaRenderer.getRoomRendererUDNs();
50 | //console.log(JSON.stringify(rendererUdns));
51 | //mediaRenderer.loadPlaylist("Rock", 2).catch(function(_data){
52 | // console.log(_data.toString());
53 | //});
54 |
55 | /*mediaRenderer.loadUri("http://mp3channels.webradio.rockantenne.de/heavy-metal").catch(function(_data){
56 | console.log(_data.toString());
57 | });*/
58 |
59 | /*
60 | if (source == "recentartists")
61 | source = "0/Playlists/Shuffles/RecentArtists";
62 | if (source == "topartists")
63 | source = "0/Playlists/Shuffles/TopArtists";
64 | if (source == "all")
65 | source = "0/Playlists/Shuffles/All";
66 | // on following types we can add selections
67 | if (source == "genre")
68 | source = "0/Playlists/Shuffles/Genre";
69 | if (source == "genre")
70 | source = "0/Playlists/Shuffles/Artists";
71 |
72 |
73 | object.container.playlistContainer.shuffle 0/Playlists/Shuffles/RecentArtists
74 | object.container.playlistContainer.shuffle 0/Playlists/Shuffles/TopArtists
75 | object.container.playlistContainer.shuffle 0/Playlists/Shuffles/All
76 | object.container.playlistContainer.shuffle.search 0/Playlists/Shuffles/Genre
77 | object.container.playlistContainer.shuffle.search 0/Playlists/Shuffles/Artists
78 | */
79 |
80 |
81 | //mediaRenderer.loadShuffle("0/Playlists/Shuffles/All", "").catch(function(_data){
82 | // console.log(_data.toString());
83 | //});
84 |
85 |
86 | setTimeout(function(){
87 | /*
88 | raumkernel.logWarning("Trying to add a media item to a zone playlist");
89 | raumkernel.zonePlaylistController.addItemToPlaylist(mediaRenderer.udn(), "0/My Music/Artists/Dido/Dido+No%20Angel/c7e7ad4423927a75c5017b2640db6574", 0).then(function(_data){
90 | raumkernel.logWarning(_data);
91 | }).catch(function(_data){
92 | raumkernel.logWarning(_data);
93 | });
94 | */
95 |
96 | var mediaRenderer = raumkernel.managerDisposer.deviceManager.getMediaRenderer("Küche")
97 | var mediaRendererVirtual = raumkernel.managerDisposer.deviceManager.getVirtualMediaRenderer("Küche")
98 | mediaRendererVirtual.leaveStandby(raumkernel.managerDisposer.zoneManager.getRoomUdnForMediaRendererUDN(mediaRenderer.udn()), true).then(function(){
99 | var nice = ""
100 | }).catch(function(){
101 | var nice = ""
102 | })
103 |
104 |
105 |
106 | //var mediaRendererK = raumkernel.managerDisposer.deviceManager.getVirtualMediaRenderer("Küche")
107 | //var mediaRendererB = raumkernel.managerDisposer.deviceManager.getVirtualMediaRenderer("Bad")
108 | //var rendererUdns = mediaRenderer.getRoomRendererUDNs();
109 | //console.log(JSON.stringify(rendererUdns));
110 | //mediaRenderer.loadLineIn("Schlafzimmer");
111 |
112 | var confirm = true;
113 |
114 | /*
115 |
116 | mediaRendererK.loadPlaylist("Rock", 2, confirm).then(function(_data){
117 | raumkernel.logWarning("Playlist Rock loaded");
118 | mediaRendererK.loadShuffle("0/Playlists/Shuffles/All", "", false, confirm).then(function(_data){
119 | raumkernel.logWarning("Schuffle All loaded");
120 | mediaRendererK.loadUri("http://mp3channels.webradio.rockantenne.de/heavy-metal", false, confirm).then(function(_data){
121 | raumkernel.logWarning("Rockantenne loaded");
122 | mediaRendererK.loadLineIn("Schlafzimmer", confirm).then(function(_data){
123 | raumkernel.logWarning("Line In loaded");
124 | mediaRendererK.loadPlaylist("Rock", 2, confirm).then(function(_data){
125 | raumkernel.logWarning("Done!!!", _data);
126 | }).catch(function(_data){
127 | raumkernel.logError("Catched", _data);
128 | });
129 | }).catch(function(_data){
130 | raumkernel.logError("Catched", _data);
131 | });
132 | }).catch(function(_data){
133 | raumkernel.logError("Catched", _data);
134 | });
135 | }).catch(function(_data){
136 | raumkernel.logError("Catched", _data);
137 | });
138 | }).catch(function(_data){
139 | raumkernel.logError("Catched", _data);
140 | });
141 |
142 | */
143 |
144 | /*mediaRenderer.loadUri("http://mp3channels.webradio.rockantenne.de/heavy-metal").catch(function(_data){
145 | console.log(_data.toString());
146 | });*/
147 |
148 | //mediaRenderer.loadShuffle("0/Playlists/Shuffles/All", "").catch(function(_data){
149 | // console.log(_data.toString());
150 | //});
151 |
152 |
153 | /*
154 | mediaRendererK.next(confirm).then(function(_data){
155 | mediaRendererK.prev(confirm).then(function(_data){
156 | raumkernel.logWarning("Done!!!", _data);
157 | }).catch(function(_data){
158 | raumkernel.logError("Catched", _data);
159 | });
160 | }).catch(function(_data){
161 | raumkernel.logError("Catched", _data);
162 | });
163 | */
164 |
165 |
166 | /*
167 | raumkernel.logInfo("SetMute true");
168 | mediaRendererK.setMute(true, confirm).then(function(_data){
169 | raumkernel.logInfo("SetVolume 33");
170 | mediaRendererK.setVolume(33, confirm).then(function(_data){
171 | //mediaRendererK.setMute(true, confirm).then(function(_data){
172 | raumkernel.logInfo("SetMute false");
173 | mediaRendererK.setMute(false, confirm).then(function(_data){
174 | raumkernel.logInfo("SetVolume 22");
175 | mediaRendererK.setVolume(22, confirm).then(function(_data){
176 | raumkernel.logWarning("Done!!!", _data);
177 | }).catch(function(_data){
178 | raumkernel.logError("Catched", _data);
179 | });
180 | }).catch(function(_data){
181 | raumkernel.logError("Catched", _data);
182 | });
183 | }).catch(function(_data){
184 | raumkernel.logError("Catched", _data);
185 | });
186 | }).catch(function(_data){
187 | raumkernel.logError("Catched", _data);
188 | });
189 | */
190 |
191 | /*
192 | mediaRendererB.pause(true).then(function(_data){
193 | raumkernel.logWarning("Paused", _data);
194 |
195 | mediaRendererB.play(true).then(function(_data){
196 | raumkernel.logWarning("Playing", _data);
197 |
198 | mediaRendererB.setPlayMode("REPEAT_ALL", true).then(function(_data){
199 | raumkernel.logWarning("REPEAT_ALL", _data);
200 | }).catch(function(_data){
201 | raumkernel.logError("Catched", _data);
202 | });
203 |
204 | }).catch(function(_data){
205 | raumkernel.logError("Catched", _data);
206 | });
207 |
208 | }).catch(function(_data){
209 | raumkernel.logError("Catched", _data);
210 | });
211 | */
212 |
213 |
214 |
215 |
216 | }, 5000);
217 |
218 |
219 |
220 |
221 | /*setTimeout(function(){
222 |
223 | raumkernel.logWarning("Trying to move a media item in a zone playlist");
224 | raumkernel.zonePlaylistController.moveItemInPlaylist(mediaRenderer.udn(), "0/Zones/uuid%3A00000000-5416-48eb-0000-0000541648eb/33761", 1).then(function(_data){
225 | raumkernel.logWarning(_data);
226 | }).catch(function(_data){
227 | raumkernel.logWarning(_data);
228 | });
229 |
230 |
231 | }, 15000);*/
232 |
233 |
234 | /*
235 | setTimeout(function(){
236 |
237 | raumkernel.logWarning("Trying to delete first two item of zone playlist");
238 | raumkernel.zonePlaylistController.removeItemsFromPlaylist(mediaRenderer.udn(), 0, 1).then(function(_data){
239 | raumkernel.logWarning(_data);
240 | }).catch(function(_data){
241 | raumkernel.logWarning(_data);
242 | });
243 |
244 |
245 | }, 15000);*/
246 |
247 |
248 |
249 |
250 | //});
251 | //raumkernel.nativePlaylistController.deletePlaylist("RAUMKERNELTEST (2)").catch(function(){});
252 | //raumkernel.nativePlaylistController.deletePlaylist("RAUMKERNELTEST");
253 | //raumkernel.nativePlaylistController.deletePlaylist("RAUMKERNELTEST X");
254 |
255 | });
256 |
257 | raumkernel.on("mediaListReady", function(_listId, _data){
258 | //raumkernel.logInfo("MediaList ready: " + _listId);
259 | //raumkernel.logWarning(JSON.stringify(_data));
260 | });
261 |
262 | raumkernel.on("rendererMediaItemDataChanged", function(_mediaRenderer, _data){
263 | //raumkernel.logInfo("MediaItem changed: " + JSON.stringify(_data));
264 | //raumkernel.logWarning(JSON.stringify(_data));
265 | });
266 |
267 | raumkernel.on("mediaRendererPlaylistReady", function(_rendererUdn, _data){
268 | //raumkernel.logInfo("mediaRendererPlaylistReady ready: " + _rendererUdn);
269 | //raumkernel.logWarning(JSON.stringify(_data));
270 | });
271 |
272 | raumkernel.on("mediaServerRaumfeldAdded", function(_udn, _mediaServer){
273 |
274 | //raumkernel.logWarning("Raumfeld Media Server found!");
275 | /*
276 | _mediaServer.browse("0").then(function(_data){
277 | raumkernel.logWarning(JSON.stringify(_data));
278 | })
279 |
280 | _mediaServer.browse("0/My Music").then(function(_data){
281 | raumkernel.logWarning(JSON.stringify(_data));
282 | })
283 | */
284 | /*
285 | raumkernel.managerDisposer.mediaListManager.searchMediaList("DUMMYLISTID", "0/RadioTime/Search", "OE3").then(function(_data){
286 | raumkernel.logWarning(JSON.stringify(_data));
287 | })
288 | */
289 |
290 |
291 | /*
292 | raumkernel.managerDisposer.mediaListManager.getMediaList("0", 0).then(function(_data){
293 | raumkernel.logWarning(JSON.stringify(_data));
294 | }).catch(function(_data){
295 | raumkernel.logError(JSON.stringify(_data));
296 | });
297 | */
298 |
299 | });
300 |
301 |
302 | function execute(){
303 |
304 | }
305 |
306 | setInterval(execute,1000);
--------------------------------------------------------------------------------
/lib/lib.mediaDataConverter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var BaseManager = require('./lib.base.manager');
3 | var ParseString = require('xml2js').parseString;
4 |
5 | module.exports = class Raumkernel extends BaseManager
6 | {
7 | constructor()
8 | {
9 | super();
10 | }
11 |
12 | additionalLogIdentifier()
13 | {
14 | return "MediaDataConverter";
15 | }
16 |
17 |
18 | convertXMLToMediaList(_xmlString)
19 | {
20 | var self = this;
21 |
22 | return new Promise(function(_resolve, _reject){
23 |
24 | // direct conversion to json
25 | ParseString(_xmlString, function (_err, _result) {
26 | if(!_err && _result)
27 | {
28 | try
29 | {
30 | var jsonMediaList = [];
31 | var containerId = "";
32 |
33 | if(!_result["DIDL-Lite"])
34 | {
35 | self.logError("Wrong formatted XML", { "xml": _xmlString } );
36 | _reject(new Error("Wrong formatted XML"));
37 | return;
38 | }
39 |
40 | // we may get an empty list
41 | if((!_result["DIDL-Lite"].item && !_result["DIDL-Lite"].container))
42 | {
43 | self.logDebug("Got empty list");
44 | _resolve(jsonMediaList);
45 | return;
46 | }
47 |
48 | // The result is a direct conversion from an xml to a json object.
49 | // That's ok but ist not very nice for handling, so we do a conversion.
50 | // If we will get into a performance problem we should consider to change this code to parse directly into a nice
51 | // json format without the step xml to xml-json to nice-json
52 |
53 | // there may be 2 types of containers that are returned. "items"" and "containers" and both may may be in the same result
54 | // so we have to 'loop' over the 2 types beginning with the containers
55 | for(var type=1; type<=2; type++)
56 | {
57 |
58 | containerId = "";
59 | if(type === 1 && _result["DIDL-Lite"].container)
60 | containerId = "container";
61 | if(type === 2 && _result["DIDL-Lite"].item)
62 | containerId = "item";
63 |
64 | if(containerId)
65 | {
66 | var containerArray = _result["DIDL-Lite"][containerId];
67 | for (var item of containerArray)
68 | {
69 | try
70 | {
71 | jsonMediaList.push(self.convertContainer(item));
72 | }
73 | catch(_exception)
74 | {
75 | // to keep the correct size of the array we put a dummy media info into the lists
76 | jsonMediaList.push({ "title": "UNKNOWN" });
77 | self.logError("Error converting media item: " + JSON.stringify(item), _exception);
78 | }
79 | }
80 | }
81 | }
82 |
83 |
84 | _resolve(jsonMediaList);
85 |
86 | }
87 | catch(_exception)
88 | {
89 | self.logError("Error converting media item list", { "xml": _xmlString } );
90 | _reject(_exception);
91 | }
92 | }
93 | else
94 | {
95 | self.logError("Error parsing media item list", { "xml": _xmlString } );
96 | _reject(new Error("Error parsing media item list"));
97 | }
98 | });
99 | });
100 | }
101 |
102 |
103 | convertContainer(_mediaContainer)
104 | {
105 | var newObject = {};
106 |
107 | // copy all the main keys
108 | this.copyRootData(newObject, _mediaContainer);
109 |
110 | switch (newObject.class.toLowerCase())
111 | {
112 | case "object.container":
113 | this.copyContainerData(newObject, _mediaContainer);
114 | break;
115 | case "object.container.person.musicartist":
116 | this.copyArtistData(newObject, _mediaContainer);
117 | break;
118 | case "object.item.audioitem.musictrack":
119 | this.copyTrackData(newObject, _mediaContainer);
120 | break;
121 | case "object.container.album.musicalbum":
122 | this.copyAlbumData(newObject, _mediaContainer);
123 | break;
124 | case "object.container.trackcontainer.alltracks":
125 | this.copyAlbumData(newObject, _mediaContainer);
126 | break;
127 | case "object.item.audioitem.audiobroadcast.radio":
128 | this.copyRadioData(newObject, _mediaContainer);
129 | break;
130 | case "object.container.favoritescontainer":
131 | this.copyFavouritesContainerData(newObject, _mediaContainer);
132 | break;
133 | case "object.container.playlistcontainer":
134 | this.copyPlaylistContainerData(newObject, _mediaContainer);
135 | break;
136 | case "object.item.audioitem.audiobroadcast.linein":
137 | this.copyLineInData(newObject, _mediaContainer);
138 | break;
139 | case "object.container.album.musicalbum.compilation":
140 | this.copyMusicalbumCompilationData(newObject, _mediaContainer);
141 | break;
142 | case "object.container.playlistcontainer.shuffle":
143 | this.copyPlaylistcontainerShuffleData(newObject, _mediaContainer);
144 | break;
145 | case "object.container.playlistcontainer.queue":
146 | this.copyPlaylistcontainerQueueData(newObject, _mediaContainer);
147 | break;
148 | case "object.container.albumcontainer":
149 | this.copyAlbumcontainerData(newObject, _mediaContainer);
150 | break;
151 | case "object.container.genre.musicgenre":
152 | this.copyGenreMusicgenreData(newObject, _mediaContainer);
153 | break;
154 | case "object.container.person.musiccomposer":
155 | this.copyMusiccomposerData(newObject, _mediaContainer);
156 | break;
157 | case "object.container.storagefolder":
158 | this.copyStorageFolderData(newObject, _mediaContainer);
159 | break;
160 | case "object.container.playlistcontainer.shuffle.search":
161 | this.copyPlaylistcontainerShuffleSearchData(newObject, _mediaContainer);
162 | break;
163 | case "object.container.trackcontainer":
164 | this.copyTrackContainerData(newObject, _mediaContainer);
165 | break;
166 | case "object.item.audioitem.audiobroadcast.rhapsody":
167 | this.copyAudiobroadcastRhapsodyData(newObject, _mediaContainer);
168 | break;
169 | default:
170 | this.logWarning("Unhandled media item type: " + newObject.class.toLowerCase(), _mediaContainer);
171 | this.copyContainerData(newObject, _mediaContainer);
172 | }
173 |
174 | return newObject;
175 | }
176 |
177 |
178 | getData(_object, _id, _stdValue = null)
179 | {
180 | if(_object[_id])
181 | {
182 | if(_object[_id].length)
183 | return _object[_id][0];
184 | else
185 | return _object[_id];
186 | }
187 | return _stdValue;
188 | }
189 |
190 |
191 | copyRootData(_newObject, _mediaContainer)
192 | {
193 | _newObject["class"] = this.getData(_mediaContainer, "upnp:class");
194 | _newObject["section"] = this.getData(_mediaContainer, "raumfeld:section");
195 | _newObject["name"] = this.getData(_mediaContainer, "raumfeld:name");
196 | _newObject["durability"] = this.getData(_mediaContainer, "raumfeld:durability");
197 | _newObject["childCount"] = this.getData(_mediaContainer, "childCount");
198 |
199 | for(var key in _mediaContainer.$)
200 | {
201 | _newObject[key] = _mediaContainer.$[key];
202 | }
203 | }
204 |
205 |
206 | copyContainerData(_newObject, _mediaContainer)
207 | {
208 | _newObject["title"] = this.getData(_mediaContainer, "dc:title");
209 | _newObject["description"] = this.getData(_mediaContainer, "dc:description");
210 | }
211 |
212 |
213 | copyArtistData(_newObject, _mediaContainer)
214 | {
215 | this.copyContainerData(_newObject, _mediaContainer);
216 | _newObject["artist"] = this.getData(_mediaContainer, "upnp:artist");
217 |
218 | // if there is an "album art uri" then use it
219 | if(_mediaContainer["upnp:albumArtURI"] && _mediaContainer["upnp:albumArtURI"][0])
220 | _newObject["albumArtURI"] = _mediaContainer["upnp:albumArtURI"][0]._;
221 | }
222 |
223 |
224 | copyAlbumData(_newObject, _mediaContainer)
225 | {
226 | this.copyArtistData(_newObject, _mediaContainer);
227 | this.copyGenreData(_newObject, _mediaContainer);
228 | _newObject["album"] = this.getData(_mediaContainer, "upnp:album");
229 | _newObject["date"] = this.getData(_mediaContainer, "dc:date");
230 | _newObject["creator"] = this.getData(_mediaContainer, "dc:creator");
231 | }
232 |
233 |
234 | copyGenreData(_newObject, _mediaContainer)
235 | {
236 | _newObject["genre"] = this.getData(_mediaContainer, "dc:genre");
237 | }
238 |
239 |
240 | copyTrackData(_newObject, _mediaContainer)
241 | {
242 | this.copyAlbumData(_newObject, _mediaContainer);
243 |
244 | _newObject["originalTrackNumber"] = this.getData(_mediaContainer, "upnp:originalTrackNumber");
245 |
246 | // if there is a "res" section the copy the data from there
247 | if(_mediaContainer["res"] && _mediaContainer["res"].length)
248 | {
249 | for(var key in _mediaContainer.res[0].$)
250 | {
251 | _newObject[key] = _mediaContainer.res[0].$[key];
252 | }
253 | }
254 | }
255 |
256 |
257 | copyRadioData(_newObject, _mediaContainer)
258 | {
259 | this.copyTrackData(_newObject, _mediaContainer);
260 |
261 | _newObject["signalStrength"] = this.getData(_mediaContainer, "upnp:signalStrength");
262 | _newObject["ebrowse"] = this.getData(_mediaContainer, "raumfeld:ebrowse");
263 | }
264 |
265 |
266 | copyFavouritesContainerData(_newObject, _mediaContainer)
267 | {
268 | this.copyContainerData(_newObject, _mediaContainer);
269 | }
270 |
271 |
272 | copyPlaylistContainerData(_newObject, _mediaContainer)
273 | {
274 | this.copyContainerData(_newObject, _mediaContainer);
275 | }
276 |
277 |
278 | copyLineInData(_newObject, _mediaContainer)
279 | {
280 | this.copyContainerData(_newObject, _mediaContainer);
281 |
282 | if(_mediaContainer["res"] && _mediaContainer["res"].length)
283 | {
284 | if(_mediaContainer["res"][0]["_"])
285 | _newObject["stream"] = _mediaContainer["res"][0]["_"];
286 | if(_mediaContainer["res"][0]["$"] && _mediaContainer["res"][0]["$"]["protocolInfo"])
287 | _newObject["protocolInfo"] = _mediaContainer["res"][0]["$"]["protocolInfo"];
288 | }
289 | }
290 |
291 |
292 | copyMusicalbumCompilationData(_newObject, _mediaContainer)
293 | {
294 | this.copyAlbumData(_newObject, _mediaContainer);
295 | }
296 |
297 |
298 | copyPlaylistcontainerShuffleData(_newObject, _mediaContainer)
299 | {
300 | this.copyContainerData(_newObject, _mediaContainer);
301 | }
302 |
303 |
304 | copyPlaylistcontainerQueueData(_newObject, _mediaContainer)
305 | {
306 | this.copyContainerData(_newObject, _mediaContainer);
307 | }
308 |
309 |
310 | copyAlbumcontainerData(_newObject, _mediaContainer)
311 | {
312 | this.copyContainerData(_newObject, _mediaContainer);
313 | }
314 |
315 |
316 | copyGenreMusicgenreData(_newObject, _mediaContainer)
317 | {
318 | this.copyContainerData(_newObject, _mediaContainer);
319 | this.copyGenreData(_newObject, _mediaContainer);
320 | }
321 |
322 |
323 | copyMusiccomposerData(_newObject, _mediaContainer)
324 | {
325 | this.copyContainerData(_newObject, _mediaContainer);
326 | _newObject["numberOfAlbums"] = this.getData(_mediaContainer, "numberOfAlbums");
327 | }
328 |
329 |
330 | copyStorageFolderData(_newObject, _mediaContainer)
331 | {
332 | this.copyContainerData(_newObject, _mediaContainer);
333 | }
334 |
335 |
336 | copyPlaylistcontainerShuffleSearchData(_newObject, _mediaContainer)
337 | {
338 | this.copyContainerData(_newObject, _mediaContainer);
339 | }
340 |
341 |
342 | copyTrackContainerData(_newObject, _mediaContainer)
343 | {
344 | this.copyContainerData(_newObject, _mediaContainer);
345 |
346 | // if there is an "album art uri" then use it
347 | if(_mediaContainer["upnp:albumArtURI"] && _mediaContainer["upnp:albumArtURI"][0])
348 | _newObject["albumArtURI"] = _mediaContainer["upnp:albumArtURI"][0]._;
349 | }
350 |
351 |
352 | copyAudiobroadcastRhapsodyData(_newObject, _mediaContainer)
353 | {
354 | this.copyAlbumData(_newObject, _mediaContainer);
355 | }
356 |
357 | }
358 |
359 |
--------------------------------------------------------------------------------
/lib/lib.raumkernel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Logger = require('./lib.logger');
3 | var BaseManager = require('./lib.base.manager');
4 | var ManagerDisposer = require('./lib.managerDisposer');
5 |
6 | var QueueControllerNativePlaylist = require('./lib.queueController.nativePlaylist');
7 | var QueueControllerZonePlaylist = require('./lib.queueController.zonePlaylist');
8 |
9 | module.exports = class Raumkernel extends BaseManager
10 | {
11 | constructor()
12 | {
13 | super();
14 | this.systemReady = false;
15 | this.systemHostReady = false;
16 | this.mediaServerReady = false;
17 | this.zoneConfigReady = false;
18 | this.deviceListReady = false;
19 |
20 | this.nativePlaylistController = new QueueControllerNativePlaylist();
21 | this.zonePlaylistController = new QueueControllerZonePlaylist();
22 |
23 | this.settings = {}
24 | this.settings.raumfeldHost = "0.0.0.0";
25 | this.settings.raumfeldHostRequestPort = 47365;
26 | this.settings.raumfeldManufacturerIds = new Array();
27 | this.settings.raumfeldManufacturerIds[0] = "Raumfeld GmbH";
28 | this.settings.raumfeldManufacturerIds[1] = "Lautsprecher Teufel GmbH";
29 | this.settings.raumfeldVirtualMediaPlayerModelDescription = "Virtual Media Player";
30 | this.settings.alivePingerIntervall = 3500;
31 | this.settings.ssdpDiscovertimeout = 5000;
32 | this.settings.bonjourDiscoverTimeout = 3000;
33 | this.settings.uriMetaDataTemplateFile = "lib/setUriMetadata.template";
34 | this.settings.rendererStateTriggerConfirmationTimout = 3500;
35 | this.settings.zoneTriggerConfirmationTimout = 6000;
36 | }
37 |
38 | additionalLogIdentifier()
39 | {
40 | return "Raumkernel";
41 | }
42 |
43 | /**
44 | * construct and set a default logger
45 | * @param {Number} the log level which should be logged
46 | */
47 | createLogger(_logLevel = 2, _path = "")
48 | {
49 | this.parmLogger(new Logger(_logLevel, _path));
50 | }
51 |
52 | /**
53 | * should be called after the class was instanced and after an external logger was set (otherwise a standard logger will be created)
54 | * this method starts up the searching for the upnp devices ans the discovering of the raumfeld master device
55 | */
56 | init()
57 | {
58 | var self = this;
59 |
60 | // TODO: disbale logger
61 | // if there is no logger defined we do create a standard logger
62 | //if(!this.parmLogger())
63 | // this.createLogger();
64 |
65 | this.logVerbose("Setting up manager disposer");
66 |
67 | // create the manager disposer and let him create the managers
68 | this.managerDisposer = new ManagerDisposer();
69 | this.managerDisposer.parmLogger(this.parmLogger());
70 | this.managerDisposer.parmRaumkernel(this);
71 | this.managerDisposer.createManagers();
72 |
73 | this.logVerbose("Creating controllers");
74 |
75 | this.nativePlaylistController = new QueueControllerNativePlaylist();
76 | this.nativePlaylistController.parmLogger(this.parmLogger());
77 | this.nativePlaylistController.parmManagerDisposer(this.managerDisposer);
78 | this.nativePlaylistController.init();
79 | this.zonePlaylistController = new QueueControllerZonePlaylist();
80 | this.zonePlaylistController.parmLogger(this.parmLogger());
81 | this.zonePlaylistController.parmManagerDisposer(this.managerDisposer);
82 | this.zonePlaylistController.init();
83 |
84 | this.logDebug("Bind manager events");
85 |
86 | this.managerDisposer.deviceManager.on("systemHostFound", function(_host) { self.onSystemHostFound(_host); } );
87 | this.managerDisposer.deviceManager.on("systemHostLost", function() { self.onSystemHostLost(); } );
88 | this.managerDisposer.deviceManager.on("deviceListChanged", function(_deviceList) { self.onDeviceListChanged(_deviceList); } );
89 | this.managerDisposer.deviceManager.on("mediaRendererAdded", function(_deviceUdn, _device) { self.onMediaRendererAdded(_deviceUdn, _device); } );
90 | this.managerDisposer.deviceManager.on("mediaRendererRaumfeldAdded", function(_deviceUdn, _device) { self.onMediaRendererRaumfeldAdded(_deviceUdn, _device); } );
91 | this.managerDisposer.deviceManager.on("mediaRendererRaumfeldVirtualAdded", function(_deviceUdn, _device) { self.onMediaRendererRaumfeldVirtualAdded(_deviceUdn, _device); } );
92 | this.managerDisposer.deviceManager.on("mediaServerAdded", function(_deviceUdn, _device) { self.onMediaServerAdded(_deviceUdn, _device); } );
93 | this.managerDisposer.deviceManager.on("mediaServerRaumfeldAdded", function(_deviceUdn, _device) { self.onMediaServerRaumfeldAdded(_deviceUdn, _device); } );
94 | this.managerDisposer.deviceManager.on("mediaRendererRemoved", function(_deviceUdn, _name) { self.onMediaRendererRemoved(_deviceUdn, _name); } );
95 | this.managerDisposer.deviceManager.on("mediaRendererRaumfeldRemoved", function(_deviceUdn, _name) { self.onMediaRendererRaumfeldRemoved(_deviceUdn, _name); } );
96 | this.managerDisposer.deviceManager.on("mediaRendererRaumfeldVirtualRemoved",function(_deviceUdn, _name) { self.onMediaRendererRaumfeldVirtualRemoved(_deviceUdn, _name); } );
97 | this.managerDisposer.deviceManager.on("mediaServerRemoved", function(_deviceUdn, _name) { self.onMediaServerRemoved(_deviceUdn, _name); } );
98 | this.managerDisposer.deviceManager.on("mediaServerRaumfeldRemoved", function(_deviceUdn, _name) { self.onMediaServerRaumfeldRemoved(_deviceUdn, _name); } );
99 | this.managerDisposer.deviceManager.on("rendererStateChanged", function(_mediaRenderer, _rendererState) { self.onRendererStateChanged(_mediaRenderer, _rendererState); } );
100 | this.managerDisposer.deviceManager.on("rendererStateKeyValueChanged", function(_mediaRenderer, _key, _oldValue, _newValue, _roomUdn) { self.onRendererStateKeyValueChanged(_mediaRenderer, _key, _oldValue, _newValue, _roomUdn); } );
101 | this.managerDisposer.deviceManager.on("rendererMediaItemDataChanged", function(_mediaRenderer, _mediaItemData) { self.onRendererMediaItemDataChanged(_mediaRenderer, _mediaItemData); } );
102 |
103 | this.managerDisposer.zoneManager.on("zoneCreated", function(_zoneUDN) { self.onZoneCreated(_zoneUDN); } );
104 | this.managerDisposer.zoneManager.on("zoneRemoved", function(_zoneUDN) { self.onZoneRemoved(_zoneUDN); } );
105 | this.managerDisposer.zoneManager.on("roomAddedToZone", function(_zoneUDN, _roomUDN) { self.onRoomAddedToZone(_zoneUDN, _roomUDN); } );
106 | this.managerDisposer.zoneManager.on("roomRemovedFromZone", function(_zoneUDN, _roomUDN) { self.onRoomRemovedFromZone(_zoneUDN, _roomUDN); } );
107 | this.managerDisposer.zoneManager.on("zoneConfigurationChanged", function(_zoneConfiguration) { self.onZoneConfigurationChanged(_zoneConfiguration); } );
108 |
109 | this.managerDisposer.mediaListManager.on("mediaListDataReady", function(_id, _mediaListData) { self.onMediaListDataReady(_id, _mediaListData); } );
110 | this.managerDisposer.mediaListManager.on("mediaListDataPackageReady", function(_id, _mediaListDataPkg, _pkgIdx, _pgkIdxEnd, _pkgDataCount) { self.onMediaListDataPackageReady(_id, _mediaListDataPkg, _pkgIdx, _pgkIdxEnd,_pkgDataCount); } );
111 | this.managerDisposer.mediaListManager.on("mediaRendererPlaylistReady", function(_id, _mediaListData) { self.onMediaRendererPlaylistReady(_id, _mediaListData); } );
112 |
113 | this.managerDisposer.infodataManager.on("combinedZoneStateChanged", function(_combinedZoneState) { self.onCombinedZoneStateChanged(_combinedZoneState) } )
114 |
115 | // start the search for the devices (media servers, renderers, ...)
116 | this.managerDisposer.deviceManager.discover();
117 | }
118 |
119 |
120 | setSystemReady()
121 | {
122 | // system is ready when the zones are retrieved, the device list was gathered and if there is an raumfeld media server active
123 | var oldSystemReady = this.systemReady;
124 | this.systemReady = this.mediaServerReady && this.zoneConfigReady && this.deviceListReady && this.systemHostReady
125 | if(this.systemReady != oldSystemReady)
126 | this.emit("systemReady", this.systemReady);
127 | }
128 |
129 |
130 | onSystemHostFound(_host)
131 | {
132 | this.logInfo("Found raumfeld host on: " + _host);
133 | this.managerDisposer.zoneManager.parmSystemHost(_host);
134 | this.managerDisposer.zoneManager.requestZones();
135 | this.systemHostReady = true;
136 | this.setSystemReady();
137 | this.emit("systemHostFound", _host);
138 | }
139 |
140 | onSystemHostLost()
141 | {
142 | this.logError("Raumfeld host lost!");
143 | // tell the zone manager that he now may stop discovering the zone configuration because no host is online
144 | this.managerDisposer.zoneManager.parmSystemHost("");
145 | this.managerDisposer.zoneManager.stopRequestZones();
146 | this.systemHostReady = false;
147 | this.mediaServerReady = false;
148 | this.zoneConfigReady = false;
149 | this.deviceListReady = false;
150 | this.setSystemReady();
151 | this.emit("systemHostLost");
152 | }
153 |
154 | onDeviceListChanged(_deviceList)
155 | {
156 | this.deviceListReady = true;
157 | this.setSystemReady();
158 | this.emit("deviceListChanged", _deviceList);
159 | }
160 |
161 | onMediaRendererAdded(_deviceUdn, _device)
162 | {
163 | this.emit("mediaRendererAdded", _deviceUdn, _device)
164 | }
165 |
166 | onMediaRendererRaumfeldAdded(_deviceUdn, _device)
167 | {
168 | this.emit("mediaRendererRaumfeldAdded", _deviceUdn, _device)
169 | }
170 |
171 | onMediaRendererRaumfeldVirtualAdded(_deviceUdn, _device)
172 | {
173 | this.emit("mediaRendererRaumfeldVirtualAdded", _deviceUdn, _device)
174 | // try to trigger the 'zoneCreated' event
175 | this.managerDisposer.infodataManager.mediaRendererRaumfeldVirtualAdded(_deviceUdn, _device)
176 | this.managerDisposer.zoneManager.triggerZoneCreated(_deviceUdn)
177 | }
178 |
179 | onMediaServerAdded(_deviceUdn, _device)
180 | {
181 | this.emit("mediaServerAdded", _deviceUdn, _device);
182 | }
183 |
184 | onMediaServerRaumfeldAdded(_deviceUdn, _device)
185 | {
186 | this.nativePlaylistController.parmMediaServer(_device);
187 | this.zonePlaylistController.parmMediaServer(_device);
188 | this.managerDisposer.mediaListManager.parmMediaServer(_device);
189 | this.mediaServerReady = true;
190 | this.setSystemReady();
191 |
192 | // refresh lists for all renderers.
193 | this.managerDisposer.deviceManager.refreshMediaRendererMediaLists();
194 |
195 | this.emit("mediaServerRaumfeldAdded", _deviceUdn, _device);
196 | }
197 |
198 | onMediaRendererRemoved(_deviceUdn, _name)
199 | {
200 | this.emit("mediaRendererRemoved", _deviceUdn, _name);
201 | }
202 |
203 | onMediaRendererRaumfeldRemoved(_deviceUdn, _name)
204 | {
205 | this.emit("mediaRendererRaumfeldRemoved", _deviceUdn, _name);
206 | }
207 |
208 | onMediaRendererRaumfeldVirtualRemoved(_deviceUdn, _name)
209 | {
210 | this.emit("mediaRendererRaumfeldVirtualRemoved", _deviceUdn, _name);
211 | // try to trigger the 'zoneRemoved' event.
212 | this.managerDisposer.zoneManager.triggerZoneRemoved(_deviceUdn);
213 |
214 | }
215 |
216 | onMediaServerRemoved(_deviceUdn, _name)
217 | {
218 | this.emit("mediaServerRemoved", _deviceUdn, _name);
219 | }
220 |
221 | onMediaServerRaumfeldRemoved(_deviceUdn, _name)
222 | {
223 | this.nativePlaylistController.parmMediaServer(null);
224 | this.zonePlaylistController.parmMediaServer(null);
225 | this.managerDisposer.mediaListManager.parmMediaServer(null);
226 | this.mediaServerReady = false;
227 | this.setSystemReady();
228 | this.emit("mediaServerRaumfeldRemoved", _deviceUdn, _name);
229 | }
230 |
231 | onZoneConfigurationChanged(_zoneConfiguration)
232 | {
233 | this.zoneConfigReady = true
234 | this.setSystemReady()
235 | this.managerDisposer.infodataManager.zoneConfigurationChanged(_zoneConfiguration)
236 | this.emit("zoneConfigurationChanged", _zoneConfiguration)
237 | }
238 |
239 | onRendererStateKeyValueChanged(_mediaRenderer, _key, _oldValue, _newValue, _roomUdn)
240 | {
241 | // when the track uri is changing we have to refresh the media list for the renderer
242 | // we can do this of course only when the media renderer is found already, otherwise it will be triggered by the media renderer aapearance
243 | if(this.mediaServerReady)
244 | {
245 | if(_key.toLowerCase() == "avtransporturi")
246 | {
247 | if(_newValue && _mediaRenderer.rendererState.AVTransportURIMetaData)
248 | this.managerDisposer.mediaListManager.loadMediaListForRendererUri(_mediaRenderer.udn(), _newValue, _mediaRenderer.rendererState.AVTransportURIMetaData).catch(function(_data){});
249 | }
250 | if(_key.toLowerCase() == "avtransporturimetadata")
251 | {
252 | if(_newValue && _mediaRenderer.rendererState.AVTransportURI)
253 | this.managerDisposer.mediaListManager.loadMediaListForRendererUri(_mediaRenderer.udn(), _mediaRenderer.rendererState.AVTransportURI, _newValue).catch(function(_data){});
254 | // av transport uri metadata will containe the datata as currenttrackmetadata and some more (album ect.)
255 | // we do only update the "track" type, this is done in the "updateCurrentMediaItemInfo" itself
256 | //_mediaRenderer.updateCurrentMediaItemInfo(_newValue);
257 | }
258 | if(_key.toLowerCase() == "currenttrackmetadata")
259 | {
260 | _mediaRenderer.lastCurrentTrackMetadata = _newValue
261 | _mediaRenderer.updateCurrentMediaItemInfo(_newValue);
262 | }
263 |
264 | }
265 |
266 | this.managerDisposer.infodataManager.rendererStateKeyValueChanged(_mediaRenderer, _key, _oldValue, _newValue, _roomUdn)
267 | // renderer states can do some triggers, so add it to the trigger manager
268 | this.managerDisposer.triggerManager.trigger("renderer", "rendererStateKeyValueChanged", { "rendererUdn": _mediaRenderer.udn(),
269 | "key": _key,
270 | "value": _newValue,
271 | "oldValue": _oldValue,
272 | "roomUdn": _roomUdn });
273 |
274 | this.emit("rendererStateKeyValueChanged", _mediaRenderer, _key, _oldValue, _newValue, _roomUdn);
275 | }
276 |
277 | onRendererStateChanged(_mediaRenderer, _rendereState)
278 | {
279 | this.managerDisposer.infodataManager.rendererStateChanged( _mediaRenderer, _rendereState)
280 | this.emit("rendererStateChanged", _mediaRenderer, _rendereState)
281 | }
282 |
283 |
284 | onMediaListDataReady(_id, _mediaListData)
285 | {
286 | this.emit("mediaListDataReady", _id, _mediaListData);
287 | }
288 |
289 |
290 | onMediaListDataPackageReady(_id, _mediaListDataPkg, _pkgIdx, _pgkIdxEnd, _pkgDataCount)
291 | {
292 | this.emit("mediaListDataPackageReady", _id, _mediaListDataPkg, _pkgIdx, _pgkIdxEnd, _pkgDataCount);
293 | }
294 |
295 |
296 | onMediaRendererPlaylistReady(_id, _mediaListData)
297 | {
298 | this.emit("mediaRendererPlaylistReady", _id, _mediaListData);
299 | }
300 |
301 |
302 | onRendererMediaItemDataChanged(_mediaRenderer, _currentMediaItemData)
303 | {
304 | this.managerDisposer.infodataManager.rendererMediaItemDataChanged(_mediaRenderer, _currentMediaItemData)
305 | this.emit("rendererMediaItemDataChanged", _mediaRenderer, _currentMediaItemData);
306 | }
307 |
308 |
309 | onZoneCreated(_zoneUDN)
310 | {
311 | this.emit("zoneCreated", _zoneUDN);
312 | }
313 |
314 |
315 | onZoneRemoved(_zoneUDN)
316 | {
317 | this.emit("zoneRemoved", _zoneUDN);
318 | }
319 |
320 |
321 | onRoomAddedToZone(_zoneUDN, _roomUDN)
322 | {
323 | this.emit("roomAddedToZone", _zoneUDN, _roomUDN);
324 | }
325 |
326 |
327 | onRoomRemovedFromZone(_zoneUDN, _roomUDN)
328 | {
329 | this.emit("roomRemovedFromZone", _zoneUDN, _roomUDN);
330 | }
331 |
332 |
333 | onCombinedZoneStateChanged(_combinedZoneState)
334 | {
335 | this.emit("combinedZoneStateChanged", _combinedZoneState)
336 | }
337 |
338 |
339 | getSettings()
340 | {
341 | return this.settings;
342 | }
343 |
344 | }
345 |
--------------------------------------------------------------------------------
/lib/lib.manager.mediaListManager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Url = require('url');
3 | var ManagerBase = require('./lib.manager.base');
4 | var MediaDataConverter = require('./lib.mediaDataConverter');
5 |
6 |
7 | module.exports = class MediaListManager extends ManagerBase
8 | {
9 | constructor()
10 | {
11 | super();
12 | this.mediaServer = null
13 | this.listCache = new Map();
14 | }
15 |
16 | additionalLogIdentifier()
17 | {
18 | return "MediaListManager";
19 | }
20 |
21 | parmMediaServer(_mediaServer = this.mediaServer)
22 | {
23 | this.mediaServer = _mediaServer
24 | return this.mediaServer;
25 | }
26 |
27 |
28 | checkForMediaServer()
29 | {
30 | if(!this.mediaServer)
31 | {
32 | this.logError("Calling Action on MediaListManager without having a valid media server!");
33 | return false;
34 | }
35 | return true;
36 | }
37 |
38 | /*
39 | DEL_getMediaList(_listId, _objectId, _useListCache = true, _emitReady = true)
40 | {
41 | var self = this;
42 | this.logVerbose("Get media list for objectId: " + _objectId);
43 |
44 | return new Promise(function(_resolve, _reject){
45 | try
46 | {
47 | if(!self.checkForMediaServer())
48 | {
49 | _reject(new Error("Calling Action on MediaListManager without having a valid media server!"));
50 | return;
51 | }
52 |
53 | // if list is in cache and we should us the cache then return list from the cache
54 | // lists like zone playlists or favourites are always up to date in cache
55 | if(_useListCache && self.listCache.has(_listId))
56 | {
57 | if(_emitReady)
58 | self.emit("mediaListDataReady", _listId, self.listCache[_listId]);
59 | _resolve(self.listCache[_listId]);
60 | return;
61 | }
62 |
63 | self.mediaServer.browse(_objectId).then(function(_data){
64 | // convert given xml data to nice JSON array
65 | var mediaDataConverter = new MediaDataConverter();
66 | mediaDataConverter.parmLogger(self.parmLogger());
67 | mediaDataConverter.parmManagerDisposer(self.parmManagerDisposer());
68 | mediaDataConverter.convertXMLToMediaList(_data).then(function(_data){
69 | self.listCache[_listId] = _data;
70 | if(_emitReady)
71 | self.emit("mediaListDataReady", _listId, _data);
72 | _resolve(_data);
73 | }).catch(function(_data){
74 | _reject(_data)
75 | });
76 | }).catch(function(_data){
77 | _reject(_data)
78 | });
79 | }
80 | catch(exception)
81 | {
82 | self.logError(exception.toString());
83 | _reject(exception);
84 | }
85 | });
86 | }
87 | */
88 |
89 |
90 | loadMediaListForUri(_listId, _uri, _uriMetadata, _useCache = false, _emitReady = true, _dataPackageCount = 25, _dataPackageCallback = null)
91 | {
92 | var self = this;
93 |
94 | return new Promise(function(_resolve, _reject){
95 | self.logVerbose("Get media list for uri " + _uri);
96 |
97 | try
98 | {
99 | // we have to decide if its a playcontainer or not. This is the case when the uri begins with "dlna-playcontainer://"
100 | // if there is no playcontainer we have to use the medadata to create the list (in fact then its only one item)
101 | var parsedUrl = Url.parse(_uri, true);
102 | if(parsedUrl.protocol != "dlna-playcontainer:")
103 | {
104 | if(_uriMetadata)
105 | {
106 | var mediaDataConverter = new MediaDataConverter();
107 | mediaDataConverter.parmLogger(self.parmLogger());
108 | mediaDataConverter.parmManagerDisposer(self.parmManagerDisposer());
109 | mediaDataConverter.convertXMLToMediaList(_uriMetadata).then(function(_data){
110 | self.listCache.set(_listId, _data);
111 | if(_emitReady)
112 | self.emit("mediaListDataReady", _listId, _data);
113 | _resolve(_data);
114 | }).catch(function(_data){
115 | self.listCache.delete(_listId);
116 | if(_emitReady)
117 | self.emit("mediaListDataReady", _listId, null);
118 | _reject(_data);
119 | });
120 | }
121 | else
122 | {
123 | self.listCache.delete(_listId);
124 | if(_emitReady)
125 | self.emit("mediaListDataReady", _listId, null);
126 | _resolve(null);
127 | }
128 | }
129 | // we do have a playcontainer, so we try to get the cid from the query. This is the id where we can search the content directory with
130 | else
131 | {
132 | self.getMediaList(_listId, parsedUrl.query.cid, "", _useCache, _emitReady, _dataPackageCount, _dataPackageCallback).then(function(_data){
133 | _resolve(_data);
134 | }).catch(function(_data){
135 | _reject(_data);
136 | });
137 | }
138 | }
139 | catch(_exception)
140 | {
141 | self.logError("Error resolving url on loadMediaListForUri", _exception);
142 | _reject(new Error("Error resolving url on loadMediaListForUri: " + _exception.toString()));
143 | }
144 | });
145 | }
146 |
147 |
148 | loadMediaListForRendererUri(_rendererUdn, _uri, _uriMetadata, _useCache = false, _dataPackageCount = 25, _dataPackageCallback = null)
149 | {
150 | var self = this;
151 |
152 | // sometimes a "false" is returned to say hey, i have no metadata!
153 | if(_uriMetadata == "false")
154 | _uriMetadata = "";
155 |
156 | return new Promise(function(_resolve, _reject){
157 | self.logDebug("Update media list for " + _rendererUdn, _uri);
158 | self.loadMediaListForUri(_rendererUdn, _uri, _uriMetadata, _useCache, false, _dataPackageCount, _dataPackageCallback).then(function(_data){
159 | self.logDebug("Media list for renderer " + _rendererUdn + " is ready");
160 | self.emit("mediaRendererPlaylistReady", _rendererUdn, _data);
161 | _resolve(_data);
162 | }).catch(function(_data){
163 | self.logError("Error getting media list for renderer " + _rendererUdn, _data.toString());
164 | //self.emit("mediaRendererPlaylistReady", _rendererUdn, null);
165 | _reject(_data);
166 | });
167 | });
168 | }
169 |
170 |
171 | loadMediaItemListsByContainerUpdateIds(_mediaServer, _containerUpdateIds)
172 | {
173 | if(!_mediaServer.isRaumfeldServer() || !_containerUpdateIds)
174 | return;
175 | var updateIds = _containerUpdateIds.split(",");
176 | for(var i=0; i 0)
235 | {
236 | _dataPackageCallback(_listId, packageData, pkgIdx, (pkgIdx + curPackageDataCount - 1) , curPackageDataCount);
237 | if(_emitReady)
238 | self.emit("mediaListDataPackageReady", _listId, packageData, pkgIdx, (pkgIdx + curPackageDataCount - 1), curPackageDataCount);
239 | }
240 | }
241 | }
242 | // emit or resolve the list if we get it from the cache immediately, we do not read
243 | // it from the media server of course.
244 | if(_emitReady)
245 | self.emit("mediaListDataReady", _listId, self.listCache.get(_listId));
246 | _resolve(self.listCache.get(_listId));
247 | return;
248 | }
249 |
250 | // we do have to browse recursivle to get data parts
251 | self.listCache.delete(_listId);
252 | self.getMediaList_Rec(_listId, _objectId, _searchCriteria, _emitReady, _dataPackageCount, _dataPackageCallback, 0).then(function(_data){
253 | if(_emitReady)
254 | self.emit("mediaListDataReady", _listId, _data);
255 | _resolve(self.listCache.get(_listId));
256 | }).catch(function(_data){
257 | _reject(_data);
258 | });
259 |
260 |
261 | }
262 | catch(exception)
263 | {
264 | self.logError(exception.toString());
265 | _reject(exception);
266 | }
267 | });
268 | }
269 |
270 |
271 | getMediaList_Rec(_listId, _objectId, _searchCriteria, _emitReady, _dataPackageCount, _dataPackageCallback, _pkgIdx)
272 | {
273 | var self = this;
274 |
275 | return new Promise(function(_resolve, _reject){
276 | try
277 | {
278 | // we do use "search" instead of browse s owe cann use this method for searching too
279 | // the result is the same so it does not matter. If there are problems we will have to jump in with an if (browse and search)
280 | self.mediaServer.search(_objectId, _searchCriteria, "*", _pkgIdx, _dataPackageCount).then(function(_data){
281 | //self.mediaServer.browse(_objectId, "BrowseDirectChildren", "*", _pkgIdx, _dataPackageCount).then(function(_data){
282 | // convert given xml data to nice JSON array
283 | var mediaDataConverter = new MediaDataConverter();
284 | mediaDataConverter.parmLogger(self.parmLogger());
285 | mediaDataConverter.parmManagerDisposer(self.parmManagerDisposer());
286 | mediaDataConverter.convertXMLToMediaList(_data).then(function(_data){
287 | // get the current package data which should always be equal to "_dataPackageCount" except we have read all data
288 | var curPackageDataCount = _data.length;
289 | if(curPackageDataCount > 0)
290 | {
291 | // update the main list data by adding the current part
292 | // Im not sure if this is the fastest way, maybe heer we should do some performance tuning?
293 | if(self.listCache.has(_listId))
294 | self.listCache.set(_listId, self.listCache.get(_listId).concat(_data));
295 | else
296 | self.listCache.set(_listId, _data);
297 |
298 | if(_dataPackageCallback)
299 | _dataPackageCallback(_listId, _data, _pkgIdx, (_pkgIdx + curPackageDataCount - 1) , curPackageDataCount);
300 | if(_emitReady)
301 | self.emit("mediaListDataPackageReady", _listId, _data, _pkgIdx, (_pkgIdx + curPackageDataCount - 1), curPackageDataCount);
302 | }
303 |
304 | // do not resolve until we do have read all data
305 | if(curPackageDataCount < _dataPackageCount)
306 | {
307 | _resolve(_data);
308 | }
309 | // if we do not have all data start browsing again with the next package idx
310 | else
311 | {
312 | self.getMediaList_Rec(_listId, _objectId, _searchCriteria, _emitReady, _dataPackageCount, _dataPackageCallback, (_pkgIdx + curPackageDataCount)).then(function(_data){
313 | _resolve(_data);
314 | }).catch(function(_data){
315 | _reject(_data);
316 | });
317 | }
318 |
319 | }).catch(function(_data){
320 | _reject(_data);
321 | });
322 | }).catch(function(_data){
323 | _reject(_data);
324 | });
325 | }
326 | catch(_exception)
327 | {
328 | _reject(_data);
329 | }
330 | });
331 | }
332 |
333 | /**
334 | * may be used to search on a special "search" container id
335 | * Examples of special search containers are:
336 | * 0/My Music/Search/Albums
337 | * 0/Napster/Search/Artist
338 | * 0/RadioTime/Search
339 | * @param {string} a list id where the result will be stored in
340 | * @param {string} a special search container id
341 | * @param {string} the search value
342 | */
343 | searchMediaList(_listId, _searchContainerId, _searchValue = "", _useListCache = true, _emitReady = true, _dataPackageCount = 25, _dataPackageCallback = null)
344 | {
345 | /* search only works on "search" containers e.g.:
346 | 0/My Music/Search/Albums
347 | 0/Napster/Search/Artist
348 | 0/RadioTime/Search
349 | ...
350 | So in fact a client has to check the 2nd level for a "Search" node
351 | */
352 | var searchCriteria = _searchValue ? 'dc:title contains "' + _searchValue + '"' : '';
353 | return this.getMediaList(_listId, _searchContainerId, searchCriteria, _useListCache, _emitReady, _dataPackageCount, _dataPackageCallback);
354 | }
355 |
356 | }
--------------------------------------------------------------------------------
/lib/lib.device.upnp.mediaRenderer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Url = require('url');
3 | var ParseString = require('xml2js').parseString;
4 | var UPNPDevice = require('./lib.device.base.upnp');
5 | var MediaDataConverter = require('./lib.mediaDataConverter');
6 |
7 | /**
8 | * this is the base class to use for child's which are media Renderer devices
9 | */
10 | module.exports = class UPNPMediaRenderer extends UPNPDevice
11 | {
12 | constructor(_upnpClient)
13 | {
14 | super(_upnpClient);
15 | // this object holds the current state/values of the renderer and is crated/updated from the 'avTransportData' and the 'renderingControlData' objects
16 | this.rendererState = {};
17 | // this object holds the last data which was sent from the AVTransport service subscription
18 | this.lastChangedAvTransportData = [];
19 | // this object holds the last data which was sent from the Rendering service subscription
20 | this.lastChangedRenderingControlData = [];
21 | // the last update id for the renderer state, will be updated whenever a state changes
22 | this.lastUpdateIdRendererState = "";
23 | //
24 | this.lastCurrentTrackMetadata = "";
25 | //
26 | this.currentMediaItemData = {};
27 | // the mediaOriginData reflects the current loaded item dta (eg. container or single object id)
28 | this.mediaOriginData = {
29 | containerId : "",
30 | singleId : "",
31 | uri : ""
32 | };
33 |
34 | this.setRandomUpdateId();
35 | }
36 |
37 |
38 | additionalLogIdentifier()
39 | {
40 | return "MediaRenderer|" + this.name();
41 | }
42 |
43 |
44 | roomName()
45 | {
46 | return this.name();
47 | }
48 |
49 | roomUdn()
50 | {
51 | return this.udn();
52 | }
53 |
54 |
55 | isRaumfeldRenderer()
56 | {
57 | return false;
58 | }
59 |
60 | subscribe()
61 | {
62 | var self = this;
63 | if(!this.upnpClient)
64 | {
65 | this.logError("Trying to subscribe to services on device '" + this.name() + "' without client object");
66 | return;
67 | }
68 |
69 | this.upnpClient.on("error", function(_err) {
70 | self.logError(_err);
71 | });
72 |
73 | this.logVerbose("Set up AVTransport subscription on device '" + this.name() + "'")
74 | this.upnpClient.subscribe('AVTransport', this.avTransportSubscriptionCallback(this));
75 |
76 | this.logVerbose("Set up RenderingControl subscription on device '" + this.name() + "'")
77 | this.upnpClient.subscribe('RenderingControl', this.renderingControlSubscriptionCallback(this));
78 | }
79 |
80 | unsubscribe()
81 | {
82 | if(!this.upnpClient)
83 | {
84 | this.logError("Trying to un-subscribe services on device '" + this.name() + "' without client object");
85 | return;
86 | }
87 |
88 | this.logVerbose("Remove service subscriptions for device '" + this.name() + "'");
89 | //this.upnpClient.unsubscribe("AVTransport", this.avTransportSubscriptionCallback(this));
90 | //this.upnpClient.unsubscribe("RenderingControl", this.renderingControlSubscriptionCallback(this));
91 | this.upnpClient.unsubscribeAll("AVTransport");
92 | this.upnpClient.unsubscribeAll("RenderingControl");
93 | }
94 |
95 |
96 | avTransportSubscriptionCallback(_self)
97 | {
98 | return function(_data)
99 | {
100 | _self.onAvTransportSubscription(_data);
101 | }
102 | }
103 |
104 |
105 | renderingControlSubscriptionCallback(_self)
106 | {
107 | return function(_data)
108 | {
109 | _self.onRenderingControlSubscription(_data);
110 | }
111 | }
112 |
113 |
114 | /**
115 | * should be called whenever any state of renderer (subscription) is changed
116 | * so it will be called on onAvTransportSubscription and on onRenderingControlSubscription
117 | */
118 | updateRendererState()
119 | {
120 | // update the renderer state object
121 | this.updateRendererStateObject();
122 |
123 | // create a new update id for the render
124 | this.updateRandomUpdateId();
125 |
126 | this.emit("rendererStateChanged", this, this.rendererState);
127 | }
128 |
129 | /**
130 | * this method will update the renderer state object from the lastChanged Data
131 | */
132 | updateRendererStateObject()
133 | {
134 | // copy keys and values of both subscription returned data into the renderer state so the renderer
135 | // state will fill up with the gathered and always upToDate values
136 | for(var key in this.lastChangedAvTransportData)
137 | {
138 | // check if value has changed or if new key is not existent, if so then we do emit an event with the key and value
139 | if(this.rendererState[key] != this.lastChangedAvTransportData[key])
140 | {
141 | this.logVerbose(key + " has changed from '" + this.rendererState[key] + "' to '" + this.lastChangedAvTransportData[key] + "'");
142 | this.emit("rendererStateKeyValueChanged", this, key, this.rendererState[key], this.lastChangedAvTransportData[key], "");
143 | }
144 | this.rendererState[key]=this.lastChangedAvTransportData[key];
145 | }
146 | for(var key in this.lastChangedRenderingControlData)
147 | {
148 | // check if value has changed or if new key is not existent, if so then we do emit an event with the key and value
149 | if(this.rendererState[key] != this.lastChangedRenderingControlData[key])
150 | {
151 | this.logVerbose(key + " has changed from '" + this.rendererState[key] + "' to '" + this.lastChangedRenderingControlData[key] + "'");
152 | this.emit("rendererStateKeyValueChanged", this, key, this.rendererState[key], this.lastChangedRenderingControlData[key], "");
153 | }
154 | this.rendererState[key]=this.lastChangedRenderingControlData[key];
155 | }
156 |
157 | // update the mediaOriginData from the new given renderer state
158 | this.updateMediaOriginData();
159 | }
160 |
161 |
162 | /**
163 | * will fill the 'mediaOriginData' with the help of the the avTransportUri given in the rendereState
164 | */
165 | updateMediaOriginData()
166 | {
167 | //if there is no uri we have to clear the origin data because there is no origin
168 | if(!this.rendererState.AVTransportURI)
169 | {
170 | this.mediaOriginData.containerId = "";
171 | this.mediaOriginData.singleId = "";
172 | this.mediaOriginData.uri = "";
173 | return;
174 | }
175 |
176 | var parsedUrl = Url.parse(this.rendererState.AVTransportURI, true);
177 | if(parsedUrl.protocol = "dlna-playcontainer:")
178 | {
179 | this.mediaOriginData.containerId = parsedUrl.query.cid;
180 | this.mediaOriginData.singleId = "";
181 | this.mediaOriginData.uri = "";
182 | }
183 | else if(parsedUrl.protocol = "dlna-playsingle:")
184 | {
185 | this.mediaOriginData.containerId = "";
186 | this.mediaOriginData.singleId = parsedUrl.query.sid;
187 | this.mediaOriginData.uri = "";
188 | }
189 | // well here we do have not container and no playsingle, so we do havea direct uri set by any application
190 | else
191 | {
192 | this.mediaOriginData.containerId = "";
193 | this.mediaOriginData.singleId = "";
194 | this.mediaOriginData.uri = this.rendererState.AVTransportURI;
195 | }
196 | }
197 |
198 |
199 | /**
200 | * will be called when data of the AvTransport service was changed
201 | * @param {Object} a object with the changed data
202 | */
203 | onAvTransportSubscription(_keyDataArray)
204 | {
205 | this.logDebug("AVTransport subscription callback triggered on device '" + this.name() + "'");
206 | this.lastChangedAvTransportData = _keyDataArray;
207 | this.updateRendererState();
208 | }
209 |
210 | /**
211 | * will be called when data of the RenderingControl service was changed
212 | * @param {Object} a object with the changed data
213 | */
214 | onRenderingControlSubscription(_keyDataArray)
215 | {
216 | this.logDebug("RenderingControl subscription callback triggered on device '" + this.name() + "'");
217 | this.lastChangedRenderingControlData = _keyDataArray;
218 | this.updateRendererState();
219 | }
220 |
221 | /**
222 | * returns the current volume for the device
223 | * @return {Promise} a promise with the volume as result
224 | */
225 | getVolume()
226 | {
227 | return this.callAction("RenderingControl", "GetVolume", {"Channel": "Master"}, function (_result){
228 | return _result.CurrentVolume;
229 | });
230 | }
231 |
232 | /**
233 | * returns the current mute for the device
234 | * @return {Promise} a promise with the mute state as result (0 or 1)
235 | */
236 | getMute()
237 | {
238 | return this.callAction("RenderingControl", "GetMute", {"Channel": "Master"}, function (_result){
239 | return _result.CurrentMute;
240 | });
241 | }
242 |
243 | /**
244 | * set the volume for the device
245 | * @param {Integer} the desired volume
246 | * @return {Promise} a promise with no result
247 | */
248 | setVolume(_volume, _confirm = false)
249 | {
250 | this.logDebug("Set volume to " + _volume + " on room " + this.name());
251 | return this.callActionWithTriggerWait("RenderingControl", "SetVolume", {"Channel": "Master", "DesiredVolume": _volume}, null, { "key" : "Volume", "value" : _volume} , _confirm)
252 | }
253 |
254 | /**
255 | * set the mute for the device
256 | * @param {boolean} true or false
257 | * @return {Promise} a promise with no result
258 | */
259 | setMute(_mute, _confirm = false)
260 | {
261 | this.logDebug("Set mute to " + _mute + " on room " + this.name());
262 | return this.callActionWithTriggerWait("RenderingControl", "SetMute", {"Channel": "Master", "DesiredMute": _mute}, null, { "key" : "mute", "value" : _mute ? "1" : "0"} , _confirm)
263 | }
264 |
265 | /**
266 | * start playing
267 | * @return {Promise} a promise with no result
268 | */
269 | play(_confirm = false)
270 | {
271 | return this.callActionWithTriggerWait("AVTransport", "Play", {}, null, { "key" : "TransportState", "value" : "PLAYING"} , _confirm)
272 | }
273 |
274 | /**
275 | * stop playing
276 | * @return {Promise} a promise with no result
277 | */
278 | stop(_confirm = false)
279 | {
280 | return this.callActionWithTriggerWait("AVTransport", "Stop", {}, null, { "key" : "TransportState", "value" : "STOPPED" } , _confirm)
281 | }
282 |
283 | /**
284 | * pause playing
285 | * @return {Promise} a promise with no result
286 | */
287 | pause(_confirm = false)
288 | {
289 | return this.callActionWithTriggerWait("AVTransport", "Pause", {}, null, { "key" : "TransportState", "value" : "PAUSED_PLAYBACK"} , _confirm)
290 | }
291 |
292 | /**
293 | * play next
294 | * @return {Promise} a promise with no result
295 | */
296 | next(/*_confirm = false*/)
297 | {
298 | return this.callAction("AVTransport", "Next", {});
299 | //var toValue = parseInt(this.rendererState.CurrentTrack);
300 | //if (parseInt(this.rendererState.NumberOfTracks) > 1 && toValue > 1 && toValue < parseInt(this.rendererState.NumberOfTracks))
301 | // toValue = toValue + 1;
302 | //return this.callActionWithTriggerWait("AVTransport", "Next", {}, null, { "key" : "CurrentTrack", "value" : toValue} , _confirm)
303 |
304 | }
305 |
306 | /**
307 | * play previous
308 | * @return {Promise} a promise with no result
309 | */
310 | prev(/*_confirm = false*/)
311 | {
312 | return this.callAction("AVTransport", "Previous", {});
313 | // INFO: we got a problem here. 'prev' does not have to trigger the last number. it may start the current one!
314 | //var toValue = parseInt(this.rendererState.CurrentTrack);
315 | //if (parseInt(this.rendererState.NumberOfTracks) > 1 && toValue > 1)
316 | // toValue = toValue - 1;
317 | //return this.callActionWithTriggerWait("AVTransport", "Previous", {}, null, { "key" : "CurrentTrack", "value" : toValue} , _confirm)
318 | }
319 |
320 | /**
321 | * set play mode
322 | * @param {String} the play mode as a string (NORMAL, SHUFFLE, REPEAT_ALL, RANDOM, REPEAT_ONE, DIRECT_1)
323 | * @return {Promise} a promise with no result
324 | */
325 | setPlayMode(_playMode, _confirm = false)
326 | {
327 | return this.callActionWithTriggerWait("AVTransport", "SetPlayMode", {"NewPlayMode": _playMode}, null, { "key" : "CurrentPlayMode", "value" : _playMode} , _confirm)
328 | }
329 |
330 | /**
331 | * set av transport uri
332 | * @param {String} the transport uri
333 | * @param {String} the transport uri metadata
334 | * @return {Promise} a promise with no result
335 | */
336 | setAvTransportUri(_transportUri, _transporUriMetadata, _confirm = false)
337 | {
338 | //return this.callAction("AVTransport", "SetAVTransportURI", {"CurrentURI": _transportUri, "CurrentURIMetaData": _transporUriMetadata});
339 | return this.callActionWithTriggerWait("AVTransport", "SetAVTransportURI", {"CurrentURI": _transportUri, "CurrentURIMetaData": _transporUriMetadata}, null, { "key" : "AVTransportURI", "value" : _transportUri} , _confirm)
340 | }
341 |
342 | /**
343 | * seek
344 | * @param {String} the seek unit (ABS_TIME, REL_TIME, TRACK_NR)
345 | * @param {String} the seek target
346 | * @return {Promise} a promise with no result
347 | */
348 | seek(_unit, _target)
349 | {
350 | return this.callAction("AVTransport", "Seek", {"Unit": _unit, "Target": _target});
351 | }
352 |
353 | /**
354 | * getPositionInfo
355 | * @return {Promise} a promise with the position info
356 | */
357 | getPositionInfo()
358 | {
359 | return this.callAction("AVTransport", "GetPositionInfo", {});
360 | }
361 |
362 | /**
363 | * getTransportInfo
364 | * @return {Promise} a promise with the transport info
365 | */
366 | getTransportInfo()
367 | {
368 | return this.callAction("AVTransport", "GetTransportInfo", {});
369 | }
370 |
371 | /**
372 | * getTransportSettings
373 | * @return {Promise} a promise with the transport settings
374 | */
375 | getTransportSettings()
376 | {
377 | return this.callAction("AVTransport", "GetTransportSettings", {});
378 | }
379 |
380 |
381 | /**
382 | * sets a random update id
383 | */
384 | setRandomUpdateId(_min = 100000, _max = 990000)
385 | {
386 | this.lastUpdateIdRendererState = (Math.floor(Math.random() * (_max - _min + 1)) + _min).toString();
387 | }
388 |
389 |
390 | /**
391 | * update a random update id
392 | */
393 | updateRandomUpdateId(_min = 100000, _max = 990000)
394 | {
395 | this.lastUpdateIdRendererState = (parseInt(this.lastUpdateIdRendererState) + 1).toString();
396 | this.logDebug("Set new updateId to: " + this.lastUpdateIdRendererState);
397 | }
398 |
399 |
400 | /**
401 | * updates the data of the current media item on the renderer
402 | */
403 | updateCurrentMediaItemInfo(_xmlMetadata)
404 | {
405 | var self = this;
406 | if(!_xmlMetadata)
407 | {
408 | self.currentMediaItemData = {};
409 | return;
410 | }
411 |
412 | var mediaDataConverter = new MediaDataConverter();
413 | mediaDataConverter.parmLogger(self.parmLogger());
414 | mediaDataConverter.parmManagerDisposer(self.parmManagerDisposer());
415 | mediaDataConverter.convertXMLToMediaList(_xmlMetadata).then(function(_data){
416 | // be sure the item is type of "audiotItem" and not album or something like that
417 | // Sometimes the AVTransportUriMetadata comes twice (Album and track) and current track metadata
418 | // doesn't show up so we have to do this kind of fix
419 | if(_data.length && _data[0].class.startsWith("object.item.audioItem"))
420 | {
421 | self.currentMediaItemData = _data[0];
422 | self.logDebug("Media Item on renderer changed", self.currentMediaItemData);
423 | self.emit("rendererMediaItemDataChanged", self, self.currentMediaItemData);
424 | }
425 | }).catch(function(_data){
426 | self.currentMediaItemData = {};
427 | self.logError("Can not create current media item on renderer " + self.name(), _data);
428 | self.emit("rendererMediaItemDataChanged", self, self.currentMediaItemData);
429 | });
430 | }
431 |
432 |
433 | compareRendererTriggerValue(_triggerData)
434 | {
435 | var triggerDataObject = null
436 | var keyAlreadySet = false
437 | var self = this
438 |
439 | // get trigger data object
440 | if(_triggerData.roomUdn)
441 | triggerDataObject = this.rendererState.rooms[_triggerData.roomUdn]
442 | else
443 | triggerDataObject = this.rendererState
444 |
445 | if(triggerDataObject)
446 | {
447 | if(_triggerData.values)
448 | {
449 | for(var i=0; i<_triggerData.values.length; i++)
450 | {
451 | if(triggerDataObject[_triggerData.key] && triggerDataObject[_triggerData.key].toString().toLowerCase() == _triggerData.values[i].toString().toLowerCase())
452 | keyAlreadySet = true
453 | }
454 | }
455 | else
456 | {
457 | if(triggerDataObject[_triggerData.key])
458 | keyAlreadySet = triggerDataObject[_triggerData.key].toString().toLowerCase() == _triggerData.value.toString().toLowerCase()
459 | }
460 | }
461 | else
462 | {
463 | this.logError("No trigger data object found")
464 | }
465 | return keyAlreadySet
466 | }
467 |
468 |
469 | /**
470 | * getTransportInfo
471 | * @return {Promise} a promise which will return when the given trigger is triggeerd
472 | */
473 | callActionWithTriggerWait(_service, _action, _params, _resultSetFunction = null, _triggerData = null, _waitForTrigger = false, _triggerContext = 'renderer', _triggerType = 'rendererStateKeyValueChanged', _timeout = this.getSettings().rendererStateTriggerConfirmationTimout)
474 | //waitForActionTrigger(_actionPromise, _triggerData, _waitForTrigger = false, _triggerContext = 'renderer', _triggerType = 'rendererStateKeyValueChanged')
475 | {
476 | var self = this
477 | var timeoutId = 0;
478 |
479 | // if we should not wait for any trigger then return the action promise
480 | if(!_waitForTrigger)
481 | return this.callAction(_service, _action, _params);
482 |
483 | // be sure we have the renderer identificatrion in the trigger
484 | if(_triggerData && !_triggerData.rendererUdn)
485 | _triggerData.rendererUdn = this.udn()
486 |
487 | return new Promise(function(resolve, reject){
488 | var triggerCreated = false
489 | var keyAlreadySet = false;
490 |
491 | // check if one of the values is already set on the renderer, then we do not have to set up a trigger because
492 | // value is already there!
493 | keyAlreadySet = self.compareRendererTriggerValue(_triggerData)
494 | if(!keyAlreadySet)
495 | {
496 | // setup a new 'oneShot' trigger before calling the action
497 | triggerCreated = true;
498 | self.managerDisposer.triggerManager.setupTrigger(_triggerContext, _triggerType, _triggerData, true, function(_data){
499 | // clear the reponse timeout
500 | // TODO: be sure it was not already rejected!
501 | if(timeoutId)
502 | clearTimeout(timeoutId)
503 | resolve(_data)
504 | })
505 | // add a timeout for states that will not change
506 | timeoutId = setTimeout(function() {
507 | reject(new Error("State was not triggered in a valid time!"));
508 | }, _timeout)
509 | }
510 | // call the action and on 'then' do not resolve, the resolve will be done by the trigger which was set up
511 | // there is a pitfall because we do not really know that the state changed by our call of 'setupTrigger'
512 | self.callAction(_service, _action, _params).then(function(_data){
513 | // well.. no resolve here! Trigger has to be set up before the action call
514 | if(!triggerCreated)
515 | resolve(_data)
516 | }).catch(function(_data){
517 | reject(_data)
518 | })
519 | })
520 | }
521 |
522 | }
523 |
--------------------------------------------------------------------------------
/lib/lib.external.upnp-device-client.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var util = require('util');
3 | var EventEmitter = require('events').EventEmitter;
4 | var et = require('elementtree');
5 | var parseUrl = require('url').parse;
6 | var os = require('os');
7 | var concat = require('concat-stream');
8 | var ip = require('ip');
9 | var debug = require('debug')('upnp-device-client');
10 |
11 | var OS_VERSION = "";
12 | var PACKAGE_VERSION = "";
13 |
14 | var SUBSCRIPTION_TIMEOUT = 300;
15 |
16 |
17 | function DeviceClient(url) {
18 | EventEmitter.call(this);
19 |
20 | this.url = url;
21 | this.deviceDescription = null;
22 | this.serviceDescriptions = {};
23 | this.server = null;
24 | this.listening = false;
25 | this.subscriptions = {};
26 | this.userAgent = "";
27 | }
28 |
29 | util.inherits(DeviceClient, EventEmitter);
30 |
31 |
32 | DeviceClient.prototype.getDeviceDescription = function(callback) {
33 | var self = this;
34 |
35 | // Use cache if available
36 | if(this.deviceDescription) {
37 | process.nextTick(function() {
38 | callback(null, self.deviceDescription);
39 | });
40 | return;
41 | }
42 |
43 | debug('fetch device description');
44 | fetch(this.url, function(err, body) {
45 | if(err) return callback(err);
46 | var desc = parseDeviceDescription(body, self.url);
47 | self.deviceDescription = desc // Store in cache for next call
48 | callback(null, desc);
49 | });
50 | };
51 |
52 |
53 | DeviceClient.prototype.getServiceDescription = function(serviceId, callback) {
54 | var self = this;
55 |
56 | serviceId = resolveService(serviceId);
57 |
58 | this.getDeviceDescription(function(err, desc) {
59 | if(err) return callback(err);
60 |
61 | var service = desc.services[serviceId];
62 | if(!service) {
63 | var err = new Error('Service ' + serviceId + ' not provided by device');
64 | err.code = 'ENOSERVICE';
65 | return callback(err);
66 | }
67 |
68 | // Use cache if available
69 | if(self.serviceDescriptions[serviceId]) {
70 | return callback(null, self.serviceDescriptions[serviceId]);
71 | }
72 |
73 | debug('fetch service description (%s)', serviceId);
74 | fetch(service.SCPDURL, function(err, body) {
75 | if(err) return callback(err);
76 | var desc = parseServiceDescription(body);
77 | self.serviceDescriptions[serviceId] = desc; // Store in cache for next call
78 | callback(null, desc);
79 | });
80 | });
81 | };
82 |
83 |
84 | DeviceClient.prototype.callAction = function(serviceId, actionName, params, callback) {
85 | var self = this;
86 | serviceId = resolveService(serviceId);
87 |
88 | this.getServiceDescription(serviceId, function(err, desc) {
89 | if(err) return callback(err);
90 |
91 | if(!desc.actions[actionName]) {
92 | var err = new Error('Action ' + actionName + ' not implemented by service');
93 | err.code = 'ENOACTION';
94 | return callback(err);
95 | }
96 |
97 | var service = self.deviceDescription.services[serviceId];
98 |
99 | // Build SOAP action body
100 | var envelope = et.Element('s:Envelope');
101 | envelope.set('xmlns:s', 'http://schemas.xmlsoap.org/soap/envelope/');
102 | envelope.set('s:encodingStyle', 'http://schemas.xmlsoap.org/soap/encoding/');
103 |
104 | var body = et.SubElement(envelope, 's:Body');
105 | var action = et.SubElement(body, 'u:' + actionName);
106 | action.set('xmlns:u', service.serviceType);
107 |
108 | Object.keys(params).forEach(function(paramName) {
109 | var tmp = et.SubElement(action, paramName);
110 | var value = params[paramName];
111 | tmp.text = (value === null)
112 | ? ''
113 | : params[paramName].toString();
114 | });
115 |
116 | var doc = new et.ElementTree(envelope);
117 | var xml = doc.write({
118 | xml_declaration: true,
119 | });
120 |
121 | // Send action request
122 | var options = parseUrl(service.controlURL);
123 | options.method = 'POST';
124 | options.headers = {
125 | 'HOST': options.host,
126 | 'User-Agent': [self.userAgent, OS_VERSION, 'UPnP/1.1', PACKAGE_VERSION].join(' '),
127 | 'Content-Type': 'text/xml; charset="utf-8"',
128 | 'Content-Length': xml.length,
129 | 'Connection': 'close',
130 | 'SOAPACTION': '"' + service.serviceType + '#' + actionName + '"'
131 | };
132 |
133 | debug('call action %s on service %s with params %j', actionName, serviceId, params);
134 |
135 | var req = http.request(options, function(res) {
136 | res.pipe(concat(function(buf) {
137 | var doc = et.parse(buf.toString());
138 |
139 | if(res.statusCode !== 200) {
140 | var errorCode = doc.findtext('.//errorCode');
141 | var errorDescription = doc.findtext('.//errorDescription').trim();
142 |
143 | var err = new Error(errorDescription + ' (' + errorCode + ')');
144 | err.code = 'EUPNP';
145 | err.statusCode = res.statusCode;
146 | err.errorCode = errorCode;
147 | return callback(err);
148 | }
149 |
150 | // Extract response outputs
151 | var serviceDesc = self.serviceDescriptions[serviceId];
152 | var actionDesc = serviceDesc.actions[actionName];
153 | var outputs = actionDesc.outputs.map(function(desc) {
154 | return desc.name;
155 | });
156 |
157 | var result = {};
158 | outputs.forEach(function(name) {
159 | result[name] = doc.findtext('.//' + name);
160 | });
161 |
162 | callback(null, result)
163 | }));
164 | });
165 |
166 | req.on('error', callback);
167 | req.end(xml);
168 | });
169 | };
170 |
171 |
172 | DeviceClient.prototype.subscribe = function(serviceId, listener) {
173 | var self = this;
174 | serviceId = resolveService(serviceId);
175 |
176 | if(this.subscriptions[serviceId]) {
177 | // If we already have a subscription to this service,
178 | // add the provided callback to the listeners and return
179 | this.subscriptions[serviceId].listeners.push(listener);
180 | return;
181 | }
182 |
183 | // If there's no subscription to this service, create one
184 | // by first fetching the event subscription URL ...
185 | this.getDeviceDescription(function(err, desc) {
186 | if(err) return self.emit('error', err);
187 |
188 | var service = desc.services[serviceId];
189 |
190 | if(!service) {
191 | var err = new Error('Service ' + serviceId + ' not provided by device');
192 | err.code = 'ENOSERVICE';
193 | return self.emit('error', err);
194 | }
195 |
196 | // ... and ensuring the event server is created and listening
197 | self.ensureEventingServer(function() {
198 |
199 | var options = parseUrl(service.eventSubURL);
200 | var server = self.server;
201 |
202 | options.method = 'SUBSCRIBE';
203 | options.headers = {
204 | 'HOST': options.host,
205 | 'USER-AGENT': [self.userAgent, OS_VERSION, 'UPnP/1.1', PACKAGE_VERSION].join(' '),
206 | 'CALLBACK': '',
207 | 'NT': 'upnp:event',
208 | 'TIMEOUT': 'Second-' + SUBSCRIPTION_TIMEOUT
209 | };
210 |
211 | var req = http.request(options, function(res) {
212 | if(res.statusCode !== 200) {
213 | var err = new Error('SUBSCRIBE error');
214 | err.statusCode = res.statusCode;
215 | self.releaseEventingServer();
216 | self.emit('error', err);
217 | return;
218 | }
219 |
220 | var sid = res.headers['sid'];
221 | var timeout = parseTimeout(res.headers['timeout']);
222 |
223 | function renew() {
224 | debug('renew subscription to %s', serviceId);
225 |
226 | var options = parseUrl(service.eventSubURL);
227 | options.method = 'SUBSCRIBE';
228 | options.headers = {
229 | 'HOST': options.host,
230 | 'SID': sid,
231 | 'TIMEOUT': 'Second-' + SUBSCRIPTION_TIMEOUT
232 | };
233 |
234 | var req = http.request(options, function(res) {
235 | if(res.statusCode !== 200) {
236 | var err = new Error('SUBSCRIBE renewal error');
237 | err.statusCode = res.statusCode;
238 | // XXX: should we clear the subscription and release the server here ?
239 | self.emit('error', err);
240 | return;
241 | }
242 |
243 | var timeout = parseTimeout(res.headers['timeout']);
244 |
245 | var renewTimeout = Math.max(timeout - 30, 30); // renew 30 seconds before expiration
246 | debug('renewing subscription to %s in %d seconds', serviceId, renewTimeout);
247 | var timer = setTimeout(renew, renewTimeout * 1000);
248 | self.subscriptions[serviceId].timer = timer;
249 | });
250 |
251 | req.on('error', function(err) {
252 | self.emit('error', err);
253 | });
254 |
255 | req.end();
256 | }
257 |
258 | var renewTimeout = Math.max(timeout - 30, 30); // renew 30 seconds before expiration
259 | debug('renewing subscription to %s in %d seconds', serviceId, renewTimeout);
260 | var timer = setTimeout(renew, renewTimeout * 1000);
261 |
262 | self.subscriptions[serviceId] = {
263 | sid: sid,
264 | url: service.eventSubURL,
265 | timer: timer,
266 | listeners: [listener]
267 | };
268 |
269 | });
270 |
271 | req.on('error', function(err) {
272 | self.releaseEventingServer();
273 | self.emit('error', err);
274 | });
275 |
276 | req.end();
277 | });
278 |
279 | });
280 | };
281 |
282 |
283 | DeviceClient.prototype.unsubscribe = function(serviceId, listener) {
284 | var self = this;
285 | serviceId = resolveService(serviceId);
286 |
287 | // First make sure there are subscriptions for this service ...
288 | var subscription = this.subscriptions[serviceId];
289 | if(!subscription) return;
290 |
291 | // ... and we know about this listener
292 | var idx = subscription.listeners.indexOf(listener);
293 | if(idx === -1) return;
294 |
295 | // Remove the listener from the list
296 | subscription.listeners.splice(idx, 1);
297 |
298 | if(subscription.listeners.length === 0) {
299 | // If there's no listener left for this service, unsubscribe from it
300 | debug('unsubscribe from service %s', serviceId);
301 |
302 | var options = parseUrl(subscription.url);
303 |
304 | options.method = 'UNSUBSCRIBE';
305 | options.headers = {
306 | 'HOST': options.host,
307 | 'SID': subscription.sid
308 | };
309 |
310 | var req = http.request(options, function(res) {
311 | if(res.statusCode !== 200) {
312 | var err = new Error('UNSUBSCRIBE error');
313 | err.statusCode = res.statusCode;
314 | return self.emit('error', err);
315 | }
316 |
317 | clearTimeout(self.subscriptions[serviceId].timer);
318 | delete self.subscriptions[serviceId];
319 | // Make sure the eventing server is shutdown if there is no
320 | // subscription left for any service
321 | self.releaseEventingServer();
322 | });
323 |
324 | req.on('error', function(err) {
325 | self.emit('error', err);
326 | // well this may occur if the client was removed and can not answer anymore, so we have to remove the subscription!
327 | clearTimeout(self.subscriptions[serviceId].timer);
328 | delete self.subscriptions[serviceId];
329 | self.releaseEventingServer();
330 | });
331 |
332 | req.end();
333 | }
334 | };
335 |
336 |
337 | DeviceClient.prototype.unsubscribeAll = function(serviceId) {
338 | var self = this;
339 | serviceId = resolveService(serviceId);
340 |
341 | // First make sure there are subscriptions for this service ...
342 | var subscription = this.subscriptions[serviceId];
343 | if(!subscription) return;
344 |
345 | // Remove the listener from the list
346 | subscription.listeners.splice(0, subscription.listeners.length);
347 |
348 | if(subscription.listeners.length === 0) {
349 | // If there's no listener left for this service, unsubscribe from it
350 | debug('unsubscribe from service %s', serviceId);
351 |
352 | var options = parseUrl(subscription.url);
353 |
354 | options.method = 'UNSUBSCRIBE';
355 | options.headers = {
356 | 'HOST': options.host,
357 | 'SID': subscription.sid
358 | };
359 |
360 | var req = http.request(options, function(res) {
361 | if(res.statusCode !== 200) {
362 | var err = new Error('UNSUBSCRIBE error');
363 | err.statusCode = res.statusCode;
364 | return self.emit('error', err);
365 | }
366 |
367 | clearTimeout(self.subscriptions[serviceId].timer);
368 | delete self.subscriptions[serviceId];
369 | // Make sure the eventing server is shutdown if there is no
370 | // subscription left for any service
371 | self.releaseEventingServer();
372 | });
373 |
374 | req.on('error', function(err) {
375 | self.emit('error', err);
376 | // well this may occur if the client was removed and can not answer anymore, so we have to remove the subscription!
377 | clearTimeout(self.subscriptions[serviceId].timer);
378 | delete self.subscriptions[serviceId];
379 | self.releaseEventingServer();
380 | });
381 |
382 | req.end();
383 | }
384 | };
385 |
386 |
387 |
388 | DeviceClient.prototype.ensureEventingServer = function(callback) {
389 | var self = this;
390 |
391 | if(!this.server) {
392 | debug('create eventing server');
393 | this.server = http.createServer(function(req, res) {
394 |
395 | req.pipe(concat(function(buf) {
396 | var sid = req.headers['sid'];
397 | var seq = req.headers['seq'];
398 | var events;
399 | try {
400 | events = parseEvents(buf);
401 | }
402 | catch (ex) {
403 | return;
404 | }
405 | //console.log(events);
406 | //console.log('received events %s %d %j', sid, seq, events);
407 |
408 | debug('received events %s %d %j', sid, seq, events);
409 |
410 | var keys = Object.keys(self.subscriptions);
411 | var sids = keys.map(function(key) {
412 | return self.subscriptions[key].sid;
413 | })
414 |
415 | var idx = sids.indexOf(sid);
416 | if(idx === -1) {
417 | debug('WARNING unknown SID %s', sid);
418 | // silently ignore unknown SIDs
419 | return;
420 | }
421 |
422 | var serviceId = keys[idx];
423 | var listeners = self.subscriptions[serviceId].listeners;
424 |
425 |
426 |
427 | // Dispatch each event to each listener registered for
428 | // this service's events
429 | listeners.forEach(function(listener) {
430 | events.forEach(function(e) {
431 | listener(e);
432 | });
433 | });
434 |
435 | // be sure we quit response by sending back a 200 OK, otherwise well developed UPNP Devices will kick us out of their subscription list
436 | res.end();
437 |
438 | }));
439 |
440 | });
441 |
442 | // be sure that we are listening on the correct interface where the client resides
443 | var iface = this.getIfaceForUrl(this.url);
444 | this.server.listen(0, ip.address(iface));
445 | }
446 |
447 | if(!this.listening) {
448 | this.server.on('listening', function() {
449 | self.listening = true;
450 | callback();
451 | });
452 | } else {
453 | process.nextTick(callback);
454 | }
455 | };
456 |
457 |
458 | DeviceClient.prototype.releaseEventingServer = function(_force = false) {
459 | if(Object.keys(this.subscriptions).length === 0 || _force) {
460 | debug('shutdown eventing server');
461 | if(this.server)
462 | {
463 | this.server.close();
464 | this.server = null;
465 | this.listening = false;
466 | }
467 | }
468 | };
469 |
470 |
471 | DeviceClient.prototype.getIfaceForUrl = function(_url) {
472 | var options = parseUrl(_url);
473 | var interfaces = os.networkInterfaces();
474 | var retIface = "";
475 |
476 | Object.keys(interfaces).map(function (nic) {
477 | for (var i = 0; i < interfaces[nic].length; i++)
478 | {
479 | if(interfaces[nic][i].family.toLowerCase() == "ipv4")
480 | {
481 | var base1 = ip.mask(interfaces[nic][i].address, interfaces[nic][i].netmask);
482 | var base2 = ip.mask(options.hostname, interfaces[nic][i].netmask); // TODO: maybe resolve IP here from hostname if hostname is not an ip
483 | if (base1 == base2)
484 | retIface = nic;
485 | }
486 | }
487 | });
488 | return retIface;
489 | };
490 |
491 |
492 | function parseEvents(buf) {
493 | var events = [];
494 | var doc = et.parse(buf.toString());
495 |
496 | var lastChange = doc.findtext('.//LastChange');
497 | if(lastChange) {
498 | // AVTransport and RenderingControl services embed event data
499 | // in an `` element stored as an URIencoded string.
500 | doc = et.parse(lastChange);
501 |
502 | // The `` element contains one ``
503 | // subtree per stream instance reporting its status.
504 | var instances = doc.findall('./InstanceID');
505 | instances.forEach(function(instance) {
506 | var data = {
507 | InstanceID: Number(instance.get('val'))
508 | };
509 | instance.findall('./*').forEach(function(node) {
510 | data[node.tag] = node.get('val');
511 | });
512 | events.push(data);
513 | });
514 | } else {
515 | // In any other case, each variable is stored separately in a
516 | // `` tag
517 | var data = {};
518 | doc.findall('./property/*').forEach(function(node) {
519 | data[node.tag] = node.text;
520 | });
521 | events.push(data);
522 | }
523 |
524 | var systemUpdateId = doc.findtext('.//SystemUpdateID');
525 | if(systemUpdateId) {
526 | var data = {};
527 | data["SystemUpdateID"] = systemUpdateId;
528 | events.push(data);
529 | };
530 |
531 | //console.log("xxxxxxxxxxxxxxxxxxxxxx");
532 | var containerUpdateIds = doc.findtext('.//ContainerUpdateIDs');
533 | if(containerUpdateIds) {
534 | var data = {};
535 | data["ContainerUpdateIDs"] = containerUpdateIds;
536 | events.push(data);
537 | };
538 |
539 | return events;
540 | }
541 |
542 |
543 | function parseTimeout(header) {
544 | return Number(header.split('-')[1]);
545 | }
546 |
547 |
548 | function parseDeviceDescription(xml, url) {
549 | var doc = et.parse(xml);
550 |
551 | var desc = extractFields(doc.find('./device'), [
552 | 'deviceType',
553 | 'friendlyName',
554 | 'manufacturer',
555 | 'manufacturerURL',
556 | 'modelName',
557 | 'modelNumber',
558 | 'modelDescription',
559 | 'UDN'
560 | ]);
561 |
562 | var nodes = doc.findall('./device/iconList/icon');
563 | desc.icons = nodes.map(function(icon) {
564 | return extractFields(icon, [
565 | 'mimetype',
566 | 'width',
567 | 'height',
568 | 'depth',
569 | 'url'
570 | ]);
571 | });
572 |
573 | var nodes = doc.findall('./device/serviceList/service');
574 | desc.services = {};
575 | nodes.forEach(function(service) {
576 | var tmp = extractFields(service, [
577 | 'serviceType',
578 | 'serviceId',
579 | 'SCPDURL',
580 | 'controlURL',
581 | 'eventSubURL'
582 | ]);
583 |
584 | var id = tmp.serviceId;
585 | delete tmp.serviceId;
586 | desc.services[id] = tmp;
587 | });
588 |
589 | // Make URLs absolute
590 | var baseUrl = extractBaseUrl(url);
591 |
592 | desc.icons.map(function(icon) {
593 | icon.url = buildAbsoluteUrl(baseUrl, icon.url);
594 | return icon;
595 | });
596 |
597 | Object.keys(desc.services).forEach(function(id) {
598 | var service = desc.services[id];
599 | service.SCPDURL = buildAbsoluteUrl(baseUrl, service.SCPDURL);
600 | service.controlURL = buildAbsoluteUrl(baseUrl, service.controlURL);
601 | service.eventSubURL = buildAbsoluteUrl(baseUrl, service.eventSubURL);
602 | });
603 |
604 | return desc;
605 | }
606 |
607 |
608 | function parseServiceDescription(xml) {
609 | var doc = et.parse(xml);
610 | var desc = {};
611 |
612 | desc.actions = {};
613 | var nodes = doc.findall('./actionList/action');
614 | nodes.forEach(function(action) {
615 | var name = action.findtext('./name');
616 | var inputs = [];
617 | var outputs = [];
618 |
619 | var nodes = action.findall('./argumentList/argument');
620 | nodes.forEach(function(argument) {
621 | var arg = extractFields(argument, [
622 | 'name',
623 | 'direction',
624 | 'relatedStateVariable'
625 | ]);
626 |
627 | var direction = arg.direction;
628 | delete arg.direction;
629 |
630 | if(direction === 'in') inputs.push(arg);
631 | else outputs.push(arg);
632 | });
633 |
634 | desc.actions[name] = {
635 | inputs: inputs,
636 | outputs: outputs
637 | };
638 | });
639 |
640 | desc.stateVariables = {};
641 | var nodes = doc.findall('./serviceStateTable/stateVariable');
642 | nodes.forEach(function(stateVariable) {
643 | var name = stateVariable.findtext('./name');
644 |
645 | var nodes = stateVariable.findall('./allowedValueList/allowedValue');
646 | var allowedValues = nodes.map(function(allowedValue) {
647 | return allowedValue.text;
648 | });
649 |
650 | desc.stateVariables[name] = {
651 | dataType: stateVariable.findtext('./dataType'),
652 | sendEvents: stateVariable.get('sendEvents'),
653 | allowedValues: allowedValues,
654 | defaultValue: stateVariable.findtext('./defaultValue')
655 | };
656 | });
657 |
658 | return desc;
659 | }
660 |
661 |
662 | function fetch(url, callback) {
663 | var req = http.get(url, function(res) {
664 | if(res.statusCode !== 200) {
665 | var err = new Error('Request failed');
666 | err.statusCode = res.statusCode;
667 | return callback(err);
668 | }
669 | res.pipe(concat(function(buf) {
670 | callback(null, buf.toString())
671 | }));
672 | });
673 |
674 | req.on('error', callback);
675 | req.end();
676 | }
677 |
678 |
679 | function extractFields(node, fields) {
680 | var data = {};
681 | fields.forEach(function(field) {
682 | var value = node.findtext('./' + field);
683 | if(typeof value !== 'undefined') {
684 | data[field] = value;
685 | }
686 | });
687 | return data;
688 | }
689 |
690 |
691 | function buildAbsoluteUrl(base, url) {
692 | if(url === '') return '';
693 | if(url.substring(0, 4) === 'http') return url;
694 | if(url[0] === '/') {
695 | var root = base.split('/').slice(0, 3).join('/'); // http://host:port
696 | return root + url;
697 | } else {
698 | return base + '/' + url;
699 | }
700 | }
701 |
702 |
703 | function extractBaseUrl(url) {
704 | return url.split('/').slice(0, -1).join('/');
705 | }
706 |
707 |
708 | function resolveService(serviceId) {
709 | return (serviceId.indexOf(':') === -1)
710 | ? 'urn:upnp-org:serviceId:' + serviceId
711 | : serviceId;
712 | }
713 |
714 |
715 | module.exports = DeviceClient;
--------------------------------------------------------------------------------