Files
JW-Player/assembly_player/bridge.js
2026-07-13 09:39:24 +02:00

158 lines
4.5 KiB
JavaScript

// Injected into stream.jw.org. Discovers the ordered list of on-demand
// sessions, drives session selection, and reports playback progress back to
// the native app through window.webkit.messageHandlers.bridge.
(function () {
if (window.__bridge) return;
const CARD_TITLE_SELECTOR = '[data-testid="vod-program-card-title"]';
const CARD_SELECTOR = '[data-testid="vod-program-card"]';
const COOKIE_ACCEPT_SELECTOR = '.lnc-acceptCookiesButton';
const PROGRESS_INTERVAL_MS = 4000;
let video = null;
let pendingSeek = null;
let lastReportedTitle = null;
let knownTitles = [];
function post(payload) {
try {
window.webkit.messageHandlers.bridge.postMessage(JSON.stringify(payload));
} catch (e) {
// messageHandlers unavailable (e.g. plain browser testing); ignore.
}
}
function currentTitle() {
return document.title.replace(/\s*::\s*JW Stream\s*$/, '').trim();
}
function dismissCookieBanner() {
const btn = document.querySelector(COOKIE_ACCEPT_SELECTOR);
if (btn) btn.click();
}
function scanPrograms() {
const nodes = document.querySelectorAll(CARD_TITLE_SELECTOR);
if (!nodes.length) return;
const titles = Array.from(nodes).map((n) => n.textContent.trim());
const changed =
titles.length !== knownTitles.length || titles.some((t, i) => t !== knownTitles[i]);
if (changed) {
knownTitles = titles;
post({ type: 'sessions', titles });
}
}
function nudgeRepaint(v) {
// Workaround for a WebKitGTK compositing glitch: switching the video
// source can leave the picture black (audio keeps playing) until
// something forces the compositor to repaint the video layer. Toggling
// a transform forces exactly that, harmlessly.
v.style.transform = 'translateZ(0)';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
v.style.transform = '';
});
});
}
function attachVideo(v) {
if (video === v) return;
video = v;
video.addEventListener('loadedmetadata', () => {
if (pendingSeek != null && isFinite(video.duration)) {
video.currentTime = Math.min(pendingSeek, Math.max(video.duration - 1, 0));
pendingSeek = null;
}
post({ type: 'metadata', title: currentTitle(), duration: video.duration });
});
video.addEventListener('ended', () => {
post({ type: 'ended', title: currentTitle() });
});
video.addEventListener('play', () => post({ type: 'playstate', paused: false }));
video.addEventListener('pause', () => post({ type: 'playstate', paused: true }));
video.addEventListener('playing', () => nudgeRepaint(v));
}
function reportProgress() {
if (!video) return;
const title = currentTitle();
if (!title) return;
post({
type: 'progress',
title,
currentTime: video.currentTime,
duration: video.duration,
paused: video.paused,
});
lastReportedTitle = title;
}
function scanForVideo() {
const v = document.querySelector('video.vjs-tech, [data-testid="main-player"] video');
if (v) attachVideo(v);
}
const observer = new MutationObserver(() => {
dismissCookieBanner();
scanPrograms();
scanForVideo();
});
observer.observe(document.documentElement, { childList: true, subtree: true });
setInterval(() => {
dismissCookieBanner();
scanPrograms();
scanForVideo();
reportProgress();
}, PROGRESS_INTERVAL_MS);
window.__bridge = {
getPrograms() {
return JSON.stringify(knownTitles);
},
openByTitle(title) {
const nodes = document.querySelectorAll(CARD_TITLE_SELECTOR);
for (const n of nodes) {
if (n.textContent.trim() === title) {
const btn = n.closest('button');
if (btn) {
btn.click();
return true;
}
}
}
return false;
},
seek(seconds) {
if (video && isFinite(video.duration)) {
video.currentTime = Math.min(seconds, Math.max(video.duration - 1, 0));
} else {
pendingSeek = seconds;
}
},
play() {
if (video) video.play().catch(() => {});
},
pause() {
if (video) video.pause();
},
getState() {
return JSON.stringify({
hasVideo: !!video,
paused: video ? video.paused : null,
currentTime: video ? video.currentTime : null,
duration: video ? video.duration : null,
title: currentTitle(),
});
},
};
dismissCookieBanner();
scanPrograms();
scanForVideo();
})();