Greasy Fork is available in English.

Stream To Android Player

Detect streaming videos and open them in external Android apps or copy to clipboard on desktop

Version vom 20.03.2025. Aktuellste Version

// ==UserScript==
// @name         Stream To Android Player
// @namespace    https://github.com/faisalbhuiyan
// @version      1.2
// @description  Detect streaming videos and open them in external Android apps or copy to clipboard on desktop
// @author       Faisal Bhuiyan
// @match        *://*/*
// @grant        none
// @license      MIT
// @run-at       document-start
// ==/UserScript==
 
(function () {
    'use strict';
 
    let streams = [];
    let originalXHROpen = XMLHttpRequest.prototype.open;
    let originalFetchOpen = window.fetch;
    let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
 
    // Inject CSS
    const style = document.createElement('style');
    style.textContent = `
        #video-handler-button {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 48px;
            height: 48px;
            background: none;
            color: white;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            z-index: 999999;
            font-size: 24px;
            transition: opacity 0.3s;
        }
 
        #video-handler-button:hover {
            opacity: 1;
        }
 
        #video-handler-menu {
            position: fixed;
            bottom: 80px;
            right: 20px;
            background: rgba(0, 0, 0, 0.9);
            border-radius: 8px;
            padding: 8px;
            z-index: 999998;
            max-height: 300px;
            overflow-y: auto;
            color: white;
            max-width: 300px;
            width: auto;
        }
 
        .stream-item {
            padding: 8px 16px;
            cursor: pointer;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }
 
        .stream-item:last-child {
            border-bottom: none;
        }
 
        .stream-item:hover {
            background: rgba(255, 255, 255, 0.1);
        }
        
        .copy-notification {
            position: fixed;
            bottom: 120px;
            right: 20px;
            background: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 8px 16px;
            border-radius: 4px;
            z-index: 999999;
            transition: opacity 0.3s;
        }
    `;
    document.head.appendChild(style);
 
    // Clear streams when page unloads
    window.addEventListener('beforeunload', () => {
        streams = [];
        const menu = document.getElementById('video-handler-menu');
        const button = document.getElementById('video-handler-button');
        if (menu) menu.remove();
        if (button) button.remove();
    });
 
    function createStreamMenu() {
        const menu = document.createElement('div');
        menu.id = 'video-handler-menu';
        menu.style.display = 'none';
        document.body.appendChild(menu);
        return menu;
    }
 
    function shareUrl(url) {
        if (isMobile && navigator.share) {
            navigator.share({
                url: url
            }).catch(console.error);
        } else {
            // Desktop: Copy to clipboard
            navigator.clipboard.writeText(url)
                .then(() => showNotification('Link copied to clipboard!'))
                .catch(err => {
                    console.error('Failed to copy: ', err);
                    // Fallback method for clipboard
                    const textarea = document.createElement('textarea');
                    textarea.value = url;
                    textarea.style.position = 'fixed';
                    textarea.style.opacity = '0';
                    document.body.appendChild(textarea);
                    textarea.focus();
                    textarea.select();
                    
                    try {
                        document.execCommand('copy');
                        showNotification('Link copied to clipboard!');
                    } catch (e) {
                        showNotification('Failed to copy link. Please copy it manually.');
                        console.error('Fallback copy failed:', e);
                    }
                    
                    document.body.removeChild(textarea);
                });
        }
    }
    
    function showNotification(message) {
        const notification = document.createElement('div');
        notification.className = 'copy-notification';
        notification.textContent = message;
        document.body.appendChild(notification);
        
        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                document.body.removeChild(notification);
            }, 300);
        }, 2000);
    }
 
    function updateMenu() {
        const menu = document.getElementById('video-handler-menu') || createStreamMenu();
        menu.innerHTML = streams.map(stream => `
            <div class="stream-item" data-url="${stream.url}" title="${stream.name}">
                ${stream.name}
            </div>
        `).join('');
 
        menu.querySelectorAll('.stream-item').forEach(item => {
            item.onclick = () => {
                shareUrl(item.dataset.url);
            };
        });
    }
 
    function createFloatingButton() {
        const button = document.createElement('div');
        button.id = 'video-handler-button';
        button.innerHTML = '▶️';
        button.style.display = 'none';
        document.body.appendChild(button);
 
        button.onclick = () => {
            const menu = document.getElementById('video-handler-menu');
            menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
        };
 
        return button;
    }
 
    // Helper functions for stream detection
    function isStreamUrl(url) {
        const videoPatterns = [
            '.mp4', '.m3u8', '.m3u', 'urlset',
            '/video/', '/media/', '/stream/',
            'manifest', 'playlist', 'master.json'
        ];
        return videoPatterns.some(pattern => url.toLowerCase().includes(pattern));
    }
 
    function isStreamContent(contentType) {
        const streamTypes = [
            'video/',
            'application/x-mpegurl',
            'application/vnd.apple.mpegurl',
            'application/octet-stream'
        ];
        return streamTypes.some(type => contentType?.toLowerCase().includes(type));
    }
 
    // Enhanced stream detection function
    function handleStreamDetection(url, contentType, content) {
        // Skip if already detected
        if (streams.some(s => s.url === url)) return;
 
        const fileName = url.split('/').pop().split('?')[0];
        let shouldAdd = false;
        let type = 'video';
 
        // Check URL patterns
        if (isStreamUrl(url)) {
            shouldAdd = true;
            if (url.includes('.m3u8')) type = 'm3u8';
        }
 
        // Check content type
        if (isStreamContent(contentType)) {
            shouldAdd = true;
            if (contentType?.includes('mpegurl')) type = 'm3u8';
        }
 
        // Check for M3U8 content
        if (content?.trim().startsWith('#EXTM3U')) {
            shouldAdd = true;
            type = 'm3u8';
        }
 
        if (shouldAdd) {
            streams.push({ url, name: fileName, type });
            const button = document.getElementById('video-handler-button') || createFloatingButton();
            button.style.display = 'block';
            updateMenu();
        }
    }
 
    // Extract video sources from the page
    function extractVideoSources() {
        // Check video elements
        document.querySelectorAll('video').forEach(video => {
            if (video.src) {
                handleStreamDetection(video.src);
            }
 
            // Check source elements within video
            video.querySelectorAll('source').forEach(source => {
                if (source.src) {
                    handleStreamDetection(source.src, source.type);
                }
            });
        });
 
        // Check data-source attributes
        document.querySelectorAll('[data-source]').forEach(el => {
            const source = el.getAttribute('data-source');
            if (source) {
                handleStreamDetection(source);
            }
        });
    }
 
    // Monitor XHR requests with enhanced detection
    XMLHttpRequest.prototype.open = function () {
        this.addEventListener('load', function () {
            try {
                const contentType = this.getResponseHeader('content-type');
                handleStreamDetection(
                    this.responseURL,
                    contentType,
                    this.responseText
                );
            } catch (e) {
                handleStreamDetection(this.responseURL);
            }
        });
        originalXHROpen.apply(this, arguments);
    };
 
    // Monitor fetch requests with enhanced detection
    window.fetch = async function () {
        const url = arguments[0]?.url || arguments[0];
        const response = await originalFetchOpen.apply(this, arguments);
 
        try {
            const contentType = response.headers.get('content-type');
            const clonedResponse = response.clone();
            const content = await clonedResponse.text();
            handleStreamDetection(url, contentType, content);
        } catch (e) {
            handleStreamDetection(url);
        }
 
        return response;
    };
 
    // Periodic scanning for dynamically added videos
    setInterval(extractVideoSources, 5000);
 
    // Monitor DOM changes for new video elements
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeName === 'VIDEO' || node.nodeName === 'AUDIO') {
                    handleStreamDetection(node.src);
                }
                // Check for video elements within added nodes
                if (node.querySelectorAll) {
                    extractVideoSources();
                }
            });
        });
    });
 
    // Start observing once DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            observer.observe(document.documentElement, {
                childList: true,
                subtree: true
            });
            extractVideoSources();
        });
    } else {
        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
        extractVideoSources();
    }
})();