Initial
This commit is contained in:
157
assembly_player/bridge.js
Normal file
157
assembly_player/bridge.js
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user