nuvio-tizen 1.4.0 → 1.5.0

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/README.md CHANGED
@@ -20,8 +20,13 @@ core behaviour, targeting 2017 Samsung TVs (Tizen 3.0, ~Chromium 47).
20
20
  - **Stream aggregation** across all installed stream addons, each source showing
21
21
  **size + languages/description** (from `behaviorHints.videoSize` and the addon's own text,
22
22
  e.g. MediaFusion / Torrentio).
23
- - **In-player menu** (press **UP**): *Select source* (switch stream mid-playback, keeping
24
- your position) and *Configure subtitles* Language / Size / Style.
23
+ - **In-player menu** (press **UP**):
24
+ - *Select source* switch stream mid-playback (keeps your position), each source shown
25
+ on two lines (name + size / seeds / languages).
26
+ - *Audio track* — shown only when the stream has more than one audio track
27
+ (AVPlay `getTotalTrackInfo` / `setSelectTrack`).
28
+ - *Configure subtitles* → *Language* (2 levels: language → the tracks within it),
29
+ *Size*, *Style*.
25
30
  - **Subtitles from addons**: fetched per title/episode, SRT + VTT parsed and rendered as a
26
31
  timed overlay; press **UP** during playback to pick a track (or turn them Off).
27
32
  Configurable in Settings: **default language** (auto-loads a matching track),
