Add Samsung Tizen TV web client.
Focus-friendly VMC shell for Tizen with live TV and playback against the master API. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
252bce838b
commit
a0311c031e
14 changed files with 4168 additions and 0 deletions
12
apps/tizen-tv/README.md
Normal file
12
apps/tizen-tv/README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Media Cluster — Samsung Tizen 1.2.0
|
||||
|
||||
Cinematic UI (Netflix-achtig), Tizen-veilige CSS. IPTV werkt hetzelfde, wel visueel strakker.
|
||||
|
||||
## Install
|
||||
|
||||
`apps/android-tv/dist/vonas-mediacluster-tizen-1.2.0.wgt` via Apps2Samsung → Custom WGT.
|
||||
|
||||
```powershell
|
||||
cd apps/tizen-tv
|
||||
.\package.ps1
|
||||
```
|
||||
79
apps/tizen-tv/TIZEN-CONSTRAINTS.md
Normal file
79
apps/tizen-tv/TIZEN-CONSTRAINTS.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Samsung Tizen — bouwconstraints (officiële docs)
|
||||
|
||||
Bronnen (raadpleeg bij elke grotere change):
|
||||
|
||||
- [Web Engine Specifications](https://developer.samsung.com/smarttv/develop/specifications/web-engine-specifications.html)
|
||||
- [General Specifications](https://developer.samsung.com/smarttv/develop/specifications/general-specifications.html)
|
||||
- [AVPlay playback](https://developer.samsung.com/smarttv/develop/guides/multimedia/media-playback/using-avplay.html)
|
||||
- [W3C/HTML5 API References](https://developer.samsung.com/smarttv/develop/api-references/w3chtml5-api-references.html)
|
||||
- [Web API References](https://developer.samsung.com/smarttv/develop/api-references/web-api-references.html)
|
||||
- [TV Device setup / Developer Mode](https://developer.samsung.com/smarttv/develop/getting-started/using-sdk/tv-device.html)
|
||||
|
||||
## Doelplatform
|
||||
|
||||
We mikken op **Tizen 5.5+ (2020, Chromium ~M69)** als ondergrens, met voorkeurstest op nieuwere sets.
|
||||
TV-webengine wordt **niet** geüpdatet na aankoopjaar — feature-matrix per jaargang geldt.
|
||||
|
||||
| Jaar | Tizen | Chromium (indicatief) |
|
||||
|------|-------|------------------------|
|
||||
| 2020 | 5.5 | M69 |
|
||||
| 2021 | 6.0 | M76 |
|
||||
| 2022 | 6.5 | M85 |
|
||||
| 2023 | 7.0 | M94 |
|
||||
| 2024 | 8.0 | M108 |
|
||||
|
||||
## CSS — vermijden tot bewezen op target
|
||||
|
||||
Volgens Samsung Web Engine Specs (check matrix vóór gebruik):
|
||||
|
||||
| Feature | Let op |
|
||||
|---------|--------|
|
||||
| `inset` | **Nee** op oudere engines (o.a. tot ~Tizen 6.x/7.x in matrix als No) → gebruik `top/right/bottom/left` |
|
||||
| `gap` (flex) | Beperkt / pas later breed → liever margins |
|
||||
| CSS Grid | Pas later volledig → liever absolute/float/inline-block/flex zonder gap |
|
||||
| `aspect-ratio`, `dvh`, `:is()`, `:where()`, `:focus-visible` | Modern — niet aannemen |
|
||||
| `object-fit` | Ja vanaf Tizen 3+ volgens matrix; poster-covers toch liever `background-size: cover` in vast kader + `overflow: hidden` |
|
||||
|
||||
Veilig: flexbox (zonder gap), transforms, transitions (basis), `border-radius`, `box-shadow`, absolute layouts, `background-size/position`.
|
||||
|
||||
## JavaScript
|
||||
|
||||
- ES5-stijl is het veiligst voor breed bereik; ES6 (Promise, arrow, let/const) checken in de Samsung ES6-tabel voor jouw minimumjaar.
|
||||
- **Geen** aanname van Node/browser globals: altijd `window` of IIFE `(function (global) { ... })(window)`.
|
||||
- `fetch`: polyfill/XHR achter de hand houden voor oudere engines.
|
||||
- Geen emoji als enige UI-signaal (font/glyph-support wisselt).
|
||||
|
||||
## AVPlay (productie-playback)
|
||||
|
||||
Volgens Samsung AVPlay guide:
|
||||
|
||||
1. Privilege: `http://developer.samsung.com/privilege/avplay` in `config.xml`
|
||||
2. Script: `$WEBAPIS/webapis/webapis.js` (lazy laden mag)
|
||||
3. State machine: `open` → `setDisplayRect` / listener / DRM → `prepare`/`prepareAsync` → `play`
|
||||
4. `prepare` niet op UI-thread blokkeren → **`prepareAsync`**
|
||||
5. Object `type="application/avplayer"` alleen tonen tijdens playback (anders fullscreen zwart vlak)
|
||||
6. Netwerk: `access origin` + internet privilege; HTTPS/certificaten/host bereikbaarheid checken bij `PLAYER_ERROR_CONNECT_FAILED`
|
||||
7. HTML5 `<video>` als fallback voor progressive streams mag, AVPlay blijft primaire TV-path
|
||||
|
||||
## config.xml checklist
|
||||
|
||||
- `tizen:profile name="tv-samsung"`
|
||||
- `required_version` realistisch (2.3/2.4 ok voor breed)
|
||||
- Privileges: `internet`, `tv.inputdevice`, `avplay`, `network.public`
|
||||
- `<access origin="*" subdomains="true"/>` of strakkere origins
|
||||
- `.wgt` = zip met **forward-slash** paden, `config.xml` in root
|
||||
|
||||
## Werkwijze (verplicht bij Tizen-wijzigingen)
|
||||
|
||||
1. Feature opzoeken in **Web Engine Specifications** voor minimum Tizen-jaar.
|
||||
2. Bij playback: AVPlay-guide + state table volgen.
|
||||
3. Op echte TV (Apps2Samsung) valideren — emulator ≠ hardware.
|
||||
4. Bij twijfel: polyfill of oudere syntax, geen “moderne default”.
|
||||
|
||||
## Bekende valkuilen (dit project)
|
||||
|
||||
- `inset: 0` → leeg/zwart scherm (containers 0×0)
|
||||
- AVPlay `<object>` in DOM terwijl UI “hidden” → zwart overlay
|
||||
- `global.McApp` zonder `global`-parameter → script stopt, `McApp=undefined`
|
||||
- `PLAYER_ERROR_CONNECT_FAILED` → vaak node/HTTPS/netwerk, niet alleen player-API
|
||||
- `Failed to register session on node` → master↔node websocket ACK, niet Tizen-UI
|
||||
36
apps/tizen-tv/package.ps1
Normal file
36
apps/tizen-tv/package.ps1
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Bouwt een .wgt voor Apps2Samsung sideload (ZIP met forward-slash paden).
|
||||
$ErrorActionPreference = "Stop"
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
||||
$root = $PSScriptRoot
|
||||
$www = Resolve-Path (Join-Path $root "www")
|
||||
$config = Join-Path $www "config.xml"
|
||||
if (-not (Test-Path $config)) { throw "config.xml ontbreekt in www/" }
|
||||
|
||||
[xml]$xml = Get-Content $config
|
||||
$version = $xml.widget.version
|
||||
if (-not $version) { $version = "1.0.0" }
|
||||
|
||||
$dist = Join-Path $root "..\android-tv\dist"
|
||||
New-Item -ItemType Directory -Force -Path $dist | Out-Null
|
||||
$outPath = Join-Path $dist "vonas-mediacluster-tizen-$version.wgt"
|
||||
if (Test-Path $outPath) { Remove-Item $outPath -Force }
|
||||
|
||||
$fs = [IO.File]::Open($outPath, [IO.FileMode]::Create)
|
||||
$zip = New-Object IO.Compression.ZipArchive($fs, [IO.Compression.ZipArchiveMode]::Create)
|
||||
try {
|
||||
Get-ChildItem $www -Recurse -File | ForEach-Object {
|
||||
$rel = $_.FullName.Substring($www.Path.Length).TrimStart("\").Replace("\", "/")
|
||||
[IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
|
||||
$zip, $_.FullName, $rel, [IO.Compression.CompressionLevel]::Optimal
|
||||
) | Out-Null
|
||||
}
|
||||
} finally {
|
||||
$zip.Dispose()
|
||||
$fs.Dispose()
|
||||
}
|
||||
|
||||
$size = [math]::Round((Get-Item $outPath).Length / 1KB, 1)
|
||||
Write-Host "OK: $outPath ($size KB)"
|
||||
Write-Host "Installeer via Apps2Samsung → Custom WGT/TPK."
|
||||
16
apps/tizen-tv/www/config.xml
Normal file
16
apps/tizen-tv/www/config.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<widget xmlns:tizen="http://tizen.org/ns/widgets" xmlns="http://www.w3.org/ns/widgets" id="http://vonas.nl/mediacluster" version="1.3.6" viewmodes="maximized">
|
||||
<tizen:application id="VonasMC001.MediaCluster" package="VonasMC001" required_version="2.3"/>
|
||||
<content src="index.html"/>
|
||||
<feature name="http://tizen.org/feature/screen.size.normal.1080.1920"/>
|
||||
<icon src="icon.png"/>
|
||||
<name>VMC</name>
|
||||
<tizen:profile name="tv-samsung"/>
|
||||
<tizen:privilege name="http://tizen.org/privilege/internet"/>
|
||||
<tizen:privilege name="http://tizen.org/privilege/tv.inputdevice"/>
|
||||
<tizen:privilege name="http://developer.samsung.com/privilege/avplay"/>
|
||||
<tizen:privilege name="http://developer.samsung.com/privilege/network.public"/>
|
||||
<access origin="*" subdomains="true"></access>
|
||||
<tizen:metadata key="http://samsung.com/tv/metadata/devel.api.version" value="2.4"/>
|
||||
<tizen:setting screen-orientation="landscape" context-menu="disable" background-support="disable" encryption="disable" install-location="auto" hwkey-event="enable"/>
|
||||
</widget>
|
||||
1078
apps/tizen-tv/www/css/app.css
Normal file
1078
apps/tizen-tv/www/css/app.css
Normal file
File diff suppressed because it is too large
Load diff
BIN
apps/tizen-tv/www/icon.png
Normal file
BIN
apps/tizen-tv/www/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/tizen-tv/www/img/icon.png
Normal file
BIN
apps/tizen-tv/www/img/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
149
apps/tizen-tv/www/index.html
Normal file
149
apps/tizen-tv/www/index.html
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080, initial-scale=1.0, user-scalable=no" />
|
||||
<title>VMC</title>
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<style type="text/css">
|
||||
html, body { margin:0; width:100%; height:100%; background:#0B0B0B; color:#fff; font-family:Arial,sans-serif; }
|
||||
#boot-banner {
|
||||
position:absolute; top:0; left:0; right:0; bottom:0;
|
||||
background:#0B0B0B; color:#E50914; font-size:48px; font-weight:900;
|
||||
text-align:center; padding-top:38%; z-index:9999; letter-spacing:0.08em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;background:#0B0B0B;color:#fff;">
|
||||
<div id="boot-banner">VMC</div>
|
||||
|
||||
<div id="app">
|
||||
<section id="screen-pair" class="screen">
|
||||
<div class="pair-card">
|
||||
<div class="brand">VMC</div>
|
||||
<h1>Koppel deze TV</h1>
|
||||
<p class="muted">Open de link op je telefoon of computer en vul de code in.</p>
|
||||
<div id="pair-code" class="pair-code">····</div>
|
||||
<p id="pair-link" class="pair-link"></p>
|
||||
<p id="pair-status" class="muted">Bezig met starten…</p>
|
||||
<button class="btn focusable" id="pair-refresh">Nieuwe code</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="screen-profiles" class="screen hidden">
|
||||
<div class="profiles-wrap">
|
||||
<h1>Wie kijkt er?</h1>
|
||||
<p class="muted">Kies een profiel</p>
|
||||
<div id="profile-grid" class="profile-grid"></div>
|
||||
<div id="pin-panel" class="pin-panel hidden">
|
||||
<h2 id="pin-title">PIN</h2>
|
||||
<div id="pin-dots" class="pin-dots"></div>
|
||||
<div class="numpad" id="pin-pad"></div>
|
||||
<button class="btn focusable" id="pin-cancel">Annuleer</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="screen-main" class="screen hidden">
|
||||
<nav id="topnav" class="topnav overlay">
|
||||
<div class="nav-brand">VMC</div>
|
||||
<button class="nav-tab focusable active" data-tab="home">Home<span class="nav-underline"></span></button>
|
||||
<button class="nav-tab focusable" data-tab="movies">Films<span class="nav-underline"></span></button>
|
||||
<button class="nav-tab focusable" data-tab="series">Series<span class="nav-underline"></span></button>
|
||||
<button class="nav-tab focusable hidden" data-tab="tv" id="nav-tv">TV<span class="nav-underline"></span></button>
|
||||
<button class="nav-tab focusable" data-tab="search">Zoeken<span class="nav-underline"></span></button>
|
||||
<button class="nav-tab focusable" data-tab="settings">Instellingen<span class="nav-underline"></span></button>
|
||||
<button class="nav-profile focusable" id="nav-profile" title="Profiel wisselen"></button>
|
||||
</nav>
|
||||
<div id="main-body" class="main-body home-mode">
|
||||
<div id="tab-home" class="tab-pane"></div>
|
||||
<div id="tab-movies" class="tab-pane hidden"></div>
|
||||
<div id="tab-series" class="tab-pane hidden"></div>
|
||||
<div id="tab-tv" class="tab-pane hidden"></div>
|
||||
<div id="tab-search" class="tab-pane hidden"></div>
|
||||
<div id="tab-settings" class="tab-pane hidden"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="screen-detail" class="screen hidden">
|
||||
<div id="detail-bg" class="detail-bg"></div>
|
||||
<div class="detail-shade"></div>
|
||||
<div class="detail-body">
|
||||
<button class="btn focusable" id="detail-back">Terug</button>
|
||||
<div id="detail-logo" class="hero-logo hidden"></div>
|
||||
<h1 id="detail-title"></h1>
|
||||
<div id="detail-meta" class="hero-meta"></div>
|
||||
<p id="detail-overview" class="detail-overview"></p>
|
||||
<div id="detail-actions" class="hero-actions"></div>
|
||||
<div id="detail-episodes" class="episodes"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="screen-player" class="screen hidden">
|
||||
<div id="av-object-wrap" class="av-wrap"></div>
|
||||
<video id="html-player" class="html-player hidden"></video>
|
||||
<div id="player-subs" class="player-subs hidden"></div>
|
||||
<div id="player-ui" class="player-ui">
|
||||
<div class="player-chrome">
|
||||
<div id="player-title" class="player-title"></div>
|
||||
<div class="player-bar">
|
||||
<div id="player-progress" class="player-progress"><div id="player-fill"></div></div>
|
||||
<div class="player-time-row">
|
||||
<span id="player-time" class="player-time"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-controls" id="player-controls">
|
||||
<button type="button" class="pctrl focusable" id="btn-play" title="Pauze">Pauze</button>
|
||||
<button type="button" class="pctrl focusable" id="btn-rew" title="10s terug">-10s</button>
|
||||
<button type="button" class="pctrl focusable" id="btn-ff" title="10s vooruit">+10s</button>
|
||||
<span class="pctrl-spacer"></span>
|
||||
<div class="player-actions" id="player-actions">
|
||||
<button type="button" class="pctrl text focusable" id="btn-audio">Audio</button>
|
||||
<button type="button" class="pctrl text focusable" id="btn-subs">Ondertiteling</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="player-menu" class="player-menu hidden">
|
||||
<div class="player-menu-panel">
|
||||
<div id="player-menu-title" class="player-menu-title">Audio</div>
|
||||
<div id="player-menu-list" class="player-menu-list"></div>
|
||||
<button type="button" class="btn focusable" id="player-menu-close">Sluiten</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="player-error" class="player-error hidden"></div>
|
||||
</section>
|
||||
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
<div id="loading" class="loading hidden"><div class="spinner"></div></div>
|
||||
<div id="fatal" class="fatal hidden"></div>
|
||||
</div>
|
||||
|
||||
<script src="js/polyfill.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/player.js"></script>
|
||||
<script src="js/focus.js"></script>
|
||||
<script src="js/live.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script>
|
||||
setTimeout(function () {
|
||||
var b = document.getElementById("boot-banner");
|
||||
if (!b) return;
|
||||
if (b.style.display === "none" || b.style.visibility === "hidden") return;
|
||||
b.style.background = "#12080a";
|
||||
b.style.color = "#ffffff";
|
||||
b.style.paddingTop = "28%";
|
||||
b.style.fontSize = "22px";
|
||||
b.style.letterSpacing = "0";
|
||||
b.innerHTML =
|
||||
"App start niet automatisch.<br/>McApi=" +
|
||||
(typeof window.McApi) +
|
||||
" McApp=" +
|
||||
(typeof window.McApp) +
|
||||
" McLive=" +
|
||||
(typeof window.McLive) +
|
||||
"<br/>Herinstalleer of stuur deze tekst door.";
|
||||
}, 2500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
163
apps/tizen-tv/www/js/api.js
Normal file
163
apps/tizen-tv/www/js/api.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
(function (global) {
|
||||
var BASE = "https://master.vonas.nl";
|
||||
var TOKEN_KEY = "mc_refresh_token";
|
||||
var PROFILE_KEY = "mc_profile";
|
||||
var FAV_KEY = "mc_iptv_favs";
|
||||
|
||||
function store() {
|
||||
try {
|
||||
return localStorage;
|
||||
} catch (e) {
|
||||
return {
|
||||
_d: {},
|
||||
getItem: function (k) { return this._d[k] || null; },
|
||||
setItem: function (k, v) { this._d[k] = String(v); },
|
||||
removeItem: function (k) { delete this._d[k]; },
|
||||
};
|
||||
}
|
||||
}
|
||||
var ls = store();
|
||||
|
||||
function getToken() { return ls.getItem(TOKEN_KEY); }
|
||||
function setToken(token) {
|
||||
if (token) ls.setItem(TOKEN_KEY, token);
|
||||
else ls.removeItem(TOKEN_KEY);
|
||||
}
|
||||
function getProfileCache() {
|
||||
try { return JSON.parse(ls.getItem(PROFILE_KEY) || "null"); } catch (e) { return null; }
|
||||
}
|
||||
function setProfileCache(profile) {
|
||||
if (profile) ls.setItem(PROFILE_KEY, JSON.stringify(profile));
|
||||
else ls.removeItem(PROFILE_KEY);
|
||||
}
|
||||
function getFavs() {
|
||||
try { return JSON.parse(ls.getItem(FAV_KEY) || "[]"); } catch (e) { return []; }
|
||||
}
|
||||
function setFavs(ids) { ls.setItem(FAV_KEY, JSON.stringify(ids || [])); }
|
||||
function toggleFav(streamId) {
|
||||
var id = String(streamId);
|
||||
var favs = getFavs();
|
||||
var i = favs.indexOf(id);
|
||||
if (i >= 0) favs.splice(i, 1);
|
||||
else favs.push(id);
|
||||
setFavs(favs);
|
||||
return favs.indexOf(id) >= 0;
|
||||
}
|
||||
function isFav(streamId) { return getFavs().indexOf(String(streamId)) >= 0; }
|
||||
|
||||
function errMessage(body, status) {
|
||||
if (body && body.error && body.error.message) return body.error.message;
|
||||
if (body && body.error && body.error.code) return body.error.code;
|
||||
return "HTTP " + status;
|
||||
}
|
||||
|
||||
function request(path, opts) {
|
||||
opts = opts || {};
|
||||
var headers = opts.headers || {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
var token = getToken();
|
||||
if (token && !opts.noAuth) headers.Authorization = "Bearer " + token;
|
||||
return fetch(BASE + path, {
|
||||
method: opts.method || "GET",
|
||||
headers: headers,
|
||||
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
||||
}).then(function (res) {
|
||||
return res.text().then(function (text) {
|
||||
var body = null;
|
||||
try { body = text ? JSON.parse(text) : null; } catch (e) { body = { raw: text }; }
|
||||
if (!res.ok) {
|
||||
var err = new Error(errMessage(body, res.status));
|
||||
err.status = res.status;
|
||||
err.code = body && body.error && body.error.code;
|
||||
err.body = body;
|
||||
throw err;
|
||||
}
|
||||
return body;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
global.McApi = {
|
||||
BASE: BASE,
|
||||
getToken: getToken,
|
||||
setToken: setToken,
|
||||
getProfileCache: getProfileCache,
|
||||
setProfileCache: setProfileCache,
|
||||
getFavs: getFavs,
|
||||
toggleFav: toggleFav,
|
||||
isFav: isFav,
|
||||
clearSession: function () { setToken(null); setProfileCache(null); },
|
||||
deviceCode: function () {
|
||||
return request("/api/v1/client/device/code", {
|
||||
method: "POST", noAuth: true,
|
||||
body: { deviceName: "Samsung Tizen", platform: "tizen" },
|
||||
});
|
||||
},
|
||||
devicePoll: function (code) {
|
||||
return request("/api/v1/client/device/poll?code=" + encodeURIComponent(code), { noAuth: true });
|
||||
},
|
||||
me: function () { return request("/api/v1/client/me"); },
|
||||
listProfiles: function () { return request("/api/v1/client/profiles"); },
|
||||
selectProfile: function (id, pin) {
|
||||
var body = {};
|
||||
if (pin) body.pin = pin;
|
||||
return request("/api/v1/client/profiles/" + encodeURIComponent(id) + "/select", { method: "POST", body: body });
|
||||
},
|
||||
clearProfile: function () {
|
||||
return request("/api/v1/client/profiles/clear", { method: "POST", body: {} });
|
||||
},
|
||||
home: function () { return request("/api/v1/client/home"); },
|
||||
search: function (q) {
|
||||
return request("/api/v1/client/search?q=" + encodeURIComponent(q) + "&take=60");
|
||||
},
|
||||
catalog: function (id, skip, take) {
|
||||
skip = skip || 0; take = take || 60;
|
||||
return request(
|
||||
"/api/v1/client/catalog/" + encodeURIComponent(id) +
|
||||
"?skip=" + skip + "&take=" + take
|
||||
);
|
||||
},
|
||||
movie: function (id) { return request("/api/v1/client/movies/" + encodeURIComponent(id)); },
|
||||
series: function (id) { return request("/api/v1/client/series/" + encodeURIComponent(id)); },
|
||||
play: function (mediaFileId) {
|
||||
return request("/api/v1/client/play", { method: "POST", body: { mediaFileId: mediaFileId } });
|
||||
},
|
||||
progress: function (payload) {
|
||||
return request("/api/v1/client/progress", { method: "POST", body: payload });
|
||||
},
|
||||
dismissContinue: function (progressId) {
|
||||
return request("/api/v1/client/progress/" + encodeURIComponent(progressId) + "/dismiss", {
|
||||
method: "POST", body: {},
|
||||
});
|
||||
},
|
||||
iptvCategories: function () { return request("/api/v1/client/iptv/categories"); },
|
||||
iptvChannels: function (categoryId, includeEpg) {
|
||||
var params = [];
|
||||
if (categoryId) params.push("categoryId=" + encodeURIComponent(categoryId));
|
||||
if (includeEpg) params.push("epg=1");
|
||||
var q = params.length ? "?" + params.join("&") : "";
|
||||
return request("/api/v1/client/iptv/channels" + q);
|
||||
},
|
||||
iptvPlay: function (streamId) {
|
||||
return request("/api/v1/client/iptv/channels/" + streamId + "/play");
|
||||
},
|
||||
iptvEpg: function (streamId) {
|
||||
return request("/api/v1/client/iptv/channels/" + streamId + "/epg");
|
||||
},
|
||||
searchSubtitles: function (mediaFileId, languages) {
|
||||
languages = languages || "nl,en";
|
||||
return request(
|
||||
"/api/v1/client/subtitles/search?mediaFileId=" +
|
||||
encodeURIComponent(mediaFileId) +
|
||||
"&languages=" +
|
||||
encodeURIComponent(languages)
|
||||
);
|
||||
},
|
||||
fetchSubtitle: function (fileId, source) {
|
||||
return request("/api/v1/client/subtitles/fetch", {
|
||||
method: "POST",
|
||||
body: { fileId: fileId, source: source || "org" },
|
||||
});
|
||||
},
|
||||
};
|
||||
})(window);
|
||||
1346
apps/tizen-tv/www/js/app.js
Normal file
1346
apps/tizen-tv/www/js/app.js
Normal file
File diff suppressed because it is too large
Load diff
100
apps/tizen-tv/www/js/focus.js
Normal file
100
apps/tizen-tv/www/js/focus.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
(function (global) {
|
||||
var keys = {
|
||||
LEFT: 37,
|
||||
UP: 38,
|
||||
RIGHT: 39,
|
||||
DOWN: 40,
|
||||
ENTER: 13,
|
||||
BACK: 10009,
|
||||
ESC: 27,
|
||||
PLAY: 415,
|
||||
PAUSE: 19,
|
||||
PLAYPAUSE: 10252,
|
||||
FF: 417,
|
||||
REW: 412,
|
||||
STOP: 413,
|
||||
};
|
||||
|
||||
function visibleFocusables(root) {
|
||||
root = root || document;
|
||||
var nodes = root.querySelectorAll(".focusable, .tile, .profile-tile, .ep, .num-key, .btn");
|
||||
var out = [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var el = nodes[i];
|
||||
if (el.disabled) continue;
|
||||
if (el.classList.contains("hidden")) continue;
|
||||
if (el.closest && el.closest(".hidden")) continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 2 || r.height < 2) continue;
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function center(el) {
|
||||
var r = el.getBoundingClientRect();
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2, r: r };
|
||||
}
|
||||
|
||||
function move(direction) {
|
||||
var active = document.activeElement;
|
||||
var list = visibleFocusables();
|
||||
if (!list.length) return;
|
||||
if (!active || list.indexOf(active) < 0) {
|
||||
list[0].focus();
|
||||
return;
|
||||
}
|
||||
var a = center(active);
|
||||
var best = null;
|
||||
var bestScore = Infinity;
|
||||
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var el = list[i];
|
||||
if (el === active) continue;
|
||||
var b = center(el);
|
||||
var dx = b.x - a.x;
|
||||
var dy = b.y - a.y;
|
||||
var ok = false;
|
||||
if (direction === "left") ok = dx < -10 && Math.abs(dy) < Math.abs(dx) * 1.6;
|
||||
if (direction === "right") ok = dx > 10 && Math.abs(dy) < Math.abs(dx) * 1.6;
|
||||
if (direction === "up") ok = dy < -10 && Math.abs(dx) < Math.abs(dy) * 1.8;
|
||||
if (direction === "down") ok = dy > 10 && Math.abs(dx) < Math.abs(dy) * 1.8;
|
||||
if (!ok) continue;
|
||||
var score = Math.abs(dx) + Math.abs(dy) * 1.2;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
best = el;
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
best.focus();
|
||||
if (best.scrollIntoView) best.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
function registerKeys() {
|
||||
try {
|
||||
if (global.tizen && tizen.tvinputdevice) {
|
||||
var supported = tizen.tvinputdevice.getSupportedKeys() || [];
|
||||
var names = ["MediaPlay", "MediaPause", "MediaPlayPause", "MediaFastForward", "MediaRewind", "MediaStop"];
|
||||
for (var i = 0; i < supported.length; i++) {
|
||||
if (names.indexOf(supported[i].name) >= 0) {
|
||||
try {
|
||||
tizen.tvinputdevice.registerKey(supported[i].name);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
global.McFocus = {
|
||||
keys: keys,
|
||||
move: move,
|
||||
focusFirst: function (root) {
|
||||
var list = visibleFocusables(root);
|
||||
if (list[0]) list[0].focus();
|
||||
},
|
||||
registerKeys: registerKeys,
|
||||
};
|
||||
})(window);
|
||||
366
apps/tizen-tv/www/js/live.js
Normal file
366
apps/tizen-tv/www/js/live.js
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
(function (global) {
|
||||
var state = {
|
||||
categories: [],
|
||||
channels: [],
|
||||
categoryId: "__all__",
|
||||
selected: null,
|
||||
playList: [],
|
||||
playIndex: -1,
|
||||
mounted: false,
|
||||
focusInList: false,
|
||||
};
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function toast(msg) {
|
||||
if (global.McApp && McApp.toast) McApp.toast(msg);
|
||||
}
|
||||
|
||||
function showLoading(on) {
|
||||
if (global.McApp && McApp.showLoading) McApp.showLoading(on);
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function safeUrl(u) {
|
||||
return String(u || "").replace(/'/g, "%27");
|
||||
}
|
||||
|
||||
function categoryList() {
|
||||
var all = [{ id: "__all__", name: "Alles", channelCount: 0 }].concat(state.categories || []);
|
||||
var favs = McApi.getFavs();
|
||||
if (favs.length) {
|
||||
all.splice(1, 0, { id: "__fav__", name: "Favorieten", channelCount: favs.length });
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/** Bouw categoriechips één keer; update alleen active + behoud focus. */
|
||||
function renderCategories(opts) {
|
||||
opts = opts || {};
|
||||
var el = $("tv-cats");
|
||||
if (!el) return;
|
||||
var all = categoryList();
|
||||
var activeId = state.categoryId || "__all__";
|
||||
var prevFocusId = null;
|
||||
var ae = document.activeElement;
|
||||
if (ae && ae.getAttribute && ae.getAttribute("data-cat-id")) {
|
||||
prevFocusId = ae.getAttribute("data-cat-id");
|
||||
}
|
||||
|
||||
el.innerHTML = "";
|
||||
var focusTarget = null;
|
||||
all.forEach(function (c) {
|
||||
var b = document.createElement("button");
|
||||
b.className = "cat-chip focusable" + (activeId === c.id ? " active" : "");
|
||||
b.type = "button";
|
||||
b.setAttribute("data-cat-id", c.id);
|
||||
b.innerHTML =
|
||||
escapeHtml(c.name) +
|
||||
(c.channelCount ? " (" + c.channelCount + ")" : "") +
|
||||
'<span class="chip-line"></span>';
|
||||
b.addEventListener("click", function () {
|
||||
if (state.categoryId === c.id) {
|
||||
b.focus();
|
||||
return;
|
||||
}
|
||||
state.categoryId = c.id;
|
||||
state.focusInList = false;
|
||||
setCategoryActive(c.id);
|
||||
loadChannels(c.id, { keepCategoryFocus: true, focusChipId: c.id });
|
||||
});
|
||||
b.addEventListener("focus", function () {
|
||||
state.focusInList = false;
|
||||
});
|
||||
el.appendChild(b);
|
||||
if (c.id === (opts.focusChipId || prevFocusId || activeId)) {
|
||||
focusTarget = b;
|
||||
}
|
||||
});
|
||||
|
||||
if (opts.autoFocus || opts.keepCategoryFocus) {
|
||||
setTimeout(function () {
|
||||
if (focusTarget) focusTarget.focus();
|
||||
}, 30);
|
||||
}
|
||||
}
|
||||
|
||||
function setCategoryActive(id) {
|
||||
var chips = document.querySelectorAll("#tv-cats .cat-chip");
|
||||
for (var i = 0; i < chips.length; i++) {
|
||||
var chip = chips[i];
|
||||
if (chip.getAttribute("data-cat-id") === id) chip.classList.add("active");
|
||||
else chip.classList.remove("active");
|
||||
}
|
||||
}
|
||||
|
||||
function renderChannels(channels, opts) {
|
||||
opts = opts || {};
|
||||
state.channels = channels || [];
|
||||
var list = $("tv-list");
|
||||
var detail = $("tv-detail");
|
||||
if (!list) return;
|
||||
list.innerHTML = "";
|
||||
if (!state.channels.length) {
|
||||
list.innerHTML = '<div class="muted" style="padding:28px">Geen zenders in deze categorie</div>';
|
||||
if (detail) {
|
||||
detail.innerHTML = '<p class="muted">Geen zenders</p>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
state.channels.forEach(function (ch, idx) {
|
||||
var row = document.createElement("button");
|
||||
row.className = "ch-row focusable";
|
||||
row.type = "button";
|
||||
row.setAttribute("data-idx", String(idx));
|
||||
var now = ch.nowTitle || "";
|
||||
var fav = McApi.isFav(ch.streamId);
|
||||
var logo = ch.logoUrl
|
||||
? '<div class="ch-logo"><div class="ch-logo-img" style="background-image:url(\'' +
|
||||
safeUrl(ch.logoUrl) +
|
||||
"')\"></div></div>"
|
||||
: '<div class="ch-logo"><div class="ch-logo-img"></div></div>';
|
||||
row.innerHTML =
|
||||
'<span class="ch-bar"></span>' +
|
||||
logo +
|
||||
'<div class="ch-meta">' +
|
||||
'<div class="ch-name">' +
|
||||
(fav ? "* " : "") +
|
||||
escapeHtml(ch.name) +
|
||||
"</div>" +
|
||||
'<div class="ch-epg">' +
|
||||
escapeHtml(now ? "Nu · " + now : "Geen programma-info") +
|
||||
"</div></div>";
|
||||
row.addEventListener("click", function () {
|
||||
selectChannel(ch, idx);
|
||||
});
|
||||
row.addEventListener("focus", function () {
|
||||
state.focusInList = true;
|
||||
selectChannel(ch, idx, { skipRebuildActions: true });
|
||||
});
|
||||
row.addEventListener("keydown", function (e) {
|
||||
if ((e.keyCode || e.which) === 13) playChannel(ch, idx);
|
||||
});
|
||||
list.appendChild(row);
|
||||
});
|
||||
|
||||
var keepIdx = 0;
|
||||
if (state.selected) {
|
||||
for (var i = 0; i < state.channels.length; i++) {
|
||||
if (state.channels[i].streamId === state.selected.streamId) {
|
||||
keepIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
selectChannel(state.channels[keepIdx], keepIdx, { skipRebuildActions: false });
|
||||
|
||||
if (opts.focusFirstChannel) {
|
||||
var first = list.querySelector(".ch-row");
|
||||
if (first) setTimeout(function () { first.focus(); }, 40);
|
||||
}
|
||||
}
|
||||
|
||||
function paintDetail(ch, idx) {
|
||||
var detail = $("tv-detail");
|
||||
if (!detail || !ch) return;
|
||||
|
||||
var logoBlock = ch.logoUrl
|
||||
? '<div class="tv-detail-logo"><div class="tv-detail-logo-img" style="background-image:url(\'' +
|
||||
safeUrl(ch.logoUrl) +
|
||||
"')\"></div></div>"
|
||||
: "";
|
||||
|
||||
detail.innerHTML =
|
||||
logoBlock +
|
||||
"<h2>" + escapeHtml(ch.name) + "</h2>" +
|
||||
'<div class="epg-card">' +
|
||||
'<div class="epg-label">Nu</div>' +
|
||||
'<div class="epg-title" id="tv-epg-now">' +
|
||||
escapeHtml(ch.nowTitle || "Geen EPG beschikbaar") +
|
||||
"</div></div>" +
|
||||
'<div class="epg-card">' +
|
||||
'<div class="epg-label">Straks</div>' +
|
||||
'<div class="epg-title" id="tv-epg-next">' +
|
||||
escapeHtml(ch.nextTitle || "—") +
|
||||
"</div></div>" +
|
||||
'<div class="tv-actions">' +
|
||||
'<button class="btn primary focusable" id="tv-play">Kijken</button>' +
|
||||
'<button class="btn focusable" id="tv-fav">' +
|
||||
(McApi.isFav(ch.streamId) ? "* Favoriet" : "Favoriet") +
|
||||
"</button></div>" +
|
||||
'<p class="tv-hint">OK = kijken · Favoriet toevoegen met Favoriet</p>';
|
||||
|
||||
var playBtn = $("tv-play");
|
||||
var favBtn = $("tv-fav");
|
||||
if (playBtn) playBtn.onclick = function () { playChannel(ch, idx); };
|
||||
if (favBtn) {
|
||||
favBtn.onclick = function () {
|
||||
McApi.toggleFav(ch.streamId);
|
||||
renderCategories({ keepCategoryFocus: true, focusChipId: state.categoryId });
|
||||
loadChannels(state.categoryId || "__all__", {
|
||||
keepCategoryFocus: true,
|
||||
focusChipId: state.categoryId,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function selectChannel(ch, idx, opts) {
|
||||
opts = opts || {};
|
||||
state.selected = ch;
|
||||
state.playIndex = idx;
|
||||
var rows = document.querySelectorAll("#tv-list .ch-row");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
if (i === idx) rows[i].classList.add("active");
|
||||
else rows[i].classList.remove("active");
|
||||
}
|
||||
paintDetail(ch, idx);
|
||||
|
||||
McApi.iptvEpg(ch.streamId).then(function (epg) {
|
||||
if (!state.selected || state.selected.streamId !== ch.streamId) return;
|
||||
ch.nowTitle = epg.now && epg.now.title;
|
||||
ch.nextTitle = epg.next && epg.next.title;
|
||||
var nowEl = $("tv-epg-now");
|
||||
var nextEl = $("tv-epg-next");
|
||||
if (nowEl) nowEl.textContent = ch.nowTitle || "Geen EPG beschikbaar";
|
||||
if (nextEl) nextEl.textContent = ch.nextTitle || "—";
|
||||
var row = document.querySelector('#tv-list .ch-row[data-idx="' + idx + '"] .ch-epg');
|
||||
if (row) {
|
||||
row.textContent = ch.nowTitle ? "Nu · " + ch.nowTitle : "Geen programma-info";
|
||||
}
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function playChannel(ch, idx) {
|
||||
if (!ch) return;
|
||||
showLoading(true);
|
||||
McApi.iptvPlay(ch.streamId)
|
||||
.then(function (info) {
|
||||
showLoading(false);
|
||||
state.playList = state.channels.slice();
|
||||
state.playIndex = idx;
|
||||
if (global.McApp && McApp.openLivePlayer) {
|
||||
McApp.openLivePlayer({
|
||||
title: info.name || ch.name,
|
||||
url: info.streamUrl,
|
||||
fallbackUrl: info.fallbackStreamUrl,
|
||||
channels: state.playList,
|
||||
index: idx,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
showLoading(false);
|
||||
toast(e.message || "Live starten mislukt");
|
||||
});
|
||||
}
|
||||
|
||||
function loadChannels(categoryId, opts) {
|
||||
opts = opts || {};
|
||||
var id = categoryId || "__all__";
|
||||
state.categoryId = id;
|
||||
setCategoryActive(id);
|
||||
|
||||
showLoading(true);
|
||||
var req;
|
||||
if (id === "__fav__") {
|
||||
req = McApi.iptvChannels(null, true).then(function (res) {
|
||||
var all = (res && res.channels) || res || [];
|
||||
return (all || []).filter(function (c) { return McApi.isFav(c.streamId); });
|
||||
});
|
||||
} else if (id === "__all__") {
|
||||
req = McApi.iptvChannels(null, true).then(function (res) {
|
||||
return (res && res.channels) || res || [];
|
||||
});
|
||||
} else {
|
||||
req = McApi.iptvChannels(id, true).then(function (res) {
|
||||
return (res && res.channels) || res || [];
|
||||
});
|
||||
}
|
||||
req
|
||||
.then(function (channels) {
|
||||
showLoading(false);
|
||||
renderChannels(channels, {
|
||||
focusFirstChannel: !!opts.focusFirstChannel,
|
||||
});
|
||||
if (opts.keepCategoryFocus) {
|
||||
var chip =
|
||||
document.querySelector('#tv-cats .cat-chip[data-cat-id="' + (opts.focusChipId || id) + '"]');
|
||||
if (chip) {
|
||||
setTimeout(function () { chip.focus(); }, 40);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
showLoading(false);
|
||||
toast(e.message || "Zenders laden mislukt");
|
||||
if (opts.keepCategoryFocus) {
|
||||
var chip =
|
||||
document.querySelector('#tv-cats .cat-chip[data-cat-id="' + (opts.focusChipId || id) + '"]');
|
||||
if (chip) chip.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mount(container) {
|
||||
state.mounted = true;
|
||||
state.focusInList = false;
|
||||
container.innerHTML =
|
||||
'<div class="tv-layout">' +
|
||||
'<div id="tv-cats" class="tv-cats"></div>' +
|
||||
'<div class="tv-body">' +
|
||||
'<div id="tv-list" class="tv-list"></div>' +
|
||||
'<div id="tv-detail" class="tv-detail"><p class="muted">Kies een zender om te kijken</p></div>' +
|
||||
"</div></div>";
|
||||
|
||||
if (state.categories.length) {
|
||||
var id = state.categoryId || "__all__";
|
||||
renderCategories({ autoFocus: true, focusChipId: id });
|
||||
loadChannels(id, { keepCategoryFocus: true, focusChipId: id });
|
||||
return;
|
||||
}
|
||||
|
||||
state.categoryId = "__all__";
|
||||
state.selected = null;
|
||||
showLoading(true);
|
||||
McApi.iptvCategories()
|
||||
.then(function (res) {
|
||||
state.categories = (res && res.categories) || res || [];
|
||||
renderCategories({ autoFocus: true, focusChipId: "__all__" });
|
||||
loadChannels("__all__", { keepCategoryFocus: true, focusChipId: "__all__" });
|
||||
})
|
||||
.catch(function (e) {
|
||||
showLoading(false);
|
||||
container.innerHTML =
|
||||
'<p class="muted" style="padding:48px">' +
|
||||
escapeHtml(e.message || "Live TV niet beschikbaar") +
|
||||
"</p>";
|
||||
});
|
||||
}
|
||||
|
||||
function zap(delta) {
|
||||
if (!state.playList.length) return null;
|
||||
var next = state.playIndex + delta;
|
||||
if (next < 0) next = state.playList.length - 1;
|
||||
if (next >= state.playList.length) next = 0;
|
||||
var ch = state.playList[next];
|
||||
state.playIndex = next;
|
||||
return ch;
|
||||
}
|
||||
|
||||
global.McLive = {
|
||||
mount: mount,
|
||||
zap: zap,
|
||||
playChannel: playChannel,
|
||||
getPlayState: function () {
|
||||
return { list: state.playList, index: state.playIndex };
|
||||
},
|
||||
};
|
||||
})(window);
|
||||
764
apps/tizen-tv/www/js/player.js
Normal file
764
apps/tizen-tv/www/js/player.js
Normal file
|
|
@ -0,0 +1,764 @@
|
|||
(function (global) {
|
||||
var usingAvplay = false;
|
||||
var htmlVideo = null;
|
||||
var listeners = {};
|
||||
var hideTimer = null;
|
||||
var tickTimer = null;
|
||||
var webapisLoaded = false;
|
||||
var currentUrl = "";
|
||||
var currentTitle = "";
|
||||
var externalCues = [];
|
||||
var externalLabel = "";
|
||||
var silentText = false;
|
||||
var lastAvSubText = "";
|
||||
var playSession = 0;
|
||||
var fallbackOnce = false;
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function hostOf(url) {
|
||||
try {
|
||||
var m = String(url).match(/^https?:\/\/([^\/]+)/i);
|
||||
return m ? m[1] : "?";
|
||||
} catch (e) {
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
function loadWebapis(cb) {
|
||||
if (webapisLoaded || (global.webapis && webapis.avplay)) {
|
||||
webapisLoaded = true;
|
||||
cb(true);
|
||||
return;
|
||||
}
|
||||
var s = document.createElement("script");
|
||||
s.src = "$WEBAPIS/webapis/webapis.js";
|
||||
s.onload = function () {
|
||||
webapisLoaded = true;
|
||||
cb(!!(global.webapis && webapis.avplay));
|
||||
};
|
||||
s.onerror = function () {
|
||||
cb(false);
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function hasAvplay() {
|
||||
return !!(global.webapis && webapis.avplay);
|
||||
}
|
||||
|
||||
function ensureAvObject() {
|
||||
var wrap = $("av-object-wrap");
|
||||
if (!wrap) return null;
|
||||
wrap.style.display = "block";
|
||||
wrap.style.position = "absolute";
|
||||
wrap.style.left = "0";
|
||||
wrap.style.top = "0";
|
||||
wrap.style.width = "100%";
|
||||
wrap.style.height = "100%";
|
||||
var obj = document.getElementById("av-player");
|
||||
if (!obj) {
|
||||
obj = document.createElement("object");
|
||||
obj.id = "av-player";
|
||||
obj.type = "application/avplayer";
|
||||
obj.setAttribute(
|
||||
"style",
|
||||
"position:absolute;left:0;top:0;width:1920px;height:1080px;"
|
||||
);
|
||||
wrap.appendChild(obj);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function showUi(show, opts) {
|
||||
opts = opts || {};
|
||||
var ui = $("player-ui");
|
||||
if (!ui) return;
|
||||
if (show) ui.classList.remove("fade");
|
||||
else ui.classList.add("fade");
|
||||
clearTimeout(hideTimer);
|
||||
if (show) {
|
||||
if (opts.focusPlay) {
|
||||
var playBtn = $("btn-play");
|
||||
if (playBtn) setTimeout(function () { playBtn.focus(); }, 30);
|
||||
}
|
||||
hideTimer = setTimeout(function () {
|
||||
if ($("player-menu") && !$("player-menu").classList.contains("hidden")) return;
|
||||
var ae = document.activeElement;
|
||||
if (ae && ae.className && String(ae.className).indexOf("pctrl") >= 0) {
|
||||
/* focus in controls: niet auto-verbergen */
|
||||
return;
|
||||
}
|
||||
ui.classList.add("fade");
|
||||
}, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
function isUiVisible() {
|
||||
var ui = $("player-ui");
|
||||
return !!(ui && !ui.classList.contains("fade"));
|
||||
}
|
||||
|
||||
function setPlayingIcon(playing) {
|
||||
var btn = $("btn-play");
|
||||
if (!btn) return;
|
||||
/* Geen fancy unicode — ontbreekt vaak op Tizen-fonts */
|
||||
btn.textContent = playing ? "Pauze" : "Play";
|
||||
}
|
||||
|
||||
function safeAvClose() {
|
||||
if (!hasAvplay()) return;
|
||||
try {
|
||||
var st = "";
|
||||
try { st = webapis.avplay.getState(); } catch (e0) { st = ""; }
|
||||
if (st === "PLAYING" || st === "PAUSED" || st === "READY") {
|
||||
try { webapis.avplay.stop(); } catch (e1) {}
|
||||
}
|
||||
if (st && st !== "NONE") {
|
||||
try { webapis.avplay.close(); } catch (e2) {}
|
||||
}
|
||||
} catch (e) {
|
||||
try { webapis.avplay.close(); } catch (e3) {}
|
||||
}
|
||||
}
|
||||
|
||||
function destroyAvObject() {
|
||||
safeAvClose();
|
||||
var wrap = $("av-object-wrap");
|
||||
if (wrap) {
|
||||
wrap.innerHTML = "";
|
||||
wrap.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(ms) {
|
||||
if (!ms || ms < 0) return "0:00";
|
||||
var s = Math.floor(ms / 1000);
|
||||
var m = Math.floor(s / 60);
|
||||
var h = Math.floor(m / 60);
|
||||
s = s % 60;
|
||||
m = m % 60;
|
||||
function two(n) { return n < 10 ? "0" + n : String(n); }
|
||||
if (h > 0) return h + ":" + two(m) + ":" + two(s);
|
||||
return m + ":" + two(s);
|
||||
}
|
||||
|
||||
function getPositionMs() {
|
||||
try {
|
||||
if (usingAvplay) return webapis.avplay.getCurrentTime() || 0;
|
||||
if (htmlVideo) return (htmlVideo.currentTime || 0) * 1000;
|
||||
} catch (e) {}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getDurationMs() {
|
||||
try {
|
||||
if (usingAvplay) return webapis.avplay.getDuration() || 0;
|
||||
if (htmlVideo) return (htmlVideo.duration || 0) * 1000;
|
||||
} catch (e) {}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function setSubOverlay(text) {
|
||||
var el = $("player-subs");
|
||||
if (!el) return;
|
||||
if (!text || silentText) {
|
||||
el.classList.add("hidden");
|
||||
el.textContent = "";
|
||||
return;
|
||||
}
|
||||
el.textContent = text;
|
||||
el.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function updateExternalCue() {
|
||||
if (!externalCues.length || silentText) {
|
||||
if (!lastAvSubText) setSubOverlay("");
|
||||
return;
|
||||
}
|
||||
var pos = getPositionMs();
|
||||
var text = "";
|
||||
for (var i = 0; i < externalCues.length; i++) {
|
||||
var c = externalCues[i];
|
||||
if (pos >= c.start && pos < c.end) {
|
||||
text = c.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setSubOverlay(text);
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
var pos = getPositionMs();
|
||||
var dur = getDurationMs();
|
||||
var fill = $("player-fill");
|
||||
var time = $("player-time");
|
||||
if (fill && dur > 0) fill.style.width = Math.min(100, (pos / dur) * 100) + "%";
|
||||
if (time) time.textContent = formatTime(pos) + " / " + formatTime(dur);
|
||||
updateExternalCue();
|
||||
}
|
||||
|
||||
function startTick() {
|
||||
stopTick();
|
||||
tickTimer = setInterval(updateProgress, 400);
|
||||
}
|
||||
|
||||
function stopTick() {
|
||||
if (tickTimer) clearInterval(tickTimer);
|
||||
tickTimer = null;
|
||||
}
|
||||
|
||||
function setError(msg) {
|
||||
var el = $("player-error");
|
||||
if (!el) return;
|
||||
if (msg) {
|
||||
el.textContent = msg;
|
||||
el.classList.remove("hidden");
|
||||
} else {
|
||||
el.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function clearExternal() {
|
||||
externalCues = [];
|
||||
externalLabel = "";
|
||||
lastAvSubText = "";
|
||||
setSubOverlay("");
|
||||
}
|
||||
|
||||
function stop() {
|
||||
playSession += 1;
|
||||
fallbackOnce = false;
|
||||
stopTick();
|
||||
setError(null);
|
||||
clearExternal();
|
||||
silentText = false;
|
||||
usingAvplay = false;
|
||||
destroyAvObject();
|
||||
if (htmlVideo) {
|
||||
try {
|
||||
htmlVideo.onplaying = null;
|
||||
htmlVideo.onended = null;
|
||||
htmlVideo.onerror = null;
|
||||
htmlVideo.removeAttribute("src");
|
||||
htmlVideo.load();
|
||||
} catch (e) {}
|
||||
htmlVideo.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function playWithHtml(url, note) {
|
||||
var session = playSession;
|
||||
usingAvplay = false;
|
||||
setError(null);
|
||||
|
||||
htmlVideo = $("html-player");
|
||||
if (!htmlVideo) {
|
||||
setError("Geen HTML5-speler beschikbaar");
|
||||
return;
|
||||
}
|
||||
htmlVideo.classList.remove("hidden");
|
||||
htmlVideo.setAttribute(
|
||||
"style",
|
||||
"position:absolute;left:0;top:0;width:100%;height:100%;background:#000;"
|
||||
);
|
||||
|
||||
try {
|
||||
htmlVideo.onplaying = null;
|
||||
htmlVideo.onended = null;
|
||||
htmlVideo.onerror = null;
|
||||
} catch (e0) {}
|
||||
|
||||
htmlVideo.onplaying = function () {
|
||||
if (session !== playSession) return;
|
||||
setError(null);
|
||||
setPlayingIcon(true);
|
||||
};
|
||||
htmlVideo.onended = function () {
|
||||
if (session !== playSession) return;
|
||||
if (listeners.onEnded) listeners.onEnded();
|
||||
};
|
||||
htmlVideo.onerror = function () {
|
||||
if (session !== playSession) return;
|
||||
setError("Kan stream niet openen.\nHost: " + hostOf(url));
|
||||
};
|
||||
|
||||
function tryPlay(attempt) {
|
||||
if (session !== playSession) return;
|
||||
try {
|
||||
if (!htmlVideo.src || htmlVideo.src.indexOf(url.split("?")[0]) < 0) {
|
||||
htmlVideo.src = url;
|
||||
}
|
||||
var p = htmlVideo.play();
|
||||
if (p && p.catch) {
|
||||
p.catch(function (e) {
|
||||
if (session !== playSession) return;
|
||||
var msg = (e && e.message) || String(e);
|
||||
if (/interrupted|AbortError/i.test(msg) && attempt < 3) {
|
||||
setTimeout(function () { tryPlay(attempt + 1); }, 300 * attempt);
|
||||
return;
|
||||
}
|
||||
setError(
|
||||
"Afspelen mislukt op deze TV.\nHost: " +
|
||||
hostOf(url) +
|
||||
(note ? "\n(" + note + ")" : "")
|
||||
);
|
||||
});
|
||||
}
|
||||
startTick();
|
||||
} catch (e) {
|
||||
if (attempt < 3) {
|
||||
setTimeout(function () { tryPlay(attempt + 1); }, 300);
|
||||
} else {
|
||||
setError("HTML5 crash: " + (e.message || e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function () { tryPlay(1); }, 50);
|
||||
}
|
||||
|
||||
function fallbackHtml(url, reason) {
|
||||
if (fallbackOnce) return;
|
||||
fallbackOnce = true;
|
||||
/* AVPlay-callbacks doden + decoder vrijgeven vóór HTML5 */
|
||||
playSession += 1;
|
||||
var session = playSession;
|
||||
usingAvplay = false;
|
||||
destroyAvObject();
|
||||
setError(null);
|
||||
setTimeout(function () {
|
||||
if (session !== playSession) return;
|
||||
playWithHtml(url, reason || null);
|
||||
}, 450);
|
||||
}
|
||||
|
||||
function playWithAvplay(url) {
|
||||
var session = playSession;
|
||||
fallbackOnce = false;
|
||||
usingAvplay = true;
|
||||
var hv = $("html-player");
|
||||
if (hv) {
|
||||
try {
|
||||
hv.onplaying = null;
|
||||
hv.onended = null;
|
||||
hv.onerror = null;
|
||||
hv.removeAttribute("src");
|
||||
hv.load();
|
||||
} catch (eH) {}
|
||||
hv.classList.add("hidden");
|
||||
}
|
||||
ensureAvObject();
|
||||
|
||||
setTimeout(function () {
|
||||
if (session !== playSession) return;
|
||||
try {
|
||||
safeAvClose();
|
||||
} catch (eClose) {}
|
||||
|
||||
setTimeout(function () {
|
||||
if (session !== playSession) return;
|
||||
try {
|
||||
webapis.avplay.open(url);
|
||||
try {
|
||||
webapis.avplay.setDisplayMethod("PLAYER_DISPLAY_MODE_AUTO_ASPECT_RATIO");
|
||||
} catch (e1) {}
|
||||
webapis.avplay.setDisplayRect(0, 0, 1920, 1080);
|
||||
|
||||
try {
|
||||
webapis.avplay.setStreamingProperty(
|
||||
"USER_AGENT",
|
||||
"Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0) AppleWebKit/537.36"
|
||||
);
|
||||
} catch (e2) {}
|
||||
|
||||
webapis.avplay.setListener({
|
||||
onbufferingstart: function () {},
|
||||
onbufferingprogress: function () {},
|
||||
onbufferingcomplete: function () {},
|
||||
oncurrentplaytime: function () {
|
||||
if (session !== playSession) return;
|
||||
updateProgress();
|
||||
},
|
||||
onsubtitlechange: function (duration, text) {
|
||||
if (session !== playSession) return;
|
||||
if (externalCues.length) return;
|
||||
var s = text;
|
||||
if (Object.prototype.toString.call(text) === "[object Array]") {
|
||||
s = text.join("\n");
|
||||
} else if (text && typeof text === "object") {
|
||||
s = text.text || text.Data || text.data || "";
|
||||
}
|
||||
lastAvSubText = String(s || "").replace(/<[^>]+>/g, "");
|
||||
if (!silentText) setSubOverlay(lastAvSubText);
|
||||
},
|
||||
onstreamcompleted: function () {
|
||||
if (session !== playSession) return;
|
||||
stop();
|
||||
if (listeners.onEnded) listeners.onEnded();
|
||||
},
|
||||
onerror: function (type) {
|
||||
if (session !== playSession) return;
|
||||
fallbackHtml(url, String(type || "error"));
|
||||
if (listeners.onError) listeners.onError(type);
|
||||
},
|
||||
});
|
||||
|
||||
webapis.avplay.prepareAsync(
|
||||
function () {
|
||||
if (session !== playSession) return;
|
||||
try {
|
||||
var st = "";
|
||||
try { st = webapis.avplay.getState(); } catch (eS) {}
|
||||
if (st !== "READY" && st !== "PAUSED" && st !== "PLAYING") {
|
||||
fallbackHtml(url, "AVPlay state " + st);
|
||||
return;
|
||||
}
|
||||
webapis.avplay.play();
|
||||
startTick();
|
||||
setError(null);
|
||||
setPlayingIcon(true);
|
||||
} catch (ePlay) {
|
||||
fallbackHtml(url, "play");
|
||||
}
|
||||
},
|
||||
function (e) {
|
||||
if (session !== playSession) return;
|
||||
fallbackHtml(url, e && (e.message || e.name) ? String(e.message || e.name) : "prepare");
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
if (session !== playSession) return;
|
||||
fallbackHtml(url, e.message || String(e));
|
||||
}
|
||||
}, 150);
|
||||
}, 80);
|
||||
}
|
||||
|
||||
function playUrl(url, title) {
|
||||
stop();
|
||||
playSession += 1;
|
||||
fallbackOnce = false;
|
||||
currentUrl = url;
|
||||
currentTitle = title || "";
|
||||
$("player-title").textContent = currentTitle;
|
||||
setError(null);
|
||||
setPlayingIcon(true);
|
||||
showUi(true, { focusPlay: true });
|
||||
|
||||
if (/^http:\/\//i.test(url)) {
|
||||
playWithHtml(url, null);
|
||||
return;
|
||||
}
|
||||
|
||||
loadWebapis(function (ok) {
|
||||
if (ok && hasAvplay()) playWithAvplay(url);
|
||||
else playWithHtml(url, null);
|
||||
});
|
||||
}
|
||||
|
||||
function parseExtra(extra) {
|
||||
if (!extra) return {};
|
||||
if (typeof extra === "object") return extra;
|
||||
try { return JSON.parse(extra); } catch (e) { return { raw: String(extra) }; }
|
||||
}
|
||||
|
||||
function langLabel(code) {
|
||||
var c = String(code || "").trim().toLowerCase();
|
||||
if (!c || c === "und" || c === "unknown" || c === "null") return "";
|
||||
if (c === "nl" || c === "dut" || c === "nld") return "Nederlands";
|
||||
if (c === "en" || c === "eng") return "Engels";
|
||||
if (c === "de" || c === "ger" || c === "deu") return "Duits";
|
||||
if (c === "fr" || c === "fre" || c === "fra") return "Frans";
|
||||
if (c === "es" || c === "spa") return "Spaans";
|
||||
if (c === "it" || c === "ita") return "Italiaans";
|
||||
if (c === "pt" || c === "por") return "Portugees";
|
||||
if (c === "pl" || c === "pol") return "Pools";
|
||||
if (c === "ru" || c === "rus") return "Russisch";
|
||||
if (c === "ja" || c === "jpn") return "Japans";
|
||||
if (c === "zh" || c === "chi" || c === "zho") return "Chinees";
|
||||
return c.toUpperCase();
|
||||
}
|
||||
|
||||
function isJunkLang(v) {
|
||||
var s = String(v || "").trim().toLowerCase();
|
||||
if (!s) return true;
|
||||
if (s.indexOf("/") >= 0) return true; /* text/plain, application/x-subrip */
|
||||
if (s.indexOf("text") === 0) return true;
|
||||
if (s === "plain" || s === "utf-8" || s === "utf8") return true;
|
||||
if (s === "und" || s === "unknown" || s === "null" || s === "undefined") return true;
|
||||
if (/^(srt|ass|ssa|vtt|sub|idx|pgs|hdmv)$/.test(s)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function pickLangFromExtra(extra) {
|
||||
var keys = [
|
||||
"track_lang",
|
||||
"language",
|
||||
"lang",
|
||||
"language_code",
|
||||
"Language",
|
||||
"lang_code",
|
||||
];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var v = extra[keys[i]];
|
||||
if (v && typeof v === "object") v = v.code || v.name || "";
|
||||
if (!isJunkLang(v)) return String(v).trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function trackLabel(type, index, extra) {
|
||||
var lang = pickLangFromExtra(extra);
|
||||
var nice = langLabel(lang);
|
||||
var channels = extra.channels || extra.audio_channels || "";
|
||||
if (typeof channels === "object") channels = "";
|
||||
var base =
|
||||
nice ||
|
||||
(type === "AUDIO" ? "Audio " + (index + 1) : "Ondertitel " + (index + 1));
|
||||
if (channels && String(channels).match(/^\d+$/)) base += " · " + channels + "ch";
|
||||
return base;
|
||||
}
|
||||
|
||||
function listAvTracks(type) {
|
||||
var out = [];
|
||||
if (!usingAvplay || !hasAvplay()) return out;
|
||||
try {
|
||||
var info = webapis.avplay.getTotalTrackInfo() || [];
|
||||
for (var i = 0; i < info.length; i++) {
|
||||
var t = info[i];
|
||||
if (!t) continue;
|
||||
var tType = String(t.type || "").toUpperCase();
|
||||
if (tType !== type) continue;
|
||||
var idx = typeof t.index === "number" ? t.index : i;
|
||||
var extra = parseExtra(t.extra_info);
|
||||
out.push({
|
||||
type: type,
|
||||
index: idx,
|
||||
label: trackLabel(type, idx, extra),
|
||||
lang: extra.track_lang || extra.language || "",
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
return out;
|
||||
}
|
||||
|
||||
function listHtmlAudio() {
|
||||
var out = [];
|
||||
if (!htmlVideo) return out;
|
||||
var tracks = htmlVideo.audioTracks;
|
||||
if (!tracks || !tracks.length) return out;
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
var t = tracks[i];
|
||||
var nice = langLabel(t.language || "") || t.label || ("Audio " + (i + 1));
|
||||
out.push({ type: "AUDIO", index: i, label: nice, lang: t.language || "" });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function listHtmlText() {
|
||||
var out = [];
|
||||
if (!htmlVideo) return out;
|
||||
var tracks = htmlVideo.textTracks;
|
||||
if (!tracks || !tracks.length) return out;
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
var t = tracks[i];
|
||||
var kind = String(t.kind || "").toLowerCase();
|
||||
if (kind && kind !== "subtitles" && kind !== "captions") continue;
|
||||
var nice = langLabel(t.language || "") || t.label || ("Ondertitel " + (i + 1));
|
||||
out.push({ type: "TEXT", index: i, label: nice, lang: t.language || "" });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function listAudioTracks() {
|
||||
return usingAvplay ? listAvTracks("AUDIO") : listHtmlAudio();
|
||||
}
|
||||
|
||||
function listTextTracks() {
|
||||
var embedded = usingAvplay ? listAvTracks("TEXT") : listHtmlText();
|
||||
if (externalCues.length) {
|
||||
embedded = embedded.concat([{
|
||||
type: "EXTERNAL",
|
||||
index: -1,
|
||||
label: externalLabel || "OpenSubtitles",
|
||||
lang: "",
|
||||
}]);
|
||||
}
|
||||
return embedded;
|
||||
}
|
||||
|
||||
function selectAudio(index) {
|
||||
try {
|
||||
if (usingAvplay) {
|
||||
webapis.avplay.setSelectTrack("AUDIO", index);
|
||||
} else if (htmlVideo && htmlVideo.audioTracks) {
|
||||
for (var i = 0; i < htmlVideo.audioTracks.length; i++) {
|
||||
htmlVideo.audioTracks[i].enabled = i === index;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function disableText() {
|
||||
silentText = true;
|
||||
lastAvSubText = "";
|
||||
setSubOverlay("");
|
||||
try {
|
||||
if (usingAvplay) {
|
||||
webapis.avplay.setSilentSubtitle(true);
|
||||
} else if (htmlVideo && htmlVideo.textTracks) {
|
||||
for (var i = 0; i < htmlVideo.textTracks.length; i++) {
|
||||
htmlVideo.textTracks[i].mode = "disabled";
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function selectText(index) {
|
||||
silentText = false;
|
||||
try {
|
||||
if (usingAvplay) {
|
||||
webapis.avplay.setSilentSubtitle(false);
|
||||
webapis.avplay.setSelectTrack("TEXT", index);
|
||||
} else if (htmlVideo && htmlVideo.textTracks) {
|
||||
for (var i = 0; i < htmlVideo.textTracks.length; i++) {
|
||||
htmlVideo.textTracks[i].mode = i === index ? "showing" : "disabled";
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function parseTimeToMs(raw) {
|
||||
var s = String(raw).trim().replace(",", ".");
|
||||
var parts = s.split(":");
|
||||
if (parts.length < 3) return 0;
|
||||
var h = parseInt(parts[0], 10) || 0;
|
||||
var m = parseInt(parts[1], 10) || 0;
|
||||
var sec = parseFloat(parts[2]) || 0;
|
||||
return Math.round(((h * 60 + m) * 60 + sec) * 1000);
|
||||
}
|
||||
|
||||
function parseSrt(text) {
|
||||
var blocks = String(text).replace(/\r/g, "").split(/\n\n+/);
|
||||
var cues = [];
|
||||
for (var i = 0; i < blocks.length; i++) {
|
||||
var lines = blocks[i].split("\n").filter(function (l) { return l.length; });
|
||||
if (lines.length < 2) continue;
|
||||
var timeLine = lines[0].indexOf("-->") >= 0 ? lines[0] : lines[1];
|
||||
if (!timeLine || timeLine.indexOf("-->") < 0) continue;
|
||||
var times = timeLine.split("-->");
|
||||
var start = parseTimeToMs(times[0]);
|
||||
var end = parseTimeToMs(times[1]);
|
||||
var bodyStart = lines[0].indexOf("-->") >= 0 ? 1 : 2;
|
||||
var body = lines.slice(bodyStart).join("\n").replace(/<[^>]+>/g, "");
|
||||
if (end > start && body) cues.push({ start: start, end: end, text: body });
|
||||
}
|
||||
return cues;
|
||||
}
|
||||
|
||||
function parseVtt(text) {
|
||||
var cleaned = String(text).replace(/\r/g, "");
|
||||
if (cleaned.indexOf("WEBVTT") === 0) {
|
||||
cleaned = cleaned.replace(/^WEBVTT[^\n]*\n+/, "");
|
||||
}
|
||||
return parseSrt(cleaned);
|
||||
}
|
||||
|
||||
function b64ToText(b64) {
|
||||
try {
|
||||
var bin = atob(b64);
|
||||
try {
|
||||
return decodeURIComponent(escape(bin));
|
||||
} catch (e1) {
|
||||
return bin;
|
||||
}
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function applyExternalSubtitle(payload) {
|
||||
var text = b64ToText(payload.contentBase64 || "");
|
||||
if (!text) throw new Error("Lege ondertitel");
|
||||
var format = String(payload.format || "srt").toLowerCase();
|
||||
var cues = format === "vtt" ? parseVtt(text) : parseSrt(text);
|
||||
if (!cues.length) throw new Error("Kon ondertitel niet lezen");
|
||||
externalCues = cues;
|
||||
externalLabel = payload.label || payload.fileName || "OpenSubtitles";
|
||||
silentText = false;
|
||||
lastAvSubText = "";
|
||||
try {
|
||||
if (usingAvplay) webapis.avplay.setSilentSubtitle(true);
|
||||
else if (htmlVideo && htmlVideo.textTracks) {
|
||||
for (var i = 0; i < htmlVideo.textTracks.length; i++) {
|
||||
htmlVideo.textTracks[i].mode = "disabled";
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
updateExternalCue();
|
||||
return cues.length;
|
||||
}
|
||||
|
||||
function togglePause() {
|
||||
showUi(true);
|
||||
try {
|
||||
if (usingAvplay) {
|
||||
var state = webapis.avplay.getState();
|
||||
if (state === "PLAYING") {
|
||||
webapis.avplay.pause();
|
||||
setPlayingIcon(false);
|
||||
} else if (state === "PAUSED") {
|
||||
webapis.avplay.play();
|
||||
setPlayingIcon(true);
|
||||
}
|
||||
} else if (htmlVideo) {
|
||||
if (htmlVideo.paused) {
|
||||
htmlVideo.play();
|
||||
setPlayingIcon(true);
|
||||
} else {
|
||||
htmlVideo.pause();
|
||||
setPlayingIcon(false);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function seek(deltaMs) {
|
||||
showUi(true);
|
||||
try {
|
||||
if (usingAvplay) {
|
||||
var pos = webapis.avplay.getCurrentTime() + deltaMs;
|
||||
if (pos < 0) pos = 0;
|
||||
webapis.avplay.seekTo(pos);
|
||||
} else if (htmlVideo) {
|
||||
htmlVideo.currentTime = Math.max(0, htmlVideo.currentTime + deltaMs / 1000);
|
||||
}
|
||||
} catch (e) {}
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
global.McPlayer = {
|
||||
playUrl: playUrl,
|
||||
stop: stop,
|
||||
togglePause: togglePause,
|
||||
seek: seek,
|
||||
showUi: showUi,
|
||||
isUiVisible: isUiVisible,
|
||||
setPlayingIcon: setPlayingIcon,
|
||||
getPositionMs: getPositionMs,
|
||||
getDurationMs: getDurationMs,
|
||||
listAudioTracks: listAudioTracks,
|
||||
listTextTracks: listTextTracks,
|
||||
selectAudio: selectAudio,
|
||||
selectText: selectText,
|
||||
disableText: disableText,
|
||||
applyExternalSubtitle: applyExternalSubtitle,
|
||||
langLabel: langLabel,
|
||||
on: function (name, fn) {
|
||||
listeners[name] = fn;
|
||||
},
|
||||
};
|
||||
})(window);
|
||||
59
apps/tizen-tv/www/js/polyfill.js
Normal file
59
apps/tizen-tv/www/js/polyfill.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/* Oudere Tizen-browsers: fetch + pad helpers */
|
||||
(function (global) {
|
||||
if (typeof global.fetch !== "function") {
|
||||
global.fetch = function (url, opts) {
|
||||
opts = opts || {};
|
||||
return new Promise(function (resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(opts.method || "GET", url, true);
|
||||
var headers = opts.headers || {};
|
||||
for (var k in headers) {
|
||||
if (headers.hasOwnProperty(k)) xhr.setRequestHeader(k, headers[k]);
|
||||
}
|
||||
xhr.onload = function () {
|
||||
var body = xhr.responseText;
|
||||
resolve({
|
||||
ok: xhr.status >= 200 && xhr.status < 300,
|
||||
status: xhr.status,
|
||||
text: function () {
|
||||
return Promise.resolve(body);
|
||||
},
|
||||
json: function () {
|
||||
return Promise.resolve(JSON.parse(body || "null"));
|
||||
},
|
||||
});
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
reject(new Error("Netwerkfout"));
|
||||
};
|
||||
xhr.send(opts.body || null);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (!Element.prototype.closest) {
|
||||
Element.prototype.closest = function (sel) {
|
||||
var el = this;
|
||||
while (el && el.nodeType === 1) {
|
||||
if (el.matches ? el.matches(sel) : el.msMatchesSelector(sel)) return el;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
if (!Element.prototype.matches) {
|
||||
Element.prototype.matches =
|
||||
Element.prototype.msMatchesSelector ||
|
||||
Element.prototype.webkitMatchesSelector;
|
||||
}
|
||||
|
||||
if (typeof Promise === "undefined") {
|
||||
var fatal = document.getElementById("fatal");
|
||||
if (fatal) {
|
||||
fatal.classList.remove("hidden");
|
||||
fatal.textContent =
|
||||
"Deze TV ondersteunt geen moderne JavaScript (Promise ontbreekt).";
|
||||
}
|
||||
}
|
||||
})(window);
|
||||
Loading…
Reference in a new issue