@edenware/dlnacasts 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,124 +1,148 @@
1
- const MediaRenderer = require('upnp-mediarenderer-client')
2
- const events = require('events')
3
- const parallel = require('run-parallel')
4
- const parseString = require('xml2js').parseString
5
- const SSDP = require('@edenware/ssdp').Client
6
- const { isIP } = require('node:net')
7
- const http = require('node:http')
8
- const os = require('os')
9
- const agent = new http.Agent({ keepAlive: true, maxSockets: 5 })
10
-
11
- const SERVICE_TYPE = 'urn:schemas-upnp-org:device:MediaRenderer:1';
12
- const thunky = require('thunky')
13
-
14
- const noop = () => {}
15
-
16
- module.exports = (options = {}) => {
17
- const log = options.log ? console.debug : () => {}
18
-
19
- // Find local IP
20
- const interfaces = os.networkInterfaces()
21
- let localIP = '192.168.1.8' // fallback
1
+ 'use strict';
2
+
3
+ var MediaRenderer = require('upnp-mediarenderer-client');
4
+ var events = require('events');
5
+ var parallel = require('run-parallel');
6
+ var xml2js = require('xml2js');
7
+ var ssdp = require('@edenware/ssdp');
8
+ var node_net = require('node:net');
9
+ var http = require('node:http');
10
+ var os = require('os');
11
+ var thunky = require('thunky');
12
+
13
+ const noop = () => {};
14
+
15
+ var index = (options = {}) => {
16
+ const SERVICE_TYPE = 'urn:schemas-upnp-org:device:MediaRenderer:1';
17
+ const agent = new http.Agent({ keepAlive: true, maxSockets: 5 });
18
+ const log = options.log ? console.debug : () => {};
19
+
20
+ // Find local IP - prefer Wi-Fi/Ethernet
21
+ const interfaces = os.networkInterfaces();
22
+ log('[DLNACASTS] Network interfaces:', interfaces);
23
+ let localIP = null; // fallback
24
+ let localInterface = null;
25
+ // First pass: prefer Wi-Fi or Ethernet
22
26
  for (const name of Object.keys(interfaces)) {
23
- for (const iface of interfaces[name]) {
24
- if (iface.family === 'IPv4' && !iface.internal) {
25
- localIP = iface.address
26
- break
27
+ if (name.includes('Wi-Fi') || name.includes('Ethernet') || name.includes('eth') || name.includes('en') || name.includes('wlan')) {
28
+ for (const iface of interfaces[name]) {
29
+ if (iface.family === 'IPv4' && !iface.internal) {
30
+ localIP = iface.address;
31
+ localInterface = name;
32
+ log('[DLNACASTS] Using preferred interface:', name, localIP);
33
+ break
34
+ }
27
35
  }
36
+ if (localIP) break
28
37
  }
29
- if (localIP !== '192.168.1.8') break
30
38
  }
31
- log('Local IP:', localIP)
39
+ // Second pass: any IPv4 if no preferred found
40
+ if (!localIP) {
41
+ for (const name of Object.keys(interfaces)) {
42
+ for (const iface of interfaces[name]) {
43
+ if (iface.family === 'IPv4' && !iface.internal) {
44
+ localIP = iface.address;
45
+ localInterface = name;
46
+ log('[DLNACASTS] Using fallback interface:', name, localIP);
47
+ break
48
+ }
49
+ }
50
+ if (localIP) break
51
+ }
52
+ }
53
+ if (!localIP) localIP = '127.0.0.1'; // ultimate fallback
54
+ log('Local IP:', localIP);
32
55
 
33
- const that = new events.EventEmitter()
34
- const casts = {}
35
- const ssdp = SSDP ? new SSDP({
36
- unicastHost: localIP,
37
- log: true
38
- }) : null
56
+ const that = new events.EventEmitter();
57
+ const casts = {};
58
+ const ssdp$1 = ssdp.Client ? new ssdp.Client({
59
+ log: true,
60
+ interfaces: [localInterface],
61
+ explicitSocketBind: true
62
+ }) : null;
39
63
 
40
- log('[DLNACASTS] SSDP client created:', !!ssdp)
64
+ log('[DLNACASTS] SSDP client created:', !!ssdp$1);
41
65
 
42
- that.players = []
66
+ that.players = [];
43
67
 
44
68
  const emit = (cst) => {
45
69
  if (!cst || !cst.host || cst.emitted) return
46
- cst.emitted = true
70
+ cst.emitted = true;
47
71
 
48
- const player = new events.EventEmitter()
49
- let getStatus = undefined
50
- let stopped = false // Flag para rastrear se stop foi chamado
51
- let playFailed = false // Flag para rastrear se play falhou
72
+ const player = new events.EventEmitter();
73
+ let getStatus = undefined;
74
+ let stopped = false; // Flag para rastrear se stop foi chamado
75
+ let playFailed = false; // Flag para rastrear se play falhou
52
76
 
53
77
  const connect = thunky(function reconnect (cb) {
54
- const client = new MediaRenderer(player.xml)
78
+ const client = new MediaRenderer(player.xml);
55
79
 
56
80
  client.on('error', (err) => {
57
- log('[DLNACASTS] Client error, clearing player.client:', err)
58
- try { clearInterval(getStatus) } catch(e) {}
59
- player.client = undefined
60
- player.emit('error', err)
61
- })
81
+ log('[DLNACASTS] Client error, clearing player.client:', err);
82
+ try { clearInterval(getStatus); } catch(e) {}
83
+ player.client = undefined;
84
+ player.emit('error', err);
85
+ });
62
86
 
63
87
  client.on('loading', (err) => {
64
- player.emit('loading', err)
65
- })
88
+ player.emit('loading', err);
89
+ });
66
90
 
67
91
  client.on('close', () => {
68
- log('[DLNACASTS] Client closed, clearing player.client')
69
- try { clearInterval(getStatus) } catch(e) {}
70
- player.client = undefined
71
- connect = thunky(reconnect) // Reset thunky para permitir nova reconexão
72
- })
92
+ log('[DLNACASTS] Client closed, clearing player.client');
93
+ try { clearInterval(getStatus); } catch(e) {}
94
+ player.client = undefined;
95
+ connect = thunky(reconnect); // Reset thunky para permitir nova reconexão
96
+ });
73
97
 
74
- player.client = client
75
- cb(null, player.client)
76
- })
98
+ player.client = client;
99
+ cb(null, player.client);
100
+ });
77
101
 
