// ==UserScript==
// @name Mobile Redirect Video to External App
// @namespace http://tampermonkey.net/
// @version 4.7
// @description Detects video sources by injecting code to bypass player sandboxing and uses robust redirect methods.
// @author MasuRii
// @match *://*/*
// @grant GM_addStyle
// @grant unsafeWindow
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ===================================================================================
// === CONFIGURATION =================================================================
// ===================================================================================
const CONFIG = {
INTENT_TYPE: 'CHOOSER', // 'CHOOSER' or 'SPECIFIC_APP'
SPECIFIC_APP_PACKAGE: 'com.mxtech.videoplayer.pro',
BUTTON_TEXT: 'Open Externally',
DEBUG_MODE: true,
DEBUG_INSPECTOR: true
};
// ===================================================================================
// === END OF CONFIGURATION ==========================================================
// ===================================================================================
let redirectButton = null;
let linkChooserModal = null;
let inspectorButton = null;
const videoSources = new Set();
function log(message) {
if (CONFIG.DEBUG_MODE) {
console.log('[Video Redirector] ' + message);
}
}
function buildIntentUrl(videoUrl) {
if (!videoUrl) return null;
let intentUrl = `intent:${videoUrl}#Intent;action=android.intent.action.VIEW;type=video/*;launchFlags=0x10000000;`;
if (CONFIG.INTENT_TYPE === 'SPECIFIC_APP' && CONFIG.SPECIFIC_APP_PACKAGE) {
intentUrl += `package=${CONFIG.SPECIFIC_APP_PACKAGE};`;
}
intentUrl += 'end';
return intentUrl;
}
/**
* ===================================================================================
* === THE FIX IS HERE (v4.7) ========================================================
* ===================================================================================
* Issue: `window.location.href` is blocked by strict Content Security Policies (CSP).
* Solution: For the single-video case, we now use the more reliable method of
* creating a temporary link and programmatically clicking it. This is
* less likely to be blocked by CSP than a direct location change.
*/
function redirectToExternalApp(videoUrl) {
const intentUrl = buildIntentUrl(videoUrl);
if (intentUrl) {
log(`Attempting single-video redirect via programmatic click: ${intentUrl}`);
const link = document.createElement('a');
link.href = intentUrl;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
// ===================================================================================
// === END OF FIX ====================================================================
// ===================================================================================
function createLinkChooserModal() {
if (document.getElementById('video-redirect-modal')) return;
const modalContainer = document.createElement('div');
modalContainer.id = 'video-redirect-modal';
modalContainer.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h3>Select a Video Link</h3>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<p>Click a link to open in your external player:</p>
<ul id="video-link-list"></ul>
</div>
</div>
`;
document.body.appendChild(modalContainer);
linkChooserModal = modalContainer;
const closeModal = () => linkChooserModal.style.display = 'none';
modalContainer.querySelector('.modal-overlay').addEventListener('click', closeModal);
modalContainer.querySelector('.modal-close').addEventListener('click', closeModal);
}
function showLinkChooser() {
if (videoSources.size === 0) {
alert('No valid video links found on this page.');
return;
}
if (videoSources.size === 1) {
const singleUrl = videoSources.values().next().value;
redirectToExternalApp(singleUrl);
return;
}
const listElement = linkChooserModal.querySelector('#video-link-list');
listElement.innerHTML = '';
videoSources.forEach(url => {
const intentUrl = buildIntentUrl(url);
if (!intentUrl) return;
const listItem = document.createElement('li');
const link = document.createElement('a');
link.href = intentUrl;
link.textContent = url.length > 100 ? url.substring(0, 97) + '...' : url;
link.title = url;
link.addEventListener('click', () => {
linkChooserModal.style.display = 'none';
});
listItem.appendChild(link);
listElement.appendChild(listItem);
});
linkChooserModal.style.display = 'flex';
}
function createRedirectButton() {
if (document.getElementById('video-redirect-button')) return;
redirectButton = document.createElement('button');
redirectButton.id = 'video-redirect-button';
redirectButton.textContent = CONFIG.BUTTON_TEXT;
redirectButton.addEventListener('click', showLinkChooser);
document.body.appendChild(redirectButton);
}
function showRedirectButton() {
if (redirectButton && videoSources.size > 0) {
redirectButton.style.display = 'block';
}
}
function findAdvancedPlayerSources() {
document.addEventListener('VD_PlayerSourceFound', (e) => {
const url = e.detail.url;
if (url && !videoSources.has(url)) {
videoSources.add(url);
log(`Received player source via injection: ${url}`);
showRedirectButton();
}
});
const codeToInject = `
(function() {
if (typeof jwplayer !== 'function') return;
const playerDivs = document.querySelectorAll('div.jwplayer[id]');
playerDivs.forEach(div => {
try {
const player = jwplayer(div.id);
if (player && typeof player.getPlaylist === 'function') {
const playlist = player.getPlaylist();
playlist.forEach(item => {
const sources = (item.sources && Array.isArray(item.sources)) ? item.sources : [];
if (item.file) sources.push({ file: item.file });
sources.forEach(source => {
if (source.file && typeof source.file === 'string' && !source.file.startsWith('blob:')) {
document.dispatchEvent(new CustomEvent('VD_PlayerSourceFound', { detail: { url: source.file } }));
}
});
});
}
} catch (err) {}
});
})();
`;
try {
const script = document.createElement('script');
script.textContent = codeToInject;
(document.head || document.documentElement).appendChild(script);
script.remove();
log('Injected script to find player sources.');
} catch (e) {
log(`Error injecting script: ${e.message}`);
}
}
function handleVideoElement(video) {
const checkSrcAndStoreIt = () => {
const videoSrc = video.currentSrc;
if (videoSrc && !videoSrc.startsWith('blob:')) {
if (!videoSources.has(videoSrc)) {
videoSources.add(videoSrc);
log(`Found standard <video> source: ${videoSrc}`);
showRedirectButton();
}
}
};
if (video.readyState >= 1) {
checkSrcAndStoreIt();
} else {
video.addEventListener('loadedmetadata', checkSrcAndStoreIt, { once: true });
}
video.addEventListener('loadstart', () => setTimeout(checkSrcAndStoreIt, 100));
}
// --- Main Execution ---
// The rest of the script (inspector, styles, observer) remains the same.
// ... (omitted for brevity, it's identical to v4.6)
function createDebugInspector(){if(!CONFIG.DEBUG_INSPECTOR||document.getElementById("video-redirect-inspector")){return}inspectorButton=document.createElement("button");inspectorButton.id="video-redirect-inspector";inspectorButton.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5s-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></svg>';inspectorButton.title="Inspect Element for Debugging";inspectorButton.addEventListener("click",()=>{log("INSPECTOR MODE ACTIVATED. Click on or near the video player.");document.body.style.cursor="crosshair";document.body.addEventListener("click",inspectElement,{once:true,capture:true})});document.body.appendChild(inspectorButton)}function inspectElement(event){event.preventDefault();event.stopPropagation();document.body.style.cursor="default";let target=event.target;console.log("--- [Video Redirector] Inspector Report ---");console.log("User clicked on the element below. Analyzing its structure (up to 8 parents):");console.log("Please copy this report when asking for support for a new site.");for(let i=0;i<8&&target&&target.tagName!=="BODY";i++){const id=target.id?`#${target.id}`:"";const classes=target.className&&typeof target.className==="string"?`.${target.className.split(" ").join(".")}`:"";console.log(`[${i}] ${target.tagName}${id}${classes}`,target);target=target.parentElement}console.log("--- End of Report ---")}function addGlobalStyles(){GM_addStyle(`
#video-redirect-button {
position: fixed; bottom: 15px; left: 50%;
transform: translateX(-50%);
z-index: 2147483646;
padding: 8px 16px; font-size: 14px; color: white;
background-color: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px; cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
display: none; /* Initially hidden */
backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
#video-redirect-button:hover {
background-color: rgba(0, 0, 0, 0.8);
transform: translateX(-50%) scale(1.05);
}
#video-redirect-inspector {
position: fixed; bottom: 15px; right: 15px;
z-index: 2147483646;
width: 40px; height: 40px;
background-color: rgba(100, 100, 100, 0.5);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
transition: background-color 0.3s ease;
}
#video-redirect-inspector:hover {
background-color: rgba(120, 0, 0, 0.7);
}
#video-redirect-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
z-index: 2147483647;
display: none; align-items: center; justify-content: center;
}
#video-redirect-modal .modal-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7);
}
#video-redirect-modal .modal-content {
position: relative; z-index: 1;
background-color: #2c2c2c; color: #f1f1f1;
border-radius: 8px;
width: 90%; max-width: 600px;
max-height: 80vh;
display: flex; flex-direction: column;
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
}
#video-redirect-modal .modal-header {
padding: 15px; border-bottom: 1px solid #444;
display: flex; justify-content: space-between; align-items: center;
}
#video-redirect-modal .modal-header h3 { margin: 0; font-size: 1.2em; }
#video-redirect-modal .modal-close {
background: none; border: none; font-size: 1.8em;
color: #aaa; cursor: pointer; line-height: 1; padding: 0;
}
#video-redirect-modal .modal-close:hover { color: white; }
#video-redirect-modal .modal-body { padding: 15px; overflow-y: auto; }
#video-redirect-modal #video-link-list {
list-style: none; padding: 0; margin: 0;
}
#video-redirect-modal #video-link-list li {
margin-bottom: 10px;
}
#video-redirect-modal #video-link-list a {
display: block; padding: 10px;
background-color: #3a3a3a;
border-radius: 4px;
color: #aaddff; text-decoration: none;
word-break: break-all;
transition: background-color 0.2s ease;
}
#video-redirect-modal #video-link-list a:hover {
background-color: #4f4f4f;
}
`)}
addGlobalStyles();
createRedirectButton();
createLinkChooserModal();
createDebugInspector();
document.querySelectorAll('video').forEach(handleVideoElement);
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.tagName === "VIDEO") {
handleVideoElement(node);
} else if (node.querySelectorAll) {
node.querySelectorAll("video").forEach(handleVideoElement);
}
}
});
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(findAdvancedPlayerSources, 2500);
})();