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