stremio/apps/tizen-tv/www/js/player.js

781 lines
22 KiB
JavaScript

(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;
var pendingStartMs = 0;
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 applyPendingStart() {
var ms = pendingStartMs;
if (!ms || ms < 5000) return;
pendingStartMs = 0;
try {
if (usingAvplay) webapis.avplay.seekTo(ms);
else if (htmlVideo) htmlVideo.currentTime = ms / 1000;
} catch (e) {}
}
function stop() {
playSession += 1;
fallbackOnce = false;
pendingStartMs = 0;
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);
applyPendingStart();
};
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 {
var ua = "Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0) AppleWebKit/537.36";
if (/viaplay/i.test(url)) {
ua =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
}
webapis.avplay.setStreamingProperty("USER_AGENT", ua);
} 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();
applyPendingStart();
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, startMs) {
stop();
playSession += 1;
fallbackOnce = false;
pendingStartMs = startMs > 5000 ? startMs : 0;
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);