(function () { 'use strict'; const COUNTDOWN = 5; // seconds before auto-open const VIDEO_EXTS = /\.(mp4|mkv|avi|mov|wmv|flv|webm|m4v|mpg|mpeg|3gp|ts|m3u8)(\?.*)?$/i; // ── Exclusion list ───────────────────────────────────────────── const EXCLUDE = [ 'test-videos.co.uk', 'bigbuckbunny', 'sample-videos.com', 'w3schools.com', ]; function isExcluded(url) { return EXCLUDE.some(e => url.includes(e)); } function isVideoUrl(url) { return typeof url === 'string' && VIDEO_EXTS.test(url) && !isExcluded(url); } // ── URL registry ─────────────────────────────────────────────── const detected = new Map(); let currentUrl = null; let countdownTimer = null; let countdownVal = COUNTDOWN; // "idle" → no countdown running // "pending" → a video was found but the UI isn't built yet (start after buildUI) // "running" → countdown is visible and ticking let countdownState = 'idle'; let cancelled = false; function pickBest() { for (const [url] of detected) if (/\.mp4/i.test(url) && !url.includes('download')) return url; for (const [url] of detected) if (/\.mp4/i.test(url)) return url; for (const [url] of detected) if (/master\.m3u8/i.test(url)) return url; return detected.keys().next().value || null; } function register(url, label) { if (!url || !isVideoUrl(url) || detected.has(url)) return; detected.set(url, label); const best = pickBest(); if (best && best !== currentUrl) { currentUrl = best; // After the user cancels once, don't auto-interrupt again — but keep // currentUrl fresh so the manual fallback button opens the best source. if (cancelled) { updateFallbackLabel(); } else { startCountdown(); } } } // ── Open in player ───────────────────────────────────────────── // Use an anchor element + click() rather than reassigning location.href so // that, if no app can resolve the intent, the page stays put instead of // navigating to an invalid intent: URL (which would blank the page). function openInPlayer(url) { if (!url) return; const a = document.createElement('a'); a.href = `intent:${url}#Intent;` + `action=android.intent.action.VIEW;` + `type=video/*;` + `end`; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => a.remove(), 1000); } // ── XHR intercept ───────────────────────────────────────────── const OrigXHR = window.XMLHttpRequest; window.XMLHttpRequest = function () { const xhr = new OrigXHR(); const origOpen = xhr.open.bind(xhr); xhr.open = function (method, url) { try { if (isVideoUrl(url)) register(url, 'XHR'); } catch {} return origOpen.apply(xhr, arguments); }; return xhr; }; Object.defineProperties(window.XMLHttpRequest, Object.getOwnPropertyDescriptors(OrigXHR)); // ── fetch intercept ─────────────────────────────────────────── const origFetch = window.fetch; window.fetch = function (input) { try { const url = typeof input === 'string' ? input : (input && input.url); if (url && isVideoUrl(url)) register(url, 'fetch'); } catch {} return origFetch.apply(this, arguments); }; // ── JWPlayer hook ───────────────────────────────────────────── let jwHooked = false; function hookJWPlayer() { if (jwHooked || !window.jwplayer) return; jwHooked = true; const origJW = window.jwplayer; window.jwplayer = function () { const player = origJW.apply(this, arguments); if (player && player.setup) { const origSetup = player.setup.bind(player); player.setup = function (config) { try { extractFromJWConfig(config); } catch {} return origSetup(config); }; } return player; }; Object.assign(window.jwplayer, origJW); } function extractFromJWConfig(config) { if (!config) return; if (config.file) register(config.file, 'JWPlayer'); if (Array.isArray(config.sources)) config.sources.forEach(s => s && s.file && register(s.file, 'JWPlayer')); if (Array.isArray(config.playlist)) config.playlist.forEach(item => { if (item && item.file) register(item.file, 'JWPlayer'); if (Array.isArray(item.sources)) item.sources.forEach(s => s && s.file && register(s.file, 'JWPlayer')); }); } const jwPoll = setInterval(() => { if (window.jwplayer) { hookJWPlayer(); clearInterval(jwPoll); } }, 200); setTimeout(() => clearInterval(jwPoll), 15000); // ── video.src property trap ─────────────────────────────────── const origSrc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'src'); if (origSrc) { Object.defineProperty(HTMLMediaElement.prototype, 'src', { set(url) { try { if (isVideoUrl(url)) register(url, 'video.src'); } catch {} return origSrc.set.call(this, url); }, get() { return origSrc.get.call(this); }, configurable: true, }); } // ── Shadow DOM UI ────────────────────────────────────────────── const SHADOW_CSS = ` :host { all: initial; position: fixed; z-index: 2147483647; pointer-events: none; } /* Countdown banner — top centre */ #countdown-bar { position: fixed; top: 14px; left: 50%; transform: translateX(-50%); background: rgba(10,10,10,0.96); color: #fff; font-family: sans-serif; font-size: 14px; padding: 10px 18px; border-radius: 24px; border: 1px solid #333; box-shadow: 0 4px 16px rgba(0,0,0,0.8); display: flex; align-items: center; gap: 12px; pointer-events: all; opacity: 0; transition: opacity 0.3s; white-space: nowrap; } #countdown-bar.visible { opacity: 1; } #countdown-num { background: #e53935; color: #fff; font-weight: bold; font-size: 15px; border-radius: 50%; width: 28px; height: 28px; line-height: 28px; text-align: center; flex-shrink: 0; } #cancel-btn { background: rgba(255,255,255,0.12); color: #fff; border: 1px solid #555; border-radius: 12px; padding: 3px 12px; font-size: 12px; cursor: pointer; font-family: sans-serif; } #cancel-btn:active { background: rgba(255,255,255,0.25); } /* Fallback button — bottom right — shown after cancel */ #fallback-btn { position: fixed; bottom: 160px; right: 16px; display: flex; align-items: center; gap: 8px; background: rgba(15,15,15,0.95); color: #fff; font-family: sans-serif; font-size: 14px; font-weight: bold; padding: 10px 16px; border-radius: 24px; border: 1px solid #444; box-shadow: 0 4px 16px rgba(0,0,0,0.8); cursor: pointer; pointer-events: all; user-select: none; touch-action: none; opacity: 0; transform: scale(0.85); transition: opacity 0.3s, transform 0.3s; } #fallback-btn.visible { opacity: 1; transform: scale(1); } #fallback-btn:active { background: rgba(40,40,40,0.98); } .fb-icon { font-size: 20px; } .fb-sub { font-size: 10px; font-weight: normal; color: #aaa; display: block; } `; let host, shadow, countdownBar, countdownNum, cancelBtn, fallbackBtn; // Drag state for fallback button let isDragging = false, dragMoved = false, startX, startY; function buildUI() { host = document.createElement('div'); shadow = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = SHADOW_CSS; shadow.appendChild(style); // Countdown banner countdownBar = document.createElement('div'); countdownBar.id = 'countdown-bar'; countdownNum = document.createElement('div'); countdownNum.id = 'countdown-num'; countdownNum.textContent = COUNTDOWN; const label = document.createElement('span'); label.textContent = '📺 Opening in player…'; cancelBtn = document.createElement('div'); cancelBtn.id = 'cancel-btn'; cancelBtn.textContent = 'Cancel'; cancelBtn.addEventListener('pointerup', e => { e.stopPropagation(); cancel(); }); countdownBar.appendChild(countdownNum); countdownBar.appendChild(label); countdownBar.appendChild(cancelBtn); shadow.appendChild(countdownBar); // Fallback button (shown after cancel) fallbackBtn = document.createElement('div'); fallbackBtn.id = 'fallback-btn'; fallbackBtn.innerHTML = ` 📺 Open withChoose Player`; fallbackBtn.addEventListener('pointerdown', e => { isDragging = true; dragMoved = false; fallbackBtn.setPointerCapture(e.pointerId); const rect = fallbackBtn.getBoundingClientRect(); fallbackBtn.style.bottom = 'auto'; fallbackBtn.style.right = 'auto'; fallbackBtn.style.left = rect.left + 'px'; fallbackBtn.style.top = rect.top + 'px'; startX = e.clientX - rect.left; startY = e.clientY - rect.top; e.preventDefault(); }); fallbackBtn.addEventListener('pointermove', e => { if (!isDragging) return; dragMoved = true; const maxX = window.innerWidth - fallbackBtn.offsetWidth; const maxY = window.innerHeight - fallbackBtn.offsetHeight; fallbackBtn.style.left = Math.max(0, Math.min(e.clientX - startX, maxX)) + 'px'; fallbackBtn.style.top = Math.max(0, Math.min(e.clientY - startY, maxY)) + 'px'; e.preventDefault(); }); fallbackBtn.addEventListener('pointerup', () => { isDragging = false; if (!dragMoved && currentUrl) openInPlayer(currentUrl); }); shadow.appendChild(fallbackBtn); document.body.appendChild(host); } function startCountdown() { if (countdownState === 'running' || cancelled) return; // Defer until the UI exists; init() will resume once buildUI() finishes. if (!countdownBar) { countdownState = 'pending'; return; } if (countdownTimer) clearInterval(countdownTimer); countdownState = 'running'; countdownVal = COUNTDOWN; countdownBar.classList.add('visible'); countdownNum.textContent = countdownVal; countdownTimer = setInterval(() => { countdownVal--; countdownNum.textContent = countdownVal; if (countdownVal <= 0) { clearInterval(countdownTimer); countdownTimer = null; countdownBar.classList.remove('visible'); openInPlayer(currentUrl); } }, 1000); } function cancel() { cancelled = true; countdownState = 'idle'; if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; } countdownBar.classList.remove('visible'); // Show fallback button so user can still open manually. fallbackBtn.classList.add('visible'); updateFallbackLabel(); } function updateFallbackLabel() { if (!fallbackBtn) return; const text = fallbackBtn.querySelector('.fb-text'); if (!text || !currentUrl) return; // Surface which URL the button will open, so a later-found better source // is visible to the user after cancel. const short = currentUrl.length > 40 ? currentUrl.slice(0, 37) + '…' : currentUrl; text.innerHTML = `Open with${short}`; } // ── Also scan page URL and inline scripts ───────────────────── function checkPageUrl() { if (isVideoUrl(location.href)) register(location.href, 'Page URL'); } function scanInlineScripts() { const re = /['"`](https?:\/\/[^'"`\s]{10,}\.(?:mp4|mkv|m3u8|webm)[^'"`\s]*?)['"`]/gi; document.querySelectorAll('script:not([src])').forEach(s => { for (const m of s.textContent.matchAll(re)) register(m[1], 'Script'); }); } function checkVideoLinks() { document.querySelectorAll('a[href]').forEach(a => { if (isVideoUrl(a.href) && !a._owbAttached) { a._owbAttached = true; a.addEventListener('click', e => { e.preventDefault(); register(a.href, 'Link'); }); } }); } // Debounce the heavy DOM scans so a chatty SPA doesn't re-scan every // animation frame. We coalesce bursts of mutations into one pass. let scanQueued = false; function scheduleScan() { if (scanQueued) return; scanQueued = true; setTimeout(() => { scanQueued = false; checkVideoLinks(); }, 200); } // ── Init ─────────────────────────────────────────────────────── checkPageUrl(); function init() { buildUI(); // If a video showed up before the UI was built, resume the pending countdown. if (countdownState === 'pending') startCountdown(); checkVideoLinks(); scanInlineScripts(); setTimeout(() => { checkVideoLinks(); scanInlineScripts(); }, 2000); new MutationObserver(() => scheduleScan()) .observe(document.documentElement, { childList: true, subtree: true }); } if (document.body) { init(); } else { document.addEventListener('DOMContentLoaded', init); } })();