Skip to content

Commit 59e5309

Browse files
committed
Initial release
0 parents  commit 59e5309

34 files changed

+8606
-0
lines changed

.eslintrc.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
"env": {
3+
"commonjs": true,
4+
"es2021": true,
5+
"node": true,
6+
},
7+
"extends": "eslint:recommended",
8+
"parserOptions": {
9+
"ecmaVersion": 12,
10+
"sourceType": "module",
11+
},
12+
"rules": {
13+
}
14+
};

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
out/
3+
.idea/

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Samuel CHEMLA
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.

api/.eslintrc.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
"env": {
3+
"browser": false,
4+
"node": true,
5+
},
6+
};

api/index.js

+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
'use strict';
2+
3+
const logger = require('./src/loggerFactory')();
4+
const Client = require('node-ssdp').Client;
5+
const client = new Client({reuseAddr: false, explicitSocketBind: true, customLogger: logger});
6+
const axios = require('./src/axios');
7+
const EventEmitter = require('events');
8+
const eventEmitter = new EventEmitter();
9+
const deviceFactory = require('./src/DeviceFactory');
10+
const MediaServer = require('./src/MediaServer');
11+
const MediaRenderer = require('./src/MediaRenderer');
12+
13+
const ssdpDevices = {};
14+
15+
let currentMediaServer;
16+
let currentMediaRenderer;
17+
18+
function escapeXml(unsafe) {
19+
if (unsafe == null) {
20+
return '';
21+
}
22+
return unsafe.replace(/[<>&'"]/g, function (c) {
23+
switch (c) {
24+
case '<':
25+
return '&lt;';
26+
case '>':
27+
return '&gt;';
28+
case '&':
29+
return '&amp;';
30+
case '\'':
31+
return '&apos;';
32+
case '"':
33+
return '&quot;';
34+
}
35+
});
36+
}
37+
38+
client.on('notify', function () {
39+
logger('Got a notification.')
40+
})
41+
42+
client.on('response', function (headers, statusCode, rinfo) {
43+
if (statusCode !== 200) {
44+
return
45+
}
46+
if (!headers.USN || !(/MediaServer:[0-5]$/.test(headers.USN) || /MediaRenderer:[0-5]$/.test(headers.USN))) {
47+
return
48+
}
49+
if (!headers.LOCATION) {
50+
return
51+
}
52+
if (ssdpDevices[headers.USN]) {
53+
return;
54+
}
55+
logger('Got a response to an m-search:\n%d\n%s\n%s', statusCode, JSON.stringify(headers, null, ' '), JSON.stringify(rinfo, null, ' '))
56+
ssdpDevices[headers.USN] = axios.get(headers.LOCATION)
57+
.then(function (response) {
58+
const device = deviceFactory.createFromXml(response.data, headers.LOCATION)
59+
ssdpDevices[headers.USN] = device;
60+
eventEmitter.emit('device', device)
61+
return device;
62+
})
63+
.catch(function (error) {
64+
// handle error
65+
logger(error);
66+
ssdpDevices[headers.USN] = null;
67+
});
68+
});
69+
70+
const ssdpStart = function () {
71+
return client.start();
72+
}
73+
74+
const ssdpStop = function () {
75+
return client.stop();
76+
}
77+
78+
const ssdpSearch = function () {
79+
//client.search('urn:schemas-upnp-org:service:ContentDirectory:1');
80+
client.search('ssdp:all');
81+
}
82+
83+
const _getDevices = function (type) {
84+
return Object.keys(ssdpDevices)
85+
.filter(usn => ssdpDevices[usn] instanceof type)
86+
.map(usn => {
87+
return {
88+
usn,
89+
name: ssdpDevices[usn].getName()
90+
}
91+
})
92+
}
93+
94+
/**
95+
* Return discovered renderers
96+
* @returns {{usn: *, name: *}[]}
97+
*/
98+
const getRenderers = function () {
99+
return _getDevices(MediaRenderer);
100+
}
101+
102+
/**
103+
* Return discovered renderers
104+
* @returns {{usn: *, name: *}[]}
105+
*/
106+
const getServers = function () {
107+
return _getDevices(MediaServer);
108+
}
109+
110+
/**
111+
* Set current active renderer
112+
* @param {string} usn
113+
*/
114+
const selectRenderer = function ({usn}) {
115+
if (ssdpDevices[usn] == null) {
116+
throw new Error(`No such device ${usn}`);
117+
}
118+
currentMediaRenderer = ssdpDevices[usn]
119+
}
120+
121+
/**
122+
* Set current active server
123+
* @param {string} usn
124+
*/
125+
const selectServer = function ({usn}) {
126+
if (ssdpDevices[usn] == null) {
127+
throw new Error(`No such device ${usn}`);
128+
}
129+
currentMediaServer = ssdpDevices[usn]
130+
}
131+
132+
const browse = function ({id, start, count}) {
133+
if (currentMediaServer == null) {
134+
return Promise.reject(new Error('No media server selected'));
135+
}
136+
return currentMediaServer.browse({id, start, count});
137+
}
138+
139+
/**
140+
*
141+
* @param id
142+
* @param [uri] Optional URI (allows fallback if getting metadata fails)
143+
*/
144+
const play = function ({id, uri}) {
145+
if (currentMediaRenderer == null) {
146+
return Promise.reject(new Error('No renderer selected'));
147+
}
148+
return currentMediaRenderer.getTransportInfo({instanceID: 0})
149+
.then((transportInfo) => {
150+
if ([undefined, null, 'NO_MEDIA_PRESENT', 'STOPPED'].includes(transportInfo?.CurrentTransportState)) {
151+
return Promise.resolve();
152+
}
153+
// We need to stop before playing new media (some renderers don't support changing URI while playing)
154+
return currentMediaRenderer.stop({instanceID: 0});
155+
})
156+
.then(() => {
157+
return currentMediaServer
158+
.getMetadata({id})
159+
.catch((err) => {
160+
// Allows for a fallback if getting metadata fails (PlainUPnP server)
161+
if (uri == null) {
162+
throw err;
163+
}
164+
return {object: {res: {'#text': uri}}}
165+
})
166+
})
167+
.then((metadata) => {
168+
return currentMediaRenderer.setAVTransportURI({
169+
instanceID: 0,
170+
currentURI: metadata.object.res['#text'],
171+
currentUriMetadata: escapeXml(metadata.xml)
172+
})
173+
})
174+
.then(() => currentMediaRenderer.play({instanceID: 0, speed: 1}));
175+
}
176+
177+
const getPositionInfo = function () {
178+
return currentMediaRenderer.getPositionInfo({instanceID: 0});
179+
}
180+
181+
const getTransportInfo = function () {
182+
return currentMediaRenderer.getTransportInfo({instanceID: 0});
183+
}
184+
185+
const resume = function () {
186+
if (currentMediaRenderer == null) {
187+
return Promise.reject(new Error('No renderer selected'));
188+
}
189+
return currentMediaRenderer.play({instanceID: 0, speed: 1});
190+
}
191+
192+
const pause = function () {
193+
if (currentMediaRenderer == null) {
194+
return Promise.reject(new Error('No renderer selected'));
195+
}
196+
return currentMediaRenderer.pause({instanceID: 0});
197+
}
198+
199+
const stop = function () {
200+
if (currentMediaRenderer == null) {
201+
return Promise.reject(new Error('No renderer selected'));
202+
}
203+
return currentMediaRenderer.stop({instanceID: 0});
204+
}
205+
206+
const seek = function ({at}) {
207+
if (currentMediaRenderer == null) {
208+
return Promise.reject(new Error('No renderer selected'));
209+
}
210+
return currentMediaRenderer.seek({instanceID: 0, unit: 'REL_TIME', target: at})
211+
}
212+
213+
const getVolumeDBRange = function () {
214+
if (currentMediaRenderer == null) {
215+
return Promise.reject(new Error('No renderer selected'));
216+
}
217+
return currentMediaRenderer.getVolumeDBRange({instanceID: 0});
218+
}
219+
220+
const getVolumeDB = function () {
221+
if (currentMediaRenderer == null) {
222+
return Promise.reject(new Error('No renderer selected'));
223+
}
224+
return currentMediaRenderer.getVolumeDB({instanceID: 0});
225+
}
226+
227+
const getVolume = function () {
228+
if (currentMediaRenderer == null) {
229+
return Promise.reject(new Error('No renderer selected'));
230+
}
231+
return currentMediaRenderer.getVolume({instanceID: 0});
232+
}
233+
234+
const setVolume = function ({desiredVolume}) {
235+
if (currentMediaRenderer == null) {
236+
return Promise.reject(new Error('No renderer selected'));
237+
}
238+
return currentMediaRenderer
239+
.setMute({instanceID: 0, desiredMute: false}) // Try to unmute first but catch any error on this
240+
.catch(() => null)
241+
.then(() => currentMediaRenderer.setVolume({instanceID: 0, desiredVolume}));
242+
}
243+
244+
module.exports = {
245+
browse,
246+
play,
247+
resume,
248+
pause,
249+
stop,
250+
seek,
251+
ssdpStart,
252+
ssdpSearch,
253+
ssdpStop,
254+
getRenderers,
255+
getServers,
256+
selectRenderer,
257+
selectServer,
258+
eventEmitter,
259+
getPositionInfo,
260+
getTransportInfo,
261+
getVolumeDBRange,
262+
getVolumeDB,
263+
getVolume,
264+
setVolume,
265+
}