78
102
  const parseTime = (time) => {
79
103
  if (!time || time.indexOf(':') === -1) return 0
80
- const parts = time.split(':').map(Number)
104
+ const parts = time.split(':').map(Number);
81
105
  return parts[0] * 3600 + parts[1] * 60 + parts[2]
82
- }
106
+ };
83
107
 
84
- player.name = cst.name
85
- player.host = cst.host
86
- player.xml = cst.xml
87
- player._status = {}
88
- player.MAX_VOLUME = 100
108
+ player.name = cst.name;
109
+ player.host = cst.host;
110
+ player.xml = cst.xml;
111
+ player._status = {};
112
+ player.MAX_VOLUME = 100;
89
113
 
90
- player.connect = connect
114
+ player.connect = connect;
91
115
 
92
116
  player.close = (cb = noop) => {
93
- log('[DLNACASTS] Closing player, clearing player.client')
94
- try { clearInterval(getStatus) } catch(e) {}
117
+ log('[DLNACASTS] Closing player, clearing player.client');
118
+ try { clearInterval(getStatus); } catch(e) {}
95
119
  if (player.client) {
96
120
  for (let e of ["error", "status", "loading", "close"]) {
97
- player.client.removeAllListeners(e)
121
+ player.client.removeAllListeners(e);
98
122
  }
99
- player.client = undefined
123
+ player.client = undefined;
100
124
  }
101
- stopped = true // Marcar que stop foi chamado
102
- cb()
103
- }
125
+ stopped = true; // Marcar que stop foi chamado
126
+ cb();
127
+ };
104
128
 
