Greasy Fork

Greasy Fork is available in English.

Universal Video Share Button with M3U8 Support + MP4 Remux (Twitter/X Enhanced)

Share button with M3U8 support. Downloads and converts to MP4. Includes special support for Twitter/X (inline buttons).

当前为 2025-12-17 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Universal Video Share Button with M3U8 Support + MP4 Remux (Twitter/X Enhanced)
// @namespace    http://tampermonkey.net/
// @version      7.2
// @description  Share button with M3U8 support. Downloads and converts to MP4. Includes special support for Twitter/X (inline buttons).
// @author       Minoa & Azuki (Inspiration)
// @license      MIT
// @match        *://*/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/m3u8-parser.min.js
// @require      https://cdn.jsdelivr.net/npm/@warren-bank/[email protected]/dist/umd/ffmpeg.js
// @resource     classWorkerURL  https://cdn.jsdelivr.net/npm/@warren-bank/[email protected]/dist/umd/258.ffmpeg.js
// @resource     coreURL         https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/umd/ffmpeg-core.js
// @resource     wasmURL         https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/umd/ffmpeg-core.wasm
// @grant        GM_addStyle
// @grant        GM_getResourceURL
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // CONFIGURATION & STATE
    // ==========================================
    var floatingButton = null;
    var pressTimer = null;
    var isLongPress = false;
    var checkInterval = null;
    var detectedM3U8s = [];
    var detectedM3U8Urls = [];
    var allDetectedVideos = new Map();
    var downloadedBlobs = new Map();
    var debugMode = false;
    var debugConsole = null;
    var debugLogs = [];
    var longPressStartTime = 0;
    var ffmpegInstance = null;
    var ffmpegLoaded = false;
    var wasmBinaryCache = null;

    // Platform Detection
    const isTwitter = location.hostname.includes('twitter.com') || location.hostname.includes('x.com');

    // Color scheme
    const COLORS = {
        button: 'rgba(85, 66, 61, 0.7)',
        buttonHover: 'rgba(107, 86, 81, 0.85)',
        icon: '#ffc0ad',
        text: '#fff3ec',
        twitter: '#1d9bf0'
    };

    // ==========================================
    // STYLES
    // ==========================================
    GM_addStyle(`
        /* Universal Floating Button */
        #universal-video-share-container {
            position: fixed !important;
            top: 15px !important;
            left: 15px !important;
            width: 50px !important;
            height: 50px !important;
            z-index: 2147483647 !important;
            pointer-events: auto !important;
            isolation: isolate !important;
        }
        #universal-video-share-float {
            position: absolute !important;
            top: 2px !important;
            left: 2px !important;
            width: 46px !important;
            height: 46px !important;
            background: ${COLORS.button} !important;
            backdrop-filter: blur(12px) !important;
            -webkit-backdrop-filter: blur(12px) !important;
            color: ${COLORS.icon} !important;
            border: 2px solid rgba(255, 255, 255, 0.3) !important;
            border-radius: 50% !important;
            font-size: 18px !important;
            cursor: pointer !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            transition: all 0.2s ease !important;
            user-select: none !important;
            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4) !important;
            z-index: 2147483647 !important;
        }
        #universal-video-share-float:hover {
            background: ${COLORS.buttonHover} !important;
            transform: scale(1.1) !important;
            box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5) !important;
        }
        #progress-circle { pointer-events: none !important; }
        .universal-video-notification { z-index: 2147483646 !important; }
        #video-selector-popup { z-index: 2147483645 !important; }
        #debug-console { z-index: 2147483644 !important; }

        /* Twitter Specific Styles */
        .uvs-twitter-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 34px;
            height: 34px;
            border-radius: 9999px;
            transition: background-color 0.2s;
            cursor: pointer;
            color: rgb(113, 118, 123); /* Twitter Gray */
            margin-left: 2px;
        }
        .uvs-twitter-btn:hover {
            background-color: rgba(29, 155, 240, 0.1);
            color: ${COLORS.twitter};
        }
        .uvs-twitter-btn svg {
            width: 20px;
            height: 20px;
            fill: currentColor;
        }
        /* Dark mode adjustment if needed */
        @media (prefers-color-scheme: dark) {
            .uvs-twitter-btn { color: rgb(113, 118, 123); }
            .uvs-twitter-btn:hover { color: ${COLORS.twitter}; }
        }
    `);

    // ==========================================
    // HELPERS
    // ==========================================
    var VIDEO_SELECTORS = [
        'video', '.video-player video', '.player video', '#player video',
        '.video-container video', '[class*="video"] video', '[class*="player"] video',
        'iframe[src*="youtube.com"]', 'iframe[src*="vimeo.com"]',
        'iframe[src*="dailymotion.com"]', 'iframe[src*="twitch.tv"]'
    ];

    var VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv', '.m4v', '.3gp'];

    function sanitizeFilename(filename) {
        return filename.replace(/[<>:"\/\\|?*\x00-\x1F]/g, '').replace(/\s+/g, '_').replace(/_{2,}/g, '_').replace(/^\.+/, '').substring(0, 200);
    }

    function getFilenameFromPageTitle(extension = 'mp4', prefix = '') {
        const pageTitle = document.title || 'video';
        const sanitized = sanitizeFilename(pageTitle);
        return (prefix ? prefix + '-' : '') + sanitized + '.' + extension;
    }

    // ==========================================
    // FFMPEG & CONVERSION
    // ==========================================
    const getWasmBinary = async () => {
        if (wasmBinaryCache) return wasmBinaryCache.slice(0);
        const wasmURL = GM_getResourceURL('wasmURL', false);
        let wasmBinary = null;
        if (wasmURL.startsWith('data:')) {
            const index = wasmURL.indexOf(',');
            const base64 = wasmURL.substring(index + 1);
            const binaryString = atob(base64);
            const bytes = new Uint8Array(binaryString.length);
            for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
            wasmBinary = bytes.buffer;
        } else if (wasmURL.startsWith('blob:')) {
            wasmBinary = await fetch(wasmURL).then(res => res.arrayBuffer());
        }
        if (wasmBinary) wasmBinaryCache = wasmBinary.slice(0);
        return wasmBinary;
    };

    const getFFmpegLoadConfig = async () => ({
        classWorkerURL: GM_getResourceURL('classWorkerURL', false),
        coreURL: GM_getResourceURL('coreURL', false),
        wasmBinary: await getWasmBinary(),
        createTrustedTypePolicy: true
    });

    async function initFFmpeg() {
        if (ffmpegLoaded && ffmpegInstance) return ffmpegInstance;
        showNotification('⚙️ Loading FFmpeg...', 'info');
        try {
            ffmpegInstance = new window.FFmpegWASM.FFmpeg();
            ffmpegInstance.on('log', ({ message }) => debugLog('[FFMPEG LOG] ' + message));
            ffmpegInstance.on('progress', ({ progress }) => {
                const percent = Math.round(progress * 100);
                updateProgress(percent);
            });
            await ffmpegInstance.load(await getFFmpegLoadConfig());
            ffmpegLoaded = true;
            showNotification('✅ FFmpeg loaded', 'success');
            return ffmpegInstance;
        } catch(e) {
            showNotification('❌ FFmpeg failed to load', 'error');
            throw e;
        }
    }

    async function convertTStoMP4(tsBlob, baseFilename) {
        try {
            const ffmpeg = await initFFmpeg();
            const inputName = 'input.ts';
            const outputName = baseFilename.endsWith('.mp4') ? baseFilename : baseFilename + '.mp4';
            const inputData = new Uint8Array(await tsBlob.arrayBuffer());
            
            showNotification('🔄 Converting...', 'info');
            await ffmpeg.writeFile(inputName, inputData);
            await ffmpeg.exec(['-i', inputName, '-c', 'copy', '-movflags', 'faststart', outputName]);
            
            const data = await ffmpeg.readFile(outputName, 'binary');
            await ffmpeg.deleteFile(inputName);
            await ffmpeg.deleteFile(outputName);
            
            return { blob: new Blob([data.buffer], { type: 'video/mp4' }), filename: outputName };
        } catch(e) {
            showNotification('❌ Conversion failed', 'error');
            throw e;
        }
    }

    // ==========================================
    // NETWORK INTERCEPTION & DETECTION
    // ==========================================
    function isM3U8Url(url) {
        if (!url) return false;
        const lowerUrl = url.toLowerCase();
        return lowerUrl.includes('.m3u8') || lowerUrl.includes('.m3u');
    }

    function isBlobUrl(url) {
        return url && url.startsWith('blob:');
    }

    (function setupNetworkDetection() {
        const originalFetch = window.fetch;
        window.fetch = function(...args) {
            const promise = originalFetch.apply(this, args);
            const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
            promise.then(response => {
                if (url) {
                    if (isM3U8Url(url)) detectM3U8(url);
                    else if (!url.match(/seg-\d+-.*\.ts/i) && !url.endsWith('.ts') && !isBlobUrl(url)) checkUrlForVideo(url);
                }
                return response;
            }).catch(e => { throw e; });
            return promise;
        };

        const originalOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(...args) {
            this.addEventListener("load", function() {
                try {
                    const url = args[1];
                    if (url) {
                        if (isM3U8Url(url)) detectM3U8(url);
                        if (this.responseText && this.responseText.trim().startsWith("#EXTM3U")) detectM3U8(url);
                    }
                } catch(e) {}
            });
            return originalOpen.apply(this, args);
        };
    })();

    function checkUrlForVideo(url) {
        try {
            if (isBlobUrl(url) || url.match(/seg-\d+-.*\.ts/i) || url.endsWith('.ts') || isM3U8Url(url)) return;
            const lowerUrl = url.toLowerCase();
            if (VIDEO_EXTENSIONS.some(ext => lowerUrl.includes(ext))) {
                const fullUrl = new URL(url, location.href).href;
                if (!allDetectedVideos.has(fullUrl)) {
                    allDetectedVideos.set(fullUrl, {
                        url: fullUrl,
                        type: 'video',
                        timestamp: Date.now(),
                        title: 'Video - ' + fullUrl.split('/').pop()
                    });
                    checkForVideos();
                }
            }
        } catch(e) {}
    }

    async function detectM3U8(url) {
        try {
            if (isBlobUrl(url)) return;
            url = new URL(url, location.href).href;
            const urlWithoutQuery = url.split('?')[0];

            // Avoid duplicates
            if (detectedM3U8Urls.includes(url)) return;
            
            // On Twitter, we often get multiple resolutions. We want to capture them but maybe group them later.
            // For now, let's capture everything.
            
            debugLog('[M3U8] Detected: ' + url);
            detectedM3U8Urls.push(url);

            const response = await fetch(url);
            const content = await response.text();
            const parser = new m3u8Parser.Parser();
            parser.push(content);
            parser.end();
            const manifest = parser.manifest;

            // Handle Master Playlist (Variants)
            if (manifest.playlists && manifest.playlists.length > 0) {
                debugLog('[M3U8] Master playlist found. Fetching best variant.');
                const bestVariant = manifest.playlists.sort((a, b) => (b.attributes.BANDWIDTH || 0) - (a.attributes.BANDWIDTH || 0))[0];
                const baseUrl = url.substring(0, url.lastIndexOf('/') + 1);
                const variantUrl = bestVariant.uri.startsWith('http') ? bestVariant.uri : baseUrl + bestVariant.uri;
                detectM3U8(variantUrl); // Recursively fetch best variant
                return;
            }

            let duration = 0;
            if (manifest.segments) manifest.segments.forEach(s => duration += s.duration);

            const m3u8Data = {
                url: url,
                manifest: manifest,
                duration: duration,
                title: 'M3U8 Stream ' + (duration ? Math.ceil(duration) + 's' : ''),
                timestamp: Date.now()
            };

            detectedM3U8s.push(m3u8Data);
            allDetectedVideos.set(url, {
                url: url,
                type: 'm3u8',
                timestamp: Date.now(),
                title: m3u8Data.title,
                m3u8Data: m3u8Data
            });
            checkForVideos();
        } catch(e) {
            debugLog('[ERROR] M3U8 parse: ' + e.message);
        }
    }

    // ==========================================
    // VIDEO COLLECTION LOGIC
    // ==========================================
    function getUniqueVideos() {
        var videos = [];
        var seenUrls = new Set();

        // 1. Add captured M3U8s and Files from Network
        allDetectedVideos.forEach(function(videoData, url) {
            if (!seenUrls.has(url)) {
                seenUrls.add(url);
                videos.push(videoData);
            }
        });

        // 2. Scan DOM for video elements
        // On Twitter, videos are often Blob URLs. We want to include them in the count/list
        // so the user knows something is there, even if we have to match it to a network M3U8 later.
        var domElements = document.querySelectorAll('video, iframe');
        
        domElements.forEach(element => {
            var rect = element.getBoundingClientRect();
            if (rect.width > 50 && rect.height > 50) { // Lower threshold for Twitter previews
                var src = element.currentSrc || element.src;
                
                // Special handling for Twitter Blobs
                if (isTwitter && isBlobUrl(src)) {
                    // We don't add the blob URL directly to the download list because we can't download blobs easily
                    // without XHR, but we want to ensure the button appears.
                    // The actual download will use the captured M3U8s.
                    // We check if we have any M3U8s captured.
                    if (allDetectedVideos.size === 0) {
                        // If we see a video but haven't caught the M3U8 yet, we can add a placeholder
                        // or just rely on the network interceptor catching up.
                    }
                    return; 
                }

                if (src && !isBlobUrl(src) && !seenUrls.has(src)) {
                    seenUrls.add(src);
                    videos.push({
                        type: 'video',
                        element: element,
                        url: src,
                        title: document.title,
                        timestamp: Date.now()
                    });
                }
            }
        });

        // Sort: M3U8s first, then by timestamp
        return videos.sort((a, b) => {
            if (a.type === 'm3u8' && b.type !== 'm3u8') return -1;
            if (a.type !== 'm3u8' && b.type === 'm3u8') return 1;
            return b.timestamp - a.timestamp;
        });
    }

    // ==========================================
    // TWITTER SPECIFIC MODULE
    // ==========================================
    function initTwitterSupport() {
        if (!isTwitter) return;
        debugLog('[TWITTER] Initializing Twitter module...');

        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.addedNodes.length) {
                    processTwitterTweets();
                }
            });
        });

        observer.observe(document.body, { childList: true, subtree: true });
        processTwitterTweets();
    }

    function processTwitterTweets() {
        // Find all tweets
        const tweets = document.querySelectorAll('article[data-testid="tweet"]');
        
        tweets.forEach(tweet => {
            // Find the action bar (Reply, Retweet, Like, Share...)
            const actionBar = tweet.querySelector('div[role="group"]');
            if (!actionBar) return;

            // Check if we already injected
            if (actionBar.querySelector('.uvs-twitter-btn')) return;

            // Create Button
            const btn = document.createElement('div');
            btn.className = 'uvs-twitter-btn';
            btn.title = 'Download Media';
            btn.innerHTML = `
                <svg viewBox="0 0 24 24" aria-hidden="true">
                    <g><path d="M12 16 17.7 10.3 16.29 8.88 13 12.18 V2.59 h-2 v9.59 L7.7 8.88 6.29 10.3 Z M21 15l-.02 3.51c0 1.38-1.12 2.49-2.5 2.49H5.5C4.11 21 3 19.88 3 18.5V15h2v3.5c0 .28.22.5.5.5h12.98c.28 0 .5-.22.5-.5L19 15h2z"></path></g>
                </svg>
            `;

            // Insert before the share button (usually the last one) or at the end
            const shareBtn = actionBar.lastElementChild;
            if (shareBtn) {
                actionBar.insertBefore(btn, shareBtn);
            } else {
                actionBar.appendChild(btn);
            }

            // Click Handler
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                handleTwitterDownload(tweet);
            });
        });
    }

    function handleTwitterDownload(tweetElement) {
        // 1. Identify the video in this tweet
        const videoElement = tweetElement.querySelector('video');
        if (!videoElement) {
            showNotification('❌ No video found in this tweet', 'error');
            return;
        }

        // 2. Try to match with captured M3U8s
        // Since Twitter uses Blobs for src, we can't match URL directly.
        // We assume the most recently captured M3U8s are relevant, or we show the selector.
        
        const videos = getUniqueVideos();
        
        // Filter videos to find M3U8s
        const m3u8s = videos.filter(v => v.type === 'm3u8');

        if (m3u8s.length === 0) {
            showNotification('⏳ Video stream not captured yet. Play the video first!', 'info');
            return;
        }

        // If only one M3U8 captured, assume it's the one.
        if (m3u8s.length === 1) {
            downloadM3U8(m3u8s[0], true, getTwitterFilename(tweetElement));
        } else {
            // If multiple, show selector but try to be smart
            showVideoSelector(m3u8s, 'download', getTwitterFilename(tweetElement));
        }
    }

    function getTwitterFilename(tweetElement) {
        try {
            const timeEl = tweetElement.querySelector('time');
            const userEl = tweetElement.querySelector('div[data-testid="User-Name"]');
            
            let datePart = timeEl ? timeEl.getAttribute('datetime').split('T')[0] : 'twitter-video';
            let userPart = userEl ? userEl.innerText.split('\n')[0].replace('@', '') : 'user';
            
            return sanitizeFilename(`${userPart}_${datePart}`);
        } catch(e) {
            return 'twitter_video';
        }
    }

    // ==========================================
    // UI & INTERACTION
    // ==========================================
    function createFloatingButton() {
        if (floatingButton) return floatingButton;
        
        const container = document.createElement('div');
        container.id = 'universal-video-share-container';
        
        // SVG Progress Circle
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', '50');
        svg.setAttribute('height', '50');
        svg.style.cssText = 'position: absolute; top: 0; left: 0; transform: rotate(-90deg); pointer-events: none;';
        
        const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        circle.setAttribute('cx', '25');
        circle.setAttribute('cy', '25');
        circle.setAttribute('r', '22');
        circle.setAttribute('fill', 'none');
        circle.setAttribute('stroke', '#4ade80');
        circle.setAttribute('stroke-width', '3');
        circle.setAttribute('stroke-dasharray', '138');
        circle.setAttribute('stroke-dashoffset', '138');
        circle.id = 'progress-circle';
        
        svg.appendChild(circle);
        container.appendChild(svg);

        floatingButton = document.createElement('div');
        floatingButton.innerHTML = '▶';
        floatingButton.id = 'universal-video-share-float';
        floatingButton.progressCircle = circle;

        // Events
        const startPress = () => {
            isLongPress = false;
            longPressStartTime = Date.now();
            pressTimer = setTimeout(() => {
                isLongPress = true;
                if (Date.now() - longPressStartTime >= 5000) {
                    debugMode = !debugMode;
                    showNotification(`Debug Mode: ${debugMode ? 'ON' : 'OFF'}`, 'info');
                }
            }, 500);
        };

        const endPress = (e) => {
            e.preventDefault();
            clearTimeout(pressTimer);
            if (!isLongPress) handleShare();
        };

        floatingButton.addEventListener('mousedown', startPress);
        floatingButton.addEventListener('mouseup', endPress);
        floatingButton.addEventListener('touchstart', startPress);
        floatingButton.addEventListener('touchend', endPress);

        container.appendChild(floatingButton);
        document.body.appendChild(container);
        return floatingButton;
    }

    function updateProgress(percent) {
        if (!floatingButton || !floatingButton.progressCircle) return;
        floatingButton.progressCircle.setAttribute('stroke-dashoffset', 138 - (138 * percent / 100));
    }

    function handleShare() {
        const videos = getUniqueVideos();
        if (videos.length === 0) return showNotification('❌ No videos found', 'error');
        if (videos.length === 1) {
            const v = videos[0];
            if (v.type === 'm3u8') downloadM3U8(v, false);
            else shareVideo(v);
        } else {
            showVideoSelector(videos, 'share');
        }
    }

    function showVideoSelector(videos, action, customFilenamePrefix = null) {
        document.getElementById('video-selector-popup')?.remove();

        const popup = document.createElement('div');
        popup.id = 'video-selector-popup';
        popup.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center;';
        
        const content = document.createElement('div');
        content.style.cssText = 'background: #222; border: 1px solid #444; border-radius: 12px; padding: 20px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; color: #fff;';
        
        const title = document.createElement('h3');
        title.textContent = `Select Video (${videos.length})`;
        title.style.marginTop = '0';
        content.appendChild(title);

        videos.forEach(v => {
            const item = document.createElement('div');
            item.style.cssText = 'padding: 10px; border-bottom: 1px solid #333; cursor: pointer; display: flex; justify-content: space-between; align-items: center;';
            item.innerHTML = `
                <div>
                    <div style="font-weight:bold; font-size: 14px;">${v.type.toUpperCase()}</div>
                    <div style="font-size: 11px; opacity: 0.7;">${v.title}</div>
                </div>
                <div style="font-size: 20px;">${v.type === 'm3u8' ? '📥' : '🔗'}</div>
            `;
            item.onmouseover = () => item.style.background = '#333';
            item.onmouseout = () => item.style.background = 'transparent';
            item.onclick = () => {
                popup.remove();
                if (v.type === 'm3u8') downloadM3U8(v, true, customFilenamePrefix);
                else if (action === 'share') shareVideo(v);
                else copyVideoUrl(v);
            };
            content.appendChild(item);
        });

        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'Close';
        closeBtn.style.cssText = 'margin-top: 15px; width: 100%; padding: 8px; background: #444; border: none; color: white; border-radius: 6px; cursor: pointer;';
        closeBtn.onclick = () => popup.remove();
        content.appendChild(closeBtn);

        popup.appendChild(content);
        document.body.appendChild(popup);
    }

    // ==========================================
    // DOWNLOAD & SHARE LOGIC
    // ==========================================
    async function downloadM3U8(videoData, forceDownload = false, customFilenamePrefix = null) {
        const cached = downloadedBlobs.get(videoData.url);
        if (cached) return shareOrDownloadBlob(cached.blob, cached.filename);

        showNotification('📥 Downloading segments...', 'info');
        updateProgress(0);

        try {
            const manifest = videoData.m3u8Data.manifest;
            const baseUrl = videoData.url.substring(0, videoData.url.lastIndexOf('/') + 1);
            const segments = manifest.segments;
            
            if (!segments || !segments.length) throw new Error("No segments found");

            const segmentData = [];
            let totalSize = 0;

            for (let i = 0; i < segments.length; i++) {
                const segUrl = segments[i].uri.startsWith('http') ? segments[i].uri : baseUrl + segments[i].uri;
                const res = await fetch(segUrl);
                const data = await res.arrayBuffer();
                segmentData.push(data);
                totalSize += data.byteLength;
                updateProgress(Math.floor(((i + 1) / segments.length) * 100));
            }

            const merged = new Blob(segmentData, { type: 'video/mp2t' });
            const filename = getFilenameFromPageTitle('mp4', customFilenamePrefix);
            
            const converted = await convertTStoMP4(merged, filename);
            downloadedBlobs.set(videoData.url, converted);
            
            shareOrDownloadBlob(converted.blob, converted.filename);
        } catch(e) {
            showNotification('❌ Error: ' + e.message, 'error');
            updateProgress(0);
        }
    }

    async function shareOrDownloadBlob(blob, filename) {
        if (navigator.share && navigator.canShare && navigator.canShare({ files: [new File([blob], filename, { type: 'video/mp4' })] })) {
            try {
                await navigator.share({
                    files: [new File([blob], filename, { type: 'video/mp4' })],
                    title: filename
                });
                showNotification('✅ Shared!', 'success');
                return;
            } catch(e) { /* Fallback to download */ }
        }
        
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(a.href); }, 1000);
        showNotification('✅ Downloaded', 'success');
        updateProgress(0);
    }

    function shareVideo(videoData) {
        if (navigator.share) {
            navigator.share({ title: document.title, url: videoData.url }).catch(() => copyVideoUrl(videoData));
        } else {
            copyVideoUrl(videoData);
        }
    }

    function copyVideoUrl(videoData) {
        navigator.clipboard.writeText(videoData.url).then(() => showNotification('✅ URL Copied', 'success'));
    }

    function showNotification(msg, type) {
        const div = document.createElement('div');
        div.className = 'universal-video-notification';
        div.textContent = msg;
        div.style.cssText = `
            position: fixed; top: 75px; left: 15px; 
            background: ${type === 'error' ? 'rgba(239,68,68,0.9)' : 'rgba(74,222,128,0.9)'};
            color: white; padding: 10px 14px; border-radius: 8px; 
            font-weight: bold; font-size: 13px; backdrop-filter: blur(5px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
        `;
        document.body.appendChild(div);
        setTimeout(() => div.remove(), 3000);
    }

    function debugLog(msg) {
        if (debugMode) console.log(msg);
    }

    function checkForVideos() {
        const videos = getUniqueVideos();
        if (videos.length > 0) {
            const btn = createFloatingButton();
            btn.innerHTML = videos.length > 1 ? '⇓' : (videos[0].type === 'm3u8' ? '⇣' : '↯');
        } else if (floatingButton) {
            floatingButton.innerHTML = '▶';
        }
    }

    // ==========================================
    // INIT
    // ==========================================
    function init() {
        debugLog('[INIT] Universal Video Downloader v7.0');
        setTimeout(checkForVideos, 1000);
        checkInterval = setInterval(checkForVideos, 2000);
        
        if (isTwitter) {
            initTwitterSupport();
        }
    }

    init();

})();