api/schemas/connectionmanager.xsd

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!-- php-dlna v1.0
2+
Copyright 2014 Torbjørn Tyridal ([email protected])
3+
This file is part of php-dlna.
4+
php-dlna is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU Affero General Public License version 3
6+
as published by the Free Software Foundation
7+
php-dlna is distributed in the hope that it will be useful,
8+
but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
GNU Affero General Public License for more details.
11+
You can get a copy The GNU Affero General Public license from
12+
http://www.gnu.org/licenses/agpl-3.0.html
13+
-->
14+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
15+
xmlns:u="urn:schemas-upnp-org:service:ConnectionManager:1"
16+
targetNamespace="urn:schemas-upnp-org:service:ConnectionManager:1">
17+
18+
<xs:element name="GetProtocolInfo">
19+
<xs:complexType/>
20+
</xs:element>
21+
<xs:element name="GetProtocolInfoResponse">
22+
<xs:complexType>
23+
<xs:sequence>
24+
<xs:element name="Source" type="xs:string" minOccurs="1" maxOccurs="1"/>
25+
<xs:element name="Sink" type="xs:string" minOccurs="1" maxOccurs="1"/>
26+
</xs:sequence>
27+
</xs:complexType>
28+
</xs:element>
29+
30+
</xs:schema>

0 commit comments

Comments
 (0)