105
129
  player.play = (url, opts, cb = noop) => {
106
130
  if (typeof opts === 'function') return player.play(url, null, opts)
107
- if (!opts) opts = {}
131
+ if (!opts) opts = {};
108
132
  if (!url) return player.resume(cb)
109
133
 
110
- stopped = false // Resetar stopped ao chamar play
111
- playFailed = false // Resetar playFailed ao chamar play
112
- player.subtitles = opts.subtitles
134
+ stopped = false; // Resetar stopped ao chamar play
135
+ playFailed = false; // Resetar playFailed ao chamar play
136
+ player.subtitles = opts.subtitles;
113
137
 
114
138
  connect((err, p) => {
115
139
  if (err) {
116
- log('[DLNACASTS] Connect failed in play:', err)
117
- playFailed = true // Marcar que play falhou
140
+ log('[DLNACASTS] Connect failed in play:', err);
141
+ playFailed = true; // Marcar que play falhou
118
142
  return cb(err)
119
143
  }
120
144
 
121
- try { clearInterval(getStatus) } catch(e) {}
145
+ try { clearInterval(getStatus); } catch(e) {}
122
146
 
123
147
  const media = {
124
148
  autoplay: opts.autoPlay !== false,
@@ -128,119 +152,119 @@ module.exports = (options = {}) => {
128
152
  type: 'video', // can be 'video', 'audio' or 'image'
129
153
  subtitlesUrl: player.subtitles && player.subtitles.length ? player.subtitles[0] : null
130
154
  }
131
- }
155
+ };
132
156
  if (opts.dlnaFeatures) {
133
- media.dlnaFeatures = opts.dlnaFeatures || 'DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000'
157
+ media.dlnaFeatures = opts.dlnaFeatures || 'DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000';
134
158
  //media.dlnaFeatures = opts.dlnaFeatures; // for LG WebOS 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01100000000000000000000000000000' allows seeking
135
159
  }
136
160
 
137
- let callback = cb
161
+ let callback = cb;
138
162
  if (opts.seek) {
139
163
  callback = (err) => {
140
164
  if (err) {
141
- playFailed = true // Marcar que play falhou
165
+ playFailed = true; // Marcar que play falhou
142
166
  return cb(err)
143
167
  }
144
- player.seek(opts.seek, cb)
145
- }
168
+ player.seek(opts.seek, cb);
169
+ };
146
170
  }
147
171
 
148
172
  getStatus = setInterval(() => {
149
173
  if (stopped || playFailed) {
150
- log('[DLNACASTS] Skipping getStatus: stopped=%s, playFailed=%s', stopped, playFailed)
174
+ log('[DLNACASTS] Skipping getStatus: stopped=%s, playFailed=%s', stopped, playFailed);
151
175
  return
152
176
  }
153
177
  if (!player.client) {
154
- log('[DLNACASTS] player.client is undefined in getStatus, attempting to reconnect')
178
+ log('[DLNACASTS] player.client is undefined in getStatus, attempting to reconnect');
155
179
  connect((err) => {
156
180
  if (err) {
157
- log('[DLNACASTS] Reconnect failed in getStatus:', err)
158
- playFailed = true // Marcar falha se reconexão falhar
181
+ log('[DLNACASTS] Reconnect failed in getStatus:', err);
182
+ playFailed = true; // Marcar falha se reconexão falhar
159
183
  return
160
184
  }
161
- log('[DLNACASTS] Reconnected successfully in getStatus')
185
+ log('[DLNACASTS] Reconnected successfully in getStatus');
162
186
  player.client.callAction('AVTransport', 'GetTransportInfo', {
163
187
  InstanceID: player.client.instanceId
164
188
  }, (err, res) => {
165
189
  if (err) return
166
- const newStatus = res.CurrentTransportState
190
+ const newStatus = res.CurrentTransportState;
167
191
  if (newStatus !== player._status.playerState) {
168
- player._status.playerState = newStatus
192
+ player._status.playerState = newStatus;
169
193
  player.status((err, status) => {
170
194
  if (err) return
171
- player.emit('status', status)
172
- })
195
+ player.emit('status', status);
196
+ });
173
197
  }
174
- })
175
- })
198
+ });
199
+ });
176
200
  return