package/css/style.css CHANGED
@@ -116,8 +116,11 @@ html, body {
116
116
  #sub-menu { position: absolute; right: 60px; bottom: 250px; width: 560px; max-height: 620px; overflow: hidden; z-index: 30; background: rgba(20,25,36,0.98); border-radius: 12px; padding: 18px; }
117
117
  #sub-menu .sm-title { font-size: 24px; font-weight: bold; margin-bottom: 12px; color: #c7cbd4; }
118
118
  #sub-menu .sm-list { max-height: 520px; overflow: hidden; }
119
- #sub-menu .sm-item { padding: 12px 16px; font-size: 22px; border-radius: 8px; border: 3px solid transparent; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
119
+ #sub-menu .sm-item { padding: 12px 16px; border-radius: 8px; border: 3px solid transparent; }
120
+ #sub-menu .sm-item .sm-main { font-size: 22px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
121
+ #sub-menu .sm-item .sm-detail { font-size: 16px; color: #9aa2b1; margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
120
122
  #sub-menu .sm-item.sel { background: #818cf8; color: #0e1117; }
123
+ #sub-menu .sm-item.sel .sm-detail { color: #26304a; }
121
124
 
122
125
  /* --- Search --- */
123
126
  .search-wrap { position: absolute; top: 120px; left: 60px; right: 60px; bottom: 40px; }
package/js/player.js CHANGED
@@ -43,13 +43,16 @@ var Player = (function () {
43
43
  var _osdTimer = null;
44
44
  var _seekPending = 0;
45
45
 
46
+ // audio
47
+ var _audioIdx = null; // currently selected audio track index
48
+
46
49
  // subtitles
47
50
  var _subTracks = []; // [{lang,url,_addon}]
48
51
  var _cues = null; // [{start,end,text}]
49
52
  var _subOn = false;
50
53
  var _subLabel = '';
54
+ var _subTrackUrl = '';
51
55
  var _menuOpen = false;
52
- var _menuIdx = 0;
53
56
 
54
57
  function init() {
55
58
  objEl = U.byId('av-player');
@@ -124,9 +127,10 @@ var Player = (function () {
124
127
  _isLive = (ctx.type === 'tv') || !!ctx.isLive;
125
128
  _ctx.resumeSec = ctx.resumeSec || getResume(ctx.type, ctx.id);
126
129
 
127
- // reset subtitle state
128
- _subTracks = []; _cues = null; _subOn = false; _subLabel = '';
129
- _menuOpen = false; _menuIdx = 0;
130
+ // reset subtitle + audio state
131
+ _subTracks = []; _cues = null; _subOn = false; _subLabel = ''; _subTrackUrl = '';
132
+ _audioIdx = null;
133
+ _menuOpen = false;
130
134
  subsEl.className = 'hidden'; U.clear(subsEl);
131
135
  subMenuEl.className = 'hidden';
132
136
  updateHint();
@@ -192,6 +196,7 @@ var Player = (function () {
192
196
  try { _dur = avplay.getDuration() / 1000; } catch (e3) { _dur = 0; }
193
197
  markLiveFromDuration();
194
198
  updateHint();
199
+ try { var atr = getAudioTracks(); _audioIdx = atr.length ? atr[0].index : null; } catch (eA) {}
195
200
  if (!_isLive && _ctx.resumeSec > 5) { try { avplay.seekTo(_ctx.resumeSec * 1000); } catch (e4) {} }
196
201
  avplay.play();
197
202
  renderOSD();
@@ -309,17 +314,62 @@ var Player = (function () {
309
314
  U.getText(track.url, 15000).then(function (text) {
310
315
  var cues = parseCues(text);
311
316
  if (!cues.length) { U.toast('Could not parse subtitles'); return; }
312
- _cues = cues; _subOn = true; _subLabel = track.lang || 'on';
317
+ _cues = cues; _subOn = true; _subLabel = track.lang || 'on'; _subTrackUrl = track.url;
313
318
  renderSubs();
314
319
  U.toast('Subtitles: ' + (track.lang || 'on'));
315
320
  }, function () { U.toast('Failed to download subtitles'); });
316
321
  }
317
322
 
318
323
  function subsOff() {
319
- _subOn = false; _cues = null; _subLabel = '';
324
+ _subOn = false; _cues = null; _subLabel = ''; _subTrackUrl = '';
320
325
  subsEl.className = 'hidden'; U.clear(subsEl);
321
326
  }
322
327
 
328
+ // ---------- audio tracks ----------
329
+ function getAudioTracks() {
330
+ if (useAV) {
331
+ try {
332
+ var all = avplay.getTotalTrackInfo() || [];
333
+ var out = [];
334
+ for (var i = 0; i < all.length; i++) {
335
+ if (('' + all[i].type).toUpperCase() === 'AUDIO') { out.push(all[i]); }
336
+ }
337
+ return out;
338
+ } catch (e) { return []; }
339
+ }
340
+ try {
341
+ var at = videoEl.audioTracks, arr = [];
342
+ if (at) {
343
+ for (var j = 0; j < at.length; j++) {
344
+ arr.push({ index: j, extra_info: JSON.stringify({ language: at[j].language || at[j].label }) });
345
+ }
346
+ }
347
+ return arr;
348
+ } catch (e2) { return []; }
349
+ }
350
+
351
+ function audioLabel(tr, n) {
352
+ var info = tr.extra_info;
353
+ try { if (typeof info === 'string') { info = JSON.parse(info); } } catch (e) { info = null; }
354
+ var lang = info && (info.language || info.lang);
355
+ var ch = info && (info.channels || info.channel);
356
+ var parts = [];
357
+ parts.push((lang && lang !== 'und' && lang !== '') ? lang : ('Audio ' + (n + 1)));
358
+ if (ch) { parts.push(ch + 'ch'); }
359
+ return parts.join(' · ');
360
+ }
361
+
362
+ function selectAudio(index) {
363
+ if (useAV) {
364
+ try { avplay.setSelectTrack('AUDIO', index); }
365
+ catch (e) { try { avplay.setSelectTrack('AUDIO', '' + index); } catch (e2) {} }
366
+ } else {
367
+ try { var at = videoEl.audioTracks; for (var i = 0; i < at.length; i++) { at[i].enabled = (i === index); } } catch (e3) {}
368
+ }
369
+ _audioIdx = index;
370
+ U.toast('Audio track changed');
371
+ }
372
+
323
373
  function applyStyleNow() {
324
374
  var span = subsEl.firstChild;
325
375
  if (span) { styleSpan(span); }
@@ -362,8 +412,10 @@ var Player = (function () {
362
412
  subMenuEl.appendChild(U.el('div', 'sm-title', m.title));
363
413
  var list = U.el('div', 'sm-list');
364
414
  m.items.forEach(function (it, i) {
365
- var label = it.label + (it.sel ? '' : '');
366
- list.appendChild(U.el('div', 'sm-item' + (i === m.idx ? ' sel' : ''), label));
415
+ var node = U.el('div', 'sm-item' + (i === m.idx ? ' sel' : ''));
416
+ node.appendChild(U.el('div', 'sm-main', it.label + (it.sel ? '' : '')));
417
+ if (it.detail) { node.appendChild(U.el('div', 'sm-detail', it.detail)); }
418
+ list.appendChild(node);
367
419
  });
368
420
  subMenuEl.appendChild(list);
369
421
  var sel = list.children[m.idx];
@@ -375,16 +427,32 @@ var Player = (function () {
375
427
  if (_ctx.streams && _ctx.streams.length) {
376
428
  items.push({ label: 'Select source', run: function () { pushMenu(sourceMenu()); } });
377
429
  }
430
+ if (getAudioTracks().length > 1) {
431
+ items.push({ label: 'Audio track', run: function () { pushMenu(audioMenu()); } });
432
+ }
378
433
  items.push({ label: 'Configure subtitles', run: function () { pushMenu(subConfigMenu()); } });
379
434
  return { title: 'Menu', items: items, idx: 0 };
380
435
  }
381
436
 
382
437
  function sourceMenu() {
383
- var items = _ctx.streams.map(function (s) {
384
- return { label: s.label || 'Source', sel: (s.url === _ctx.url),
438
+ var idx = 0;
439
+ var items = _ctx.streams.map(function (s, i) {
440
+ if (s.url === _ctx.url) { idx = i; }
441
+ return { label: s.label || 'Source', detail: s.detail || '', sel: (s.url === _ctx.url),
385
442
  run: function () { switchSource(s); closeMenu(); } };
386
443
  });
387
- return { title: 'Select source', items: items, idx: 0 };
444
+ return { title: 'Select source', items: items, idx: idx };
445
+ }
446
+
447
+ function audioMenu() {
448
+ var tracks = getAudioTracks();
449
+ var idx = 0;
450
+ var items = tracks.map(function (tr, i) {
451
+ if (tr.index === _audioIdx) { idx = i; }
452
+ return { label: audioLabel(tr, i), sel: (tr.index === _audioIdx),
453
+ run: function () { selectAudio(tr.index); closeMenu(); } };
454
+ });
455
+ return { title: 'Audio track', items: items, idx: idx };
388
456
  }
389
457
 
390
458
  function subConfigMenu() {
@@ -395,16 +463,36 @@ var Player = (function () {
395
463
  ] };
396
464
  }
397
465
 
466
+ // Level 1: Off + one row per language (with option count).
398
467
  function langMenu() {
399
- var items = [{ label: 'Off', sel: !_subOn, run: function () { subsOff(); closeMenu(); } }];
468
+ var byLang = {}, order = [];
400
469
  _subTracks.forEach(function (tr) {
401
- items.push({ label: (tr.lang || 'unknown') + ' · ' + (tr._addon || ''),
402
- sel: (_subOn && _subLabel === (tr.lang || 'on')),
403
- run: function () { loadTrack(tr); closeMenu(); } });
470
+ var l = tr.lang || 'unknown';
471
+ if (!byLang[l]) { byLang[l] = []; order.push(l); }
472
+ byLang[l].push(tr);
473
+ });
474
+ var items = [{ label: 'Off', sel: !_subOn, run: function () { subsOff(); closeMenu(); } }];
475
+ order.forEach(function (l) {
476
+ var n = byLang[l].length;
477
+ items.push({ label: l, detail: n + ' option' + (n > 1 ? 's' : ''),
478
+ sel: (_subOn && _subLabel === l),
479
+ run: function () { pushMenu(langTracksMenu(l, byLang[l])); } });
404
480
  });
405
481
  return { title: 'Subtitle language', items: items, idx: 0 };
406
482
  }
407
483
 
484
+ // Level 2: the individual tracks within one language.
485
+ function langTracksMenu(lang, tracks) {
486
+ var idx = 0;
487
+ var items = tracks.map(function (tr, i) {
488
+ if (_subTrackUrl === tr.url) { idx = i; }
489
+ return { label: (tr._addon || 'Source') + ' · option ' + (i + 1),
490
+ sel: (_subTrackUrl === tr.url),
491
+ run: function () { loadTrack(tr); closeMenu(); } };
492
+ });
493
+ return { title: lang + ' subtitles', items: items, idx: idx };
494
+ }
495
+
408
496
  function optionMenu(title, key, opts) {
409
497
  var cfg = getSubCfg();
410
498
  var idx = 0;
package/js/views.js CHANGED
@@ -360,7 +360,15 @@ var Views = (function () {
360
360
  if (!playable) { U.toast('This source can’t play directly on the TV.'); return; }
361
361
  var sources = _d.streams.filter(Stremio.isPlayable).map(function (st) {
362
362
  var sz = Stremio.streamSize(st);
363
- return { url: st.url, label: (st._addon ? st._addon + ' · ' : '') + Stremio.streamName(st) + (sz ? ' · ' + sz : '') };
363
+ var desc = Stremio.streamDescription(st).replace(/\n/g, ' ');
364
+ var dparts = [];
365
+ if (sz && !/(TB|GB|MB)\b/i.test(desc)) { dparts.push('💾 ' + sz); }
366
+ if (desc) { dparts.push(desc); }
367
+ return {
368
+ url: st.url,
369
+ label: (st._addon ? st._addon + ' · ' : '') + Stremio.streamName(st),
370
+ detail: dparts.join(' ')
371
+ };
364
372
  });
365
373
  Player.play({
366
374
  url: stream.url, type: _d.type, id: _d.streamId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuvio-tizen",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Nuvio-style Stremio-addon streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
5
5
  "packageType": "app",
6
6
  "appName": "Nuvio",