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 +9 -2
- package/css/style.css +4 -1
- package/js/player.js +112 -17
- package/js/views.js +9 -1
- package/package.json +1 -1
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**):
|
|
24
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
|
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),
|
|
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
|
|
366
|
-
|
|
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
|
|
384
|
-
|
|
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:
|
|
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
|
|
475
|
+
var byLang = {}, order = [];
|
|
400
476
|
_subTracks.forEach(function (tr) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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,
|