177
201
  }
178
202
  player.client.callAction('AVTransport', 'GetTransportInfo', {
179
203
  InstanceID: player.client.instanceId
180
204
  }, (err, res) => {
181
205
  if (err) return
182
- const newStatus = res.CurrentTransportState
206
+ const newStatus = res.CurrentTransportState;
183
207
  if (newStatus !== player._status.playerState) {
184
- player._status.playerState = newStatus
208
+ player._status.playerState = newStatus;
185
209
  player.status((err, status) => {
186
210
  if (err) return
187
- player.emit('status', status)
188
- })
211
+ player.emit('status', status);
212
+ });
189
213
  }
190
- })
191
- }, 1000)
214
+ });
215
+ }, 1000);
192
216
 
193
- p.load(url, media, callback)
194
- })
195
- }
217
+ p.load(url, media, callback);
218
+ });
219
+ };
196
220
 
197
221
  player.resume = (cb = noop) => {
198
- player.client.play(cb)
199
- }
222
+ player.client.play(cb);
223
+ };
200
224
 
201
225
  player.pause = (cb = noop) => {
202
- player.client.pause(cb)
203
- }
226
+ player.client.pause(cb);
227
+ };
204
228
 
205
229
  player.stop = (cb = noop) => {
206
- try { clearInterval(getStatus) } catch(e) {}
207
- stopped = true // Marcar que stop foi chamado
208
- player.client.stop(cb)
209
- }
230
+ try { clearInterval(getStatus); } catch(e) {}
231
+ stopped = true; // Marcar que stop foi chamado
232
+ player.client.stop(cb);
233
+ };
210
234
 
211
235
  player.status = (cb = noop) => {
212
236
  if (stopped || playFailed) {
213
- log('[DLNACASTS] Skipping status: stopped=%s, playFailed=%s', stopped, playFailed)
237
+ log('[DLNACASTS] Skipping status: stopped=%s, playFailed=%s', stopped, playFailed);
214
238
  return cb(null, player._status)
215
239
  }
216
240
  if (!player.client) {
217
- log('[DLNACASTS] player.client is undefined in status, attempting to reconnect')
241
+ log('[DLNACASTS] player.client is undefined in status, attempting to reconnect');
218
242
  connect((err) => {
219
243
  if (err) {
220
- log('[DLNACASTS] Reconnect failed in status:', err)
221
- playFailed = true // Marcar falha se reconexão falhar
244
+ log('[DLNACASTS] Reconnect failed in status:', err);
245
+ playFailed = true; // Marcar falha se reconexão falhar
222
246
  return cb(err)
223
247
  }
224
- log('[DLNACASTS] Reconnected successfully in status')
248
+ log('[DLNACASTS] Reconnected successfully in status');
225
249
  parallel({
226
250
  currentTime: (acb) => {
227
251
  player.client.callAction('AVTransport', 'GetPositionInfo', {
228
252
  InstanceID: player.client.instanceId
229
253
  }, (err, res) => {
230
254
  if (err) return acb()
231
- acb(null, parseTime(res.AbsTime) | parseTime(res.RelTime))
232
- })
255
+ acb(null, parseTime(res.AbsTime) | parseTime(res.RelTime));
256
+ });
233
257
  },
234
258
  volume: (acb) => {
235
- player.getVolume(acb)
259
+ player.getVolume(acb);
236
260
  }
237
261
  }, (err, results) => {
238
- log('dlnacasts player.status results: %o', results)
239
- player._status.currentTime = results.currentTime
240
- player._status.volume = { level: results.volume / player.MAX_VOLUME }
262
+ log('dlnacasts player.status results: %o', results);
263
+ player._status.currentTime = results.currentTime;
264
+ player._status.volume = { level: results.volume / player.MAX_VOLUME };
241
265
  return cb(err, player._status)
242
- })
243
- })
266
+ });
267
+ });
244
268
  return
245
269
  }
246
270
 
