158 lines
4.5 KiB
JavaScript
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();
|
|
})();
|