Universal Video Share Button with M3U8 Support

Adds a floating share button that appears when videos are detected. Hold down to copy instead of share. Auto-detects and downloads M3U8 playlists.

目前為 2025-11-06 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Universal Video Share Button with M3U8 Support
// @namespace    http://tampermonkey.net/
// @version      4.5
// @description  Adds a floating share button that appears when videos are detected. Hold down to copy instead of share. Auto-detects and downloads M3U8 playlists.
// @author       Minoa
// @license MIT
// @match        *://*/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/m3u8-parser.min.js
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    var floatingButton = null;
    var pressTimer = null;
    var isLongPress = false;
    var checkInterval = null;
    var detectedM3U8s = [];
    var detectedM3U8Urls = [];
    var allDetectedVideos = new Map();
    var processedVideos = new Map(); // Track what was done with each video
    var downloadedBlobs = new Map(); // Store downloaded M3U8 blobs

    // Color scheme
    const COLORS = {
        button: '#55423d',
        buttonHover: '#6b5651',
        icon: '#ffc0ad',
        text: '#fff3ec'
    };

    // Common video selectors across different sites
    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"]'
    ];

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

    // M3U8 Detection - Hook into network requests
    (function setupNetworkDetection() {
        // Hook fetch
        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) {
                    // Don't track .ts segments
                    if (!url.match(/seg-\d+-.*\.ts/i) && !url.endsWith('.ts')) {
                        checkUrlForVideo(url);
                    }
                    if (url.includes('.m3u8') || url.includes('.m3u')) {
                        detectM3U8(url);
                    }
                }
                return response;
            }).catch(e => {
                throw e;
            });
            return promise;
        };

        // Hook XMLHttpRequest
        const originalOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(...args) {
            this.addEventListener("load", function() {
                try {
                    const url = args[1];
                    if (url) {
                        // Don't track .ts segments
                        if (!url.match(/seg-\d+-.*\.ts/i) && !url.endsWith('.ts')) {
                            checkUrlForVideo(url);
                        }
                        if (url.includes('.m3u8') || url.includes('.m3u')) {
                            detectM3U8(url);
                        }
                        if (this.responseText && this.responseText.trim().startsWith("#EXTM3U")) {
                            detectM3U8(url);
                        }
                    }
                } catch(e) {}
            });
            return originalOpen.apply(this, args);
        };

        // Hook Response.text() to catch m3u8 content
        const originalText = Response.prototype.text;
        Response.prototype.text = function() {
            return originalText.call(this).then(text => {
                if (text.trim().startsWith("#EXTM3U")) {
                    detectM3U8(this.url);
                }
                return text;
            });
        };

        // Hook video element src changes
        const originalSetAttribute = Element.prototype.setAttribute;
        Element.prototype.setAttribute = function(name, value) {
            if (this.tagName === 'VIDEO' || this.tagName === 'SOURCE') {
                if (name === 'src' && value) {
                    checkUrlForVideo(value);
                }
            }
            return originalSetAttribute.call(this, name, value);
        };
    })();

    function checkUrlForVideo(url) {
        try {
            // Skip blob URLs and .ts segments
            if (url.startsWith('blob:')) return;
            if (url.match(/seg-\d+-.*\.ts/i)) return;
            if (url.endsWith('.ts')) return;

            // Skip if it's already an M3U8 (will be handled separately)
            if (url.includes('.m3u8') || url.includes('.m3u')) return;

            // Check if it's a video URL
            const lowerUrl = url.toLowerCase();
            const isVideo = VIDEO_EXTENSIONS.some(ext => lowerUrl.includes(ext));

            if (isVideo) {
                const fullUrl = new URL(url, location.href).href;
                if (!allDetectedVideos.has(fullUrl)) {
                    allDetectedVideos.set(fullUrl, {
                        url: fullUrl,
                        type: 'video',
                        timestamp: Date.now(),
                        title: 'Video - ' + getFilenameFromUrl(fullUrl)
                    });
                    checkForVideos();
                }
            }
        } catch(e) {}
    }

    function getFilenameFromUrl(url) {
        try {
            const pathname = new URL(url).pathname;
            const filename = pathname.split('/').pop();
            return filename || 'Unknown';
        } catch(e) {
            return 'Unknown';
        }
    }

    function getTimeAgo(timestamp) {
        const seconds = Math.floor((Date.now() - timestamp) / 1000);
        if (seconds < 60) return 'just now';
        const minutes = Math.floor(seconds / 60);
        if (minutes < 60) return minutes + 'm ago';
        const hours = Math.floor(minutes / 60);
        if (hours < 24) return hours + 'h ago';
        const days = Math.floor(hours / 24);
        return days + 'd ago';
    }

    function getBaseUrlFromSegment(segmentUrl) {
        try {
            const url = new URL(segmentUrl);
            const path = url.pathname.substring(0, url.pathname.lastIndexOf('/') + 1);
            return url.origin + path;
        } catch(e) {
            return null;
        }
    }

    function isSegmentOfPlaylist(videoUrl) {
        // Check if this video URL is a segment of a detected playlist
        if (!videoUrl.endsWith('.ts')) return false;
        if (!videoUrl.match(/seg-\d+-/i)) return false;

        const baseUrl = getBaseUrlFromSegment(videoUrl);
        if (!baseUrl) return false;

        // Check if any M3U8 shares the same base path
        for (const [m3u8Url, data] of allDetectedVideos.entries()) {
            if (data.type === 'm3u8') {
                const m3u8Base = getBaseUrlFromSegment(m3u8Url);
                if (m3u8Base && baseUrl.startsWith(m3u8Base)) {
                    return true;
                }
            }
        }

        return false;
    }

    function hasM3U8Playlist() {
        return Array.from(allDetectedVideos.values()).some(v => v.type === 'm3u8');
    }

    function isMasterPlaylist(url, manifest) {
        // Master playlists have playlists, not segments
        if (url.includes('master.m3u8')) return true;
        if (manifest.playlists && manifest.playlists.length > 0 && (!manifest.segments || manifest.segments.length === 0)) {
            return true;
        }
        return false;
    }

    function shouldFilterM3U8(url, manifest) {
        // If this is a master playlist and we have index playlists from the same base, filter it out
        if (!isMasterPlaylist(url, manifest)) return false;

        const baseUrl = getBaseUrlFromSegment(url);
        if (!baseUrl) return false;

        // Check if we have any index-*.m3u8 from the same base
        for (const [otherUrl, data] of allDetectedVideos.entries()) {
            if (data.type === 'm3u8' && otherUrl !== url) {
                const otherBase = getBaseUrlFromSegment(otherUrl);
                if (otherBase === baseUrl && otherUrl.includes('index-')) {
                    return true; // Filter out this master playlist
                }
            }
        }

        return false;
    }

    async function detectM3U8(url) {
        try {
            if (url.startsWith('blob:')) return;

            url = new URL(url, location.href).href;

            if (detectedM3U8Urls.includes(url)) return;
            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;

            let duration = 0;
            if (manifest.segments) {
                for (var s = 0; s < manifest.segments.length; s++) {
                    duration += manifest.segments[s].duration;
                }
            }

            const m3u8Data = {
                url: url,
                manifest: manifest,
                content: content,
                duration: duration,
                title: 'M3U8 - ' + (duration ? Math.ceil(duration / 60) + 'min' : 'Unknown'),
                timestamp: Date.now()
            };

            detectedM3U8s.push(m3u8Data);

            allDetectedVideos.set(url, {
                url: url,
                type: 'm3u8',
                timestamp: Date.now(),
                title: m3u8Data.title,
                m3u8Data: m3u8Data
            });

            checkForVideos();
        } catch(e) {
            console.error("M3U8 parse error:", e);
        }
    }

    function getVideoUrl(videoElement) {
        if (videoElement.tagName === 'VIDEO') {
            if (videoElement.currentSrc && !videoElement.currentSrc.startsWith('blob:'))
                return videoElement.currentSrc;
            if (videoElement.src && !videoElement.src.startsWith('blob:'))
                return videoElement.src;

            var sources = videoElement.querySelectorAll('source');
            for (var i = 0; i < sources.length; i++) {
                if (sources[i].src && !sources[i].src.startsWith('blob:'))
                    return sources[i].src;
            }
        }

        if (videoElement.tagName === 'IFRAME') {
            return videoElement.src;
        }

        return null;
    }

    function getUniqueVideos() {
        var videos = [];
        var seenUrls = new Set();

        // Add all detected videos from network
        allDetectedVideos.forEach(function(videoData) {
            // Skip .ts segments if we have playlists
            if (hasM3U8Playlist() && isSegmentOfPlaylist(videoData.url)) {
                return;
            }

            // Skip master playlists if we have index playlists
            if (videoData.type === 'm3u8' && shouldFilterM3U8(videoData.url, videoData.m3u8Data.manifest)) {
                return;
            }

            if (!seenUrls.has(videoData.url)) {
                seenUrls.add(videoData.url);
                videos.push(videoData);
            }
        });

        // Add regular videos from page
        for (var s = 0; s < VIDEO_SELECTORS.length; s++) {
            var elements = document.querySelectorAll(VIDEO_SELECTORS[s]);

            for (var i = 0; i < elements.length; i++) {
                var element = elements[i];
                var rect = element.getBoundingClientRect();

                if (rect.width > 100 && rect.height > 100) {
                    var url = getVideoUrl(element);
                    if (url && !seenUrls.has(url)) {
                        seenUrls.add(url);
                        const videoData = {
                            type: 'video',
                            element: element,
                            url: url,
                            title: element.title || element.alt || ('Video ' + (videos.length + 1)),
                            timestamp: Date.now()
                        };
                        videos.push(videoData);
                        allDetectedVideos.set(url, videoData);
                    }
                }
            }
        }

        // Check iframes for videos
        var iframes = document.querySelectorAll('iframe');
        for (var i = 0; i < iframes.length; i++) {
            try {
                var iframeDoc = iframes[i].contentDocument || iframes[i].contentWindow.document;
                if (iframeDoc) {
                    var iframeVideos = iframeDoc.querySelectorAll('video');
                    for (var v = 0; v < iframeVideos.length; v++) {
                        var url = getVideoUrl(iframeVideos[v]);
                        if (url && !seenUrls.has(url)) {
                            seenUrls.add(url);
                            const videoData = {
                                type: 'video',
                                element: iframeVideos[v],
                                url: url,
                                title: 'Iframe Video ' + (videos.length + 1),
                                timestamp: Date.now()
                            };
                            videos.push(videoData);
                            allDetectedVideos.set(url, videoData);
                        }
                    }
                }
            } catch(e) {
                // Cross-origin iframe, skip
            }
        }

        return videos;
    }

    function getButtonIcon(videos) {
        if (videos.length > 1) return '⇓';
        if (videos.length === 1) {
            return videos[0].type === 'm3u8' ? '⇣' : '↯';
        }
        return '▶';
    }

    function createFloatingButton() {
        if (floatingButton) return floatingButton;

        var container = document.createElement('div');
        container.id = 'universal-video-share-container';
        container.style.cssText = 'position: fixed; top: 15px; left: 15px; width: 40px; height: 40px; z-index: 999999;';

        var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', '40');
        svg.setAttribute('height', '40');
        svg.style.cssText = 'position: absolute; top: 0; left: 0; transform: rotate(-90deg);';

        var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        circle.setAttribute('cx', '20');
        circle.setAttribute('cy', '20');
        circle.setAttribute('r', '18');
        circle.setAttribute('fill', 'none');
        circle.setAttribute('stroke', '#4ade80');
        circle.setAttribute('stroke-width', '3');
        circle.setAttribute('stroke-dasharray', '113');
        circle.setAttribute('stroke-dashoffset', '113');
        circle.setAttribute('stroke-linecap', 'round');
        circle.style.cssText = 'transition: stroke-dashoffset 0.3s ease;';
        circle.id = 'progress-circle';

        svg.appendChild(circle);
        container.appendChild(svg);

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

        floatingButton.style.cssText = `position: absolute; top: 2px; left: 2px; width: 36px; height: 36px; background: ${COLORS.button}; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); color: ${COLORS.icon}; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 50%; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; user-select: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);`;

        container.appendChild(floatingButton);
        floatingButton.progressCircle = circle;

        floatingButton.addEventListener('mouseenter', function() {
            this.style.background = COLORS.buttonHover;
            this.style.transform = 'scale(1.05)';
        });

        floatingButton.addEventListener('mouseleave', function() {
            this.style.background = COLORS.button;
            this.style.transform = 'scale(1)';
        });

        floatingButton.addEventListener('mousedown', function(e) {
            e.preventDefault();
            isLongPress = false;
            pressTimer = setTimeout(function() {
                isLongPress = true;
                floatingButton.style.background = 'rgba(74, 222, 128, 0.8)';
                floatingButton.innerHTML = '⎘';
            }, 500);
        });

        floatingButton.addEventListener('mouseup', function(e) {
            e.preventDefault();
            clearTimeout(pressTimer);

            var videos = getUniqueVideos();
            floatingButton.style.background = COLORS.button;
            floatingButton.innerHTML = getButtonIcon(videos);

            if (isLongPress) {
                handleCopy();
            } else {
                handleShare();
            }
        });

        floatingButton.addEventListener('mouseleave', function() {
            clearTimeout(pressTimer);
            var videos = getUniqueVideos();
            floatingButton.style.background = COLORS.button;
            floatingButton.innerHTML = getButtonIcon(videos);
        });

        floatingButton.addEventListener('touchstart', function(e) {
            e.preventDefault();
            isLongPress = false;
            pressTimer = setTimeout(function() {
                isLongPress = true;
                floatingButton.style.background = 'rgba(74, 222, 128, 0.8)';
                floatingButton.innerHTML = '⎘';
            }, 500);
        });

        floatingButton.addEventListener('touchend', function(e) {
            e.preventDefault();
            clearTimeout(pressTimer);

            var videos = getUniqueVideos();
            floatingButton.style.background = COLORS.button;
            floatingButton.innerHTML = getButtonIcon(videos);

            if (isLongPress) {
                handleCopy();
            } else {
                handleShare();
            }
        });

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

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

    function resetProgress() {
        if (!floatingButton || !floatingButton.progressCircle) return;
        floatingButton.progressCircle.setAttribute('stroke-dashoffset', '113');
    }

    function handleShare() {
        var videos = getUniqueVideos();

        if (videos.length === 0) {
            showNotification('No videos found', 'error');
            return;
        }

        if (videos.length === 1) {
            shareVideo(videos[0]);
        } else {
            showVideoSelector(videos, 'share');
        }
    }

    function handleCopy() {
        var videos = getUniqueVideos();

        if (videos.length === 0) {
            showNotification('No videos found', 'error');
            return;
        }

        if (videos.length === 1) {
            // For M3U8, still download but skip share
            if (videos[0].type === 'm3u8') {
                downloadM3U8(videos[0], true); // Force download
            } else {
                copyVideoUrl(videos[0]);
            }
        } else {
            showVideoSelector(videos, 'copy');
        }
    }

    function showVideoSelector(videos, action) {
        var existingSelector = document.querySelector('#video-selector-popup');
        if (existingSelector) {
            existingSelector.remove();
        }

        var 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(8px); -webkit-backdrop-filter: blur(8px); z-index: 9999999; display: flex; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box;';

        var container = document.createElement('div');
        container.style.cssText = 'background: rgba(20, 20, 20, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 24px; max-width: 600px; max-height: 70%; overflow-y: auto; position: relative; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);';

        var closeButton = document.createElement('button');
        closeButton.innerHTML = '✕';
        closeButton.style.cssText = `position: absolute; top: 12px; right: 12px; background: rgba(255, 255, 255, 0.1); border: none; font-size: 16px; cursor: pointer; color: ${COLORS.text}; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s;`;

        closeButton.addEventListener('click', function() {
            popup.remove();
        });

        closeButton.addEventListener('mouseenter', function() {
            this.style.background = 'rgba(255, 255, 255, 0.2)';
        });

        closeButton.addEventListener('mouseleave', function() {
            this.style.background = 'rgba(255, 255, 255, 0.1)';
        });

        var title = document.createElement('h3');
        title.textContent = 'Select Video to ' + (action.charAt(0).toUpperCase() + action.slice(1));
        title.style.cssText = `margin: 0 0 16px 0; color: ${COLORS.text}; font-size: 16px; font-weight: 600; text-align: center;`;

        container.appendChild(closeButton);
        container.appendChild(title);

        // Sort videos by timestamp (newest first)
        videos.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));

        for (var i = 0; i < videos.length; i++) {
            var videoData = videos[i];
            var videoItem = document.createElement('div');
            videoItem.style.cssText = 'margin-bottom: 12px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; background: rgba(255, 255, 255, 0.05);';

            (function(currentVideoData) {
                videoItem.addEventListener('mouseenter', function() {
                    this.style.borderColor = 'rgba(255, 255, 255, 0.3)';
                    this.style.background = 'rgba(255, 255, 255, 0.1)';
                });

                videoItem.addEventListener('mouseleave', function() {
                    this.style.borderColor = 'rgba(255, 255, 255, 0.1)';
                    this.style.background = 'rgba(255, 255, 255, 0.05)';
                });

                var headerDiv = document.createElement('div');
                headerDiv.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;';

                var typeBadge = document.createElement('span');
                typeBadge.textContent = currentVideoData.type === 'm3u8' ? 'M3U8' : 'VIDEO';
                typeBadge.style.cssText = 'display: inline-block; background: ' + (currentVideoData.type === 'm3u8' ? 'rgba(239, 68, 68, 0.8)' : 'rgba(59, 130, 246, 0.8)') + `; color: ${COLORS.text}; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;`;

                var timeAgo = document.createElement('span');
                timeAgo.textContent = getTimeAgo(currentVideoData.timestamp || Date.now());
                timeAgo.style.cssText = `color: ${COLORS.text}; opacity: 0.6; font-size: 10px;`;

                headerDiv.appendChild(typeBadge);
                headerDiv.appendChild(timeAgo);
                videoItem.appendChild(headerDiv);

                var videoInfo = document.createElement('div');
                videoInfo.innerHTML = `<div style="color: ${COLORS.text}; font-size: 13px; font-weight: 500; margin-bottom: 4px;">` + currentVideoData.title + `</div><div style="color: ${COLORS.text}; opacity: 0.5; font-size: 11px; word-break: break-all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">` + currentVideoData.url + '</div>';
                videoItem.appendChild(videoInfo);

                videoItem.addEventListener('click', function() {
                    popup.remove();
                    if (action === 'share') {
                        shareVideo(currentVideoData);
                    } else {
                        if (currentVideoData.type === 'm3u8') {
                            downloadM3U8(currentVideoData, true); // Force download for copy
                        } else {
                            copyVideoUrl(currentVideoData);
                        }
                    }
                });
            })(videoData);

            container.appendChild(videoItem);
        }

        popup.appendChild(container);

        popup.addEventListener('click', function(e) {
            if (e.target === popup) {
                popup.remove();
            }
        });

        document.body.appendChild(popup);
    }

    async function downloadM3U8(videoData, forceDownload = false) {
        // Check if we already downloaded this
        const cachedBlob = downloadedBlobs.get(videoData.url);
        if (cachedBlob && !forceDownload) {
            showNotification('Using cached video...', 'info');
            const filename = cachedBlob.filename;
            await shareOrDownloadBlob(cachedBlob.blob, filename, videoData, forceDownload);
            return;
        }

        showNotification('Downloading M3U8...', 'info');
        resetProgress();

        try {
            const manifest = videoData.m3u8Data.manifest;
            const baseUrl = videoData.url.substring(0, videoData.url.lastIndexOf('/') + 1);
            const segments = manifest.segments;

            if (!segments || segments.length === 0) {
                throw new Error("No segments found in this playlist");
            }

            var segmentData = [];
            for (var i = 0; i < segments.length; i++) {
                var segUrl = segments[i].uri.startsWith('http')
                    ? segments[i].uri
                    : baseUrl + segments[i].uri;

                var response = await fetch(segUrl);
                var data = await response.arrayBuffer();
                segmentData.push(data);

                var percent = Math.floor((i + 1) / segments.length * 100);
                updateProgress(percent);

                if (percent % 20 === 0 || i === segments.length - 1) {
                    showNotification('Downloading: ' + percent + '%', 'info');
                }
            }

            showNotification('Merging...', 'info');
            var merged = new Blob(segmentData, { type: 'video/mp2t' });

            var urlPath = new URL(videoData.url).pathname;
            var baseName = urlPath.split('/').pop().replace(/\.(m3u8?|ts)$/i, '') || 'video';
            var filename = baseName + '-noenc.ts';

            // Cache the blob
            downloadedBlobs.set(videoData.url, { blob: merged, filename: filename });

            await shareOrDownloadBlob(merged, filename, videoData, forceDownload);

        } catch(e) {
            console.error("M3U8 download failed:", e);
            showNotification('Download failed: ' + e.message, 'error');
            resetProgress();
        }
    }

    async function shareOrDownloadBlob(blob, filename, videoData, forceDownload = false) {
        const processed = processedVideos.get(videoData.url);

        // If already processed and user clicks again, force download
        if (processed && !forceDownload) {
            forceDownload = true;
            showNotification('Downloading...', 'info');
        }

        if (!forceDownload && navigator.share && navigator.canShare) {
            try {
                var file = new File([blob], filename, { type: 'video/mp2t' });

                if (navigator.canShare({ files: [file] })) {
                    var shareStartTime = Date.now();
                    var shareCompleted = false;

                    // Set a timeout to detect if share dialog stayed open
                    var shareTimeout = setTimeout(function() {
                        if (!shareCompleted) {
                            console.log("Share dialog appears to be active (2s elapsed)");
                        }
                    }, 2000);

                    try {
                        await navigator.share({
                            files: [file],
                            title: document.title,
                            text: 'M3U8 Video'
                        });

                        shareCompleted = true;
                        clearTimeout(shareTimeout);

                        processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() });
                        showNotification('Shared!', 'success');
                        resetProgress();
                        return;

                    } catch(e) {
                        shareCompleted = true;
                        clearTimeout(shareTimeout);

                        var timeElapsed = Date.now() - shareStartTime;

                        // If share was open for >2 seconds, assume it worked
                        if (timeElapsed > 2000) {
                            processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() });
                            showNotification('Share completed', 'success');
                            resetProgress();
                            return;
                        }

                        console.log("Share failed/cancelled after " + timeElapsed + "ms, downloading instead:", e);
                    }
                }
            } catch(e) {
                console.log("Share API error:", e);
            }
        }

        // Download the file
        var blobUrl = URL.createObjectURL(blob);
        var a = document.createElement('a');
        a.href = blobUrl;
        a.download = filename;
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();

        setTimeout(function() {
            document.body.removeChild(a);
            URL.revokeObjectURL(blobUrl);
        }, 100);

        processedVideos.set(videoData.url, { action: 'downloaded', timestamp: Date.now() });
        showNotification('Downloaded: ' + filename, 'success');
        resetProgress();
    }

    function shareVideo(videoData) {
        if (videoData.type === 'm3u8') {
            const processed = processedVideos.get(videoData.url);
            const forceDownload = processed && (processed.action === 'shared' || processed.action === 'downloaded');
            downloadM3U8(videoData, forceDownload);
            return;
        }

        const processed = processedVideos.get(videoData.url);
        if (processed && (processed.action === 'shared' || processed.action === 'copied')) {
            downloadVideo(videoData);
            return;
        }

        if (navigator.share) {
            var shareStartTime = Date.now();
            navigator.share({
                title: document.title,
                url: videoData.url
            }).then(function() {
                processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() });
                showNotification('Shared!', 'success');
            }).catch(function(error) {
                if (Date.now() - shareStartTime > 2000) {
                    processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() });
                    showNotification('Share completed', 'success');
                } else {
                    console.log('Share cancelled:', error);
                    copyVideoUrl(videoData);
                }
            });
        } else {
            copyVideoUrl(videoData);
        }
    }

    function downloadVideo(videoData) {
        showNotification('Opening video...', 'info');
        window.open(videoData.url, '_blank');
        processedVideos.set(videoData.url, { action: 'downloaded', timestamp: Date.now() });
    }

    function copyVideoUrl(videoData) {
        const processed = processedVideos.get(videoData.url);
        if (processed && processed.action === 'copied') {
            downloadVideo(videoData);
            return;
        }

        if (navigator.clipboard && navigator.clipboard.writeText) {
            var copyStartTime = Date.now();
            navigator.clipboard.writeText(videoData.url).then(function() {
                processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() });
                showNotification('URL copied', 'success');
            }).catch(function(error) {
                if (Date.now() - copyStartTime > 2000) {
                    processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() });
                    showNotification('URL copied', 'success');
                } else {
                    fallbackCopy(videoData.url, videoData);
                }
            });
        } else {
            fallbackCopy(videoData.url, videoData);
        }
    }

    function fallbackCopy(url, videoData) {
        try {
            var textArea = document.createElement('textarea');
            textArea.value = url;
            textArea.style.position = 'fixed';
            textArea.style.left = '-999999px';
            document.body.appendChild(textArea);
            textArea.select();

            if (document.execCommand('copy')) {
                processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() });
                showNotification('URL copied', 'success');
            } else {
                showNotification('Copy failed', 'error');
            }

            document.body.removeChild(textArea);
        } catch (err) {
            showNotification('Copy failed', 'error');
        }
    }

    function showNotification(message, type) {
        if (type === undefined) type = 'success';

        var existingNotifications = document.querySelectorAll('.universal-video-notification');
        for (var i = 0; i < existingNotifications.length; i++) {
            existingNotifications[i].remove();
        }

        var notification = document.createElement('div');
        notification.textContent = message;
        notification.className = 'universal-video-notification';

        var bgColor = type === 'success' ? 'rgba(74, 222, 128, 0.9)' :
                      type === 'error' ? 'rgba(239, 68, 68, 0.9)' :
                      'rgba(59, 130, 246, 0.9)';

        notification.style.cssText = `position: fixed; top: 65px; left: 15px; background: ${bgColor}; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: ${COLORS.text}; padding: 8px 12px; border-radius: 6px; z-index: 999998; font-weight: 500; font-size: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); max-width: 250px; word-wrap: break-word; transform: translateX(-300px); transition: transform 0.3s ease;`;

        document.body.appendChild(notification);

        setTimeout(function() {
            notification.style.transform = 'translateX(0)';
        }, 100);

        setTimeout(function() {
            if (notification.parentNode) {
                notification.style.transform = 'translateX(-300px)';
                setTimeout(function() {
                    notification.remove();
                }, 300);
            }
        }, 3000);
    }

    function checkForVideos() {
        var videos = getUniqueVideos();

        if (videos.length > 0) {
            if (!floatingButton) {
                createFloatingButton();
            }
            if (floatingButton) {
                floatingButton.innerHTML = getButtonIcon(videos);
            }
        } else {
            if (floatingButton && floatingButton.parentNode) {
                floatingButton.parentNode.remove();
                floatingButton = null;
            }
        }
    }

    function init() {
        console.log('Universal Video Share with M3U8 initialized');
        setTimeout(checkForVideos, 1000);
        checkInterval = setInterval(checkForVideos, 5000);
    }

    init();

})();