@@ -250,19 +274,19 @@ module.exports = (options = {}) => {
250
274
  InstanceID: player.client.instanceId
251
275
  }, (err, res) => {
252
276
  if (err) return acb()
253
- acb(null, parseTime(res.AbsTime) | parseTime(res.RelTime))
254
- })
277
+ acb(null, parseTime(res.AbsTime) | parseTime(res.RelTime));
278
+ });
255
279
  },
256
280
  volume: (acb) => {
257
- player.getVolume(acb)
281
+ player.getVolume(acb);
258
282
  }
259
283
  }, (err, results) => {
260
- log('dlnacasts player.status results: %o', results)
261
- player._status.currentTime = results.currentTime
262
- player._status.volume = { level: results.volume / player.MAX_VOLUME }
284
+ log('dlnacasts player.status results: %o', results);
285
+ player._status.currentTime = results.currentTime;
286
+ player._status.volume = { level: results.volume / player.MAX_VOLUME };
263
287
  return cb(err, player._status)
264
- })
265
- }
288
+ });
289
+ };
266
290
 
267
291
  player.getVolume = (cb) => {
268
292
  player.client.callAction('RenderingControl', 'GetVolume', {
@@ -270,124 +294,124 @@ module.exports = (options = {}) => {
270
294
  Channel: 'Master'
271
295
  }, (err, res) => {
272
296
  if (err) return cb()
273
- cb(null, res.CurrentVolume ? parseInt(res.CurrentVolume) : 0)
274
- })
275
- }
297
+ cb(null, res.CurrentVolume ? parseInt(res.CurrentVolume) : 0);
298
+ });
299
+ };
276
300
 
277
301
  player.setVolume = (vol, cb = noop) => {
278
302
  player.client.callAction('RenderingControl', 'SetVolume', {
279
303
  InstanceID: player.client.instanceId,
280
304
  Channel: 'Master',
281
305
  DesiredVolume: vol
282
- }, cb)
283
- }
306
+ }, cb);
307
+ };
284
308
 
285
309
  player.request = (target, action, data, cb = noop) => {
286
310
  if (data.InstanceID === null) {
287
- data.InstanceID = player.client.instanceId
311
+ data.InstanceID = player.client.instanceId;
288
312
  }
289
- player.client.callAction(target, action, data, cb)
290
- }
313
+ player.client.callAction(target, action, data, cb);
314
+ };
291
315
 
292
316
  player.seek = (time, cb = noop) => {
293
- player.client.seek(time, cb)
294
- }
317
+ player.client.seek(time, cb);
318
+ };
295
319
 
296
- that.players.push(player)
297
- that.emit('update', player)
298
- }
299
-
300
- if (ssdp) {
301
- // Response handler moved to update()
302
- }
320
+ that.players.push(player);
321
+ that.emit('update', player);
322
+ };
303
323
 
304
324
  that.validate = (name, host, xml) => {
305
325
  if (!casts[name]) {
306
326
  http.get(xml, { agent }, res => {
307
- const {statusCode} = res
327
+ const {statusCode} = res;
308
328
  if (statusCode == 200) {
309
329
  if (!casts[name]) {
310
- casts[name] = {name, host, xml}
311
- emit(casts[name])
312
- } else if (isIP(casts[name].host) != 4 && isIP(host) == 4) {
313
- casts[name].host = host
314
- casts[name].xml = xml
315
- emit(casts[name])
330
+ casts[name] = {name, host, xml};
331
+ emit(casts[name]);
332
+ } else if (node_net.isIP(casts[name].host) != 4 && node_net.isIP(host) == 4) {
333
+ casts[name].host = host;
334
+ casts[name].xml = xml;
335
+ emit(casts[name]);
316
336
  }
317
337
  }
318
- res.resume()
319
- }).on('error', e => {})
338
+ res.resume();
339
+ }).on('error', e => {});
320
340
  }
321
- }
341
+ };
322
342
 
