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 '<' ;
26
+ case '>' :
27
+ return '>' ;
28
+ case '&' :
29
+ return '&' ;
30
+ case '\'' :
31
+ return ''' ;
32
+ case '"' :
33
+ return '"' ;
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 || ! ( / M e d i a S e r v e r : [ 0 - 5 ] $ / . test ( headers . USN ) || / M e d i a R e n d e r e r : [ 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
+ }
0 commit comments