nuvio-tizen 1.4.0 → 1.6.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,15 @@ 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
+ - *Restart from beginning*, *Skip back 10 minutes*, *Skip forward 10 minutes*
29
+ (hidden for live streams).
30
+ - *Configure subtitles* → *Language* (2 levels: language → the tracks within it),
31
+ *Size*, *Style*.
25
32
  - **Subtitles from addons**: fetched per title/episode, SRT + VTT parsed and rendered as a
26
33
  timed overlay; press **UP** during playback to pick a track (or turn them Off).
27
34
  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();
@@ -249,9 +254,9 @@ var Player = (function () {
249
254
  showOSD(false);
250
255
  }
251
256
 
252
- function seekBy(delta) {
257
+ function seekTo(sec) {
253
258
  if (_isLive) { showOSD(false); U.toast('Live — seeking unavailable', 1500); return; }
254
- var target = Math.max(0, Math.min((_dur || 1e9), _cur + delta));
259
+ var target = Math.max(0, Math.min((_dur || 1e9), sec));
255
260
  _cur = target;
256
261
  renderOSD();
257
262
  showOSD(false);
@@ -262,6 +267,8 @@ var Player = (function () {
262
267
  }
263
268
  }
264
269
 
270
+ function seekBy(delta) { seekTo(_cur + delta); }
271
+
265
272
  // ---------- subtitles ----------
266
273
  // Parses SRT or VTT (both use HH:MM:SS,mmm / HH:MM:SS.mmm timing lines).
267
274
  function parseCues(text) {
@@ -309,17 +316,62 @@ var Player = (function () {
309
316
  U.getText(track.url, 15000).then(function (text) {
310
317
  var cues = parseCues(text);
311
318
  if (!cues.length) { U.toast('Could not parse subtitles'); return; }
312
- _cues = cues; _subOn = true; _subLabel = track.lang || 'on';
319
+ _cues = cues; _subOn = true; _subLabel = track.lang || 'on'; _subTrackUrl = track.url;
313
320
  renderSubs();
314
321
  U.toast('Subtitles: ' + (track.lang || 'on'));
315
322
  }, function () { U.toast('Failed to download subtitles'); });
316
323
  }
317
324
 
318
325
  function subsOff() {
319
- _subOn = false; _cues = null; _subLabel = '';
326
+ _subOn = false; _cues = null; _subLabel = ''; _subTrackUrl = '';
320
327
  subsEl.className = 'hidden'; U.clear(subsEl);
321
328
  }
322
329
 
330
+ // ---------- audio tracks ----------
331
+ function getAudioTracks() {
332
+ if (useAV) {
333
+ try {
334
+ var all = avplay.getTotalTrackInfo() || [];
335
+ var out = [];
336
+ for (var i = 0; i < all.length; i++) {
337
+ if (('' + all[i].type).toUpperCase() === 'AUDIO') { out.push(all[i]); }
338
+ }
339
+ return out;
340
+ } catch (e) { return []; }
341
+ }
342
+ try {
343
+ var at = videoEl.audioTracks, arr = [];
344
+ if (at) {
345
+ for (var j = 0; j < at.length; j++) {
346
+ arr.push({ index: j, extra_info: JSON.stringify({ language: at[j].language || at[j].label }) });
347
+ }
348
+ }
349
+ return arr;
350
+ } catch (e2) { return []; }
351
+ }
352
+
353
+ function audioLabel(tr, n) {
354
+ var info = tr.extra_info;
355
+ try { if (typeof info === 'string') { info = JSON.parse(info); } } catch (e) { info = null; }
356
+ var lang = info && (info.language || info.lang);
357
+ var ch = info && (info.channels || info.channel);
358
+ var parts = [];
359
+ parts.push((lang && lang !== 'und' && lang !== '') ? lang : ('Audio ' + (n + 1)));
360
+ if (ch) { parts.push(ch + 'ch'); }
361
+ return parts.join(' · ');
362
+ }
363
+
364
+ function selectAudio(index) {
365
+ if (useAV) {
366
+ try { avplay.setSelectTrack('AUDIO', index); }
367
+ catch (e) { try { avplay.setSelectTrack('AUDIO', '' + index); } catch (e2) {} }
368
+ } else {
369
+ try { var at = videoEl.audioTracks; for (var i = 0; i < at.length; i++) { at[i].enabled = (i === index); } } catch (e3) {}
370
+ }
371
+ _audioIdx = index;
372
+ U.toast('Audio track changed');
373
+ }
374
+
323
375
  function applyStyleNow() {
324
376
  var span = subsEl.firstChild;
325
377
  if (span) { styleSpan(span); }
@@ -362,8 +414,10 @@ var Player = (function () {
362
414
  subMenuEl.appendChild(U.el('div', 'sm-title', m.title));
363
415
  var list = U.el('div', 'sm-list');
364
416
  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));
417
+ var node = U.el('div', 'sm-item' + (i === m.idx ? ' sel' : ''));
418
+ node.appendChild(U.el('div', 'sm-main', it.label + (it.sel ? '' : '')));
419
+ if (it.detail) { node.appendChild(U.el('div', 'sm-detail', it.detail)); }
420
+ list.appendChild(node);
367
421
  });
368
422
  subMenuEl.appendChild(list);
369
423
  var sel = list.children[m.idx];
@@ -375,16 +429,37 @@ var Player = (function () {
375
429
  if (_ctx.streams && _ctx.streams.length) {
376
430
  items.push({ label: 'Select source', run: function () { pushMenu(sourceMenu()); } });
377
431
  }
432
+ if (getAudioTracks().length > 1) {
433
+ items.push({ label: 'Audio track', run: function () { pushMenu(audioMenu()); } });
434
+ }
435
+ if (!_isLive) {
436
+ items.push({ label: 'Restart from beginning', run: function () { seekTo(0); closeMenu(); } });
437
+ items.push({ label: 'Skip back 10 minutes', run: function () { seekBy(-600); closeMenu(); } });
438
+ items.push({ label: 'Skip forward 10 minutes', run: function () { seekBy(600); closeMenu(); } });
439
+ }
378
440
  items.push({ label: 'Configure subtitles', run: function () { pushMenu(subConfigMenu()); } });
379
441
  return { title: 'Menu', items: items, idx: 0 };
380
442
  }
381
443
 
382
444
  function sourceMenu() {
383
- var items = _ctx.streams.map(function (s) {
384
- return { label: s.label || 'Source', sel: (s.url === _ctx.url),
445
+ var idx = 0;
446
+ var items = _ctx.streams.map(function (s, i) {
447
+ if (s.url === _ctx.url) { idx = i; }
448
+ return { label: s.label || 'Source', detail: s.detail || '', sel: (s.url === _ctx.url),
385
449
  run: function () { switchSource(s); closeMenu(); } };
386
450
  });
387
- return { title: 'Select source', items: items, idx: 0 };
451
+ return { title: 'Select source', items: items, idx: idx };
452
+ }
453
+
454
+ function audioMenu() {
455
+ var tracks = getAudioTracks();
456
+ var idx = 0;
457
+ var items = tracks.map(function (tr, i) {
458
+ if (tr.index === _audioIdx) { idx = i; }
459
+ return { label: audioLabel(tr, i), sel: (tr.index === _audioIdx),
460
+ run: function () { selectAudio(tr.index); closeMenu(); } };
461
+ });
462
+ return { title: 'Audio track', items: items, idx: idx };
388
463
  }
389
464
 
390
465
  function subConfigMenu() {
@@ -395,16 +470,36 @@ var Player = (function () {
395
470
  ] };
396
471
  }
397
472
 
473
+ // Level 1: Off + one row per language (with option count).
398
474
  function langMenu() {
399
- var items = [{ label: 'Off', sel: !_subOn, run: function () { subsOff(); closeMenu(); } }];
475
+ var byLang = {}, order = [];
400
476
  _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(); } });
477
+ var l = tr.lang || 'unknown';
478
+ if (!byLang[l]) { byLang[l] = []; order.push(l); }
479
+ byLang[l].push(tr);
480
+ });
481
+ var items = [{ label: 'Off', sel: !_subOn, run: function () { subsOff(); closeMenu(); } }];
482
+ order.forEach(function (l) {
483
+ var n = byLang[l].length;
484
+ items.push({ label: l, detail: n + ' option' + (n > 1 ? 's' : ''),
485
+ sel: (_subOn && _subLabel === l),
486
+ run: function () { pushMenu(langTracksMenu(l, byLang[l])); } });
404
487
  });
405
488
  return { title: 'Subtitle language', items: items, idx: 0 };
406
489
  }
407
490
 
491
+ // Level 2: the individual tracks within one language.
492
+ function langTracksMenu(lang, tracks) {
493
+ var idx = 0;
494
+ var items = tracks.map(function (tr, i) {
495
+ if (_subTrackUrl === tr.url) { idx = i; }
496
+ return { label: (tr._addon || 'Source') + ' · option ' + (i + 1),
497
+ sel: (_subTrackUrl === tr.url),
498
+ run: function () { loadTrack(tr); closeMenu(); } };
499
+ });
500
+ return { title: lang + ' subtitles', items: items, idx: idx };
501
+ }
502
+
408
503
  function optionMenu(title, key, opts) {
409
504
  var cfg = getSubCfg();
410
505
  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.6.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",