323
343
  that.update = () => {
324
- log('[DLNACASTS] querying ssdp')
325
- if (ssdp) {
326
- const tasks = []
344
+ log('[DLNACASTS] querying ssdp');
345
+ if (ssdp$1) {
346
+ const tasks = [];
327
347
  const responseHandler = (headers, statusCode, info) => {
348
+ log('[DLNACASTS] SSDP response received:', headers.ST || 'no ST', 'from', info.address, 'LOCATION:', headers.LOCATION || 'no LOCATION');
328
349
  if (!headers.LOCATION) return
329
350
  if (headers.ST !== SERVICE_TYPE) return
330
351
 
331
352
  tasks.push((cb) => {
332
353
  http.get(headers.LOCATION, { agent }, (res) => {
333
354
  if (res.statusCode !== 200) return cb()
334
- let body = ''
335
- res.on('data', (chunk) => { body += chunk })
355
+ let body = '';
356
+ res.on('data', (chunk) => { body += chunk; });
336
357
  res.on('end', () => {
337
- parseString(body, {explicitArray: false, explicitRoot: false},
358
+ xml2js.parseString(body, {explicitArray: false, explicitRoot: false},
338
359
  (err, service) => {
339
360
  if (err) return cb()
340
361
  if (!service.device) return cb()
341
362
 
342
- log('[DLNACASTS] ssdp device:', service.device)
363
+ log('[DLNACASTS] ssdp device:', service.device);
343
364
 
344
- const name = service.device.friendlyName
365
+ const name = service.device.friendlyName;
345
366
 
346
367
  if (!name) return cb()
347
368
 
348
- const host = info.address
349
- const xml = headers.LOCATION
369
+ const host = info.address;
370
+ const xml = headers.LOCATION;
350
371
 
351
372
  if (!casts[name]) {
352
- casts[name] = {name: name, host: host, xml: xml}
353
- emit(casts[name])
373
+ casts[name] = {name: name, host: host, xml: xml};
374
+ emit(casts[name]);
354
375
  } else if (casts[name] && !casts[name].host) {
355
- casts[name].host = host
356
- casts[name].xml = xml
357
- emit(casts[name])
376
+ casts[name].host = host;
377
+ casts[name].xml = xml;
378
+ emit(casts[name]);
358
379
  }
359
- cb()
360
- })
361
- })
362
- }).on('error', () => cb())
363
- })
364
- }
365
- ssdp.on('response', responseHandler)
366
- ssdp.search(SERVICE_TYPE)
367
- log('[DLNACASTS] SSDP search started for:', SERVICE_TYPE)
380
+ cb();
381
+ });
382
+ });
383
+ }).on('error', () => cb());
384
+ });
385
+ };
386
+ ssdp$1.on('response', responseHandler);
387
+ ssdp$1.search(SERVICE_TYPE);
388
+ log('[DLNACASTS] SSDP search started for:', SERVICE_TYPE);
368
389
  setTimeout(() => {
369
- ssdp.removeListener('response', responseHandler)
370
- parallel(tasks, () => {})
371
- }, 10000)
390
+ ssdp$1.removeListener('response', responseHandler);
391
+ parallel(tasks, () => {});
392
+ }, 10000);
372
393
  }
373
- }
394
+ };
374
395
 
375
396
  that.on('removeListener', () => {
376
- if (ssdp && that.listenerCount('update') === 0) {
377
- ssdp.stop()
397
+ if (ssdp$1 && that.listenerCount('update') === 0) {
398
+ ssdp$1.stop();
378
399
  }
379
- })
400
+ });
380
401
 
381
402
  that.destroy = () => {
382
- log('[DLNACASTS] destroying ssdp...')
383
- if (ssdp) {
384
- ssdp.stop()
403
+ log('[DLNACASTS] destroying ssdp...');
404
+ if (ssdp$1) {
405
+ ssdp$1.stop();
385
406
  }
386
- }
407
+ };
387
408
 
388
409
  that.close = () => {
389
- that.removeAllListeners('update')
390
- }
410
+ that.removeAllListeners('update');
411
+ };
391
412
 
392
413
  return that
393
- }
414
+ };
415
+
416
+ module.exports = index;
417
+ //# sourceMappingURL=index.cjs.map