Share button with M3U8 support. Downloads and converts to MP4. (hold 8s for debug console)
// ==UserScript== // @name Universal Video Share Button with M3U8 Support + MP4 Remux // @namespace http://tampermonkey.net/ // @version 6.7 // @description Share button with M3U8 support. Downloads and converts to MP4. (hold 8s for debug console) // @author Minoa // @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'; var floatingButton = null; var pressTimer = null; var isLongPress = false; var checkInterval = null; var detectedM3U8s = []; var detectedM3U8Urls = []; var allDetectedVideos = new Map(); var processedVideos = 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; // Detect iOS const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); const isMobile = isIOS || /Android|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // Color scheme const COLORS = { button: 'rgba(85, 66, 61, 0.7)', // Semi-transparent buttonHover: 'rgba(107, 86, 81, 0.85)', icon: '#ffc0ad', text: '#fff3ec' }; // Add critical CSS to ensure button is always on top GM_addStyle(` #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; pointer-events: auto !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; } `); // Common video selectors 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']; // Helper: Sanitize filename function sanitizeFilename(filename) { return filename .replace(/[<>:"\/\\|?*\x00-\x1F]/g, '') .replace(/\s+/g, '_') .replace(/_{2,}/g, '_') .replace(/^\.+/, '') .substring(0, 200); } // Helper: Get filename from page title function getFilenameFromPageTitle(extension = 'mp4') { const pageTitle = document.title || 'video'; const sanitized = sanitizeFilename(pageTitle); return sanitized + '.' + extension; } // FFmpeg helpers const getWasmBinary = async () => { if (wasmBinaryCache) { return wasmBinaryCache.slice(0); } const wasmURL = GM_getResourceURL('wasmURL', false); debugLog('[FFMPEG] Loading WASM from: ' + wasmURL.substring(0, 50) + '...'); 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) { debugLog('[FFMPEG] WASM loaded: ' + (wasmBinary.byteLength / 1024 / 1024).toFixed(2) + 'MB'); 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; debugLog('[FFMPEG] Initializing FFmpeg...'); 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); debugLog('[FFMPEG] Encoding progress: ' + percent + '%'); }); await ffmpegInstance.load(await getFFmpegLoadConfig()); ffmpegLoaded = true; debugLog('[FFMPEG] ✅ FFmpeg ready!'); showNotification('✅ FFmpeg loaded', 'success'); return ffmpegInstance; } catch(e) { debugLog('[ERROR] FFmpeg init failed: ' + e.message); showNotification('❌ FFmpeg failed to load', 'error'); throw e; } } async function convertTStoMP4(tsBlob, baseFilename) { debugLog('[CONVERT] Starting TS to MP4 conversion...'); debugLog('[CONVERT] Input size: ' + (tsBlob.size / 1024 / 1024).toFixed(2) + 'MB'); try { showNotification('⚙️ Loading FFmpeg encoder...', 'info'); const ffmpeg = await initFFmpeg(); const inputName = 'input.ts'; const outputName = baseFilename.endsWith('.mp4') ? baseFilename : baseFilename + '.mp4'; debugLog('[CONVERT] Writing input file to FFmpeg...'); showNotification('📝 Preparing conversion...', 'info'); const inputData = new Uint8Array(await tsBlob.arrayBuffer()); await ffmpeg.writeFile(inputName, inputData); debugLog('[CONVERT] Starting FFmpeg conversion...'); debugLog('[CONVERT] Command: -i input.ts -c copy -movflags faststart ' + outputName); showNotification('🔄 Converting to MP4...', 'info'); await ffmpeg.exec(['-i', inputName, '-c', 'copy', '-movflags', 'faststart', outputName]); debugLog('[CONVERT] Reading output file...'); const data = await ffmpeg.readFile(outputName, 'binary'); debugLog('[CONVERT] Cleaning up FFmpeg files...'); await ffmpeg.deleteFile(inputName); await ffmpeg.deleteFile(outputName); const mp4Blob = new Blob([data.buffer], { type: 'video/mp4' }); debugLog('[CONVERT] ✅ Conversion complete!'); debugLog('[CONVERT] Output size: ' + (mp4Blob.size / 1024 / 1024).toFixed(2) + 'MB'); showNotification('✅ Converted to MP4!', 'success'); return { blob: mp4Blob, filename: outputName }; } catch(e) { debugLog('[ERROR] Conversion failed: ' + e.message); debugLog('[ERROR] Stack: ' + e.stack); showNotification('❌ Conversion failed', 'error'); throw e; } } // Helper function to check if URL is M3U8 function isM3U8Url(url) { if (!url) return false; const lowerUrl = url.toLowerCase(); return lowerUrl.includes('.m3u8') || lowerUrl.includes('.m3u'); } // Helper function to check if URL is blob function isBlobUrl(url) { if (!url) return false; return url.startsWith('blob:'); } // Debug console functions function createDebugConsole() { if (debugConsole) return; debugConsole = document.createElement('div'); debugConsole.id = 'debug-console'; debugConsole.style.cssText = ` position: fixed; top: 70px; left: 10px; right: 10px; bottom: 10px; background: rgba(0, 0, 0, 0.95); border: 2px solid ${COLORS.icon}; border-radius: 12px; z-index: 2147483644; display: flex; flex-direction: column; font-family: monospace; font-size: 11px; color: ${COLORS.text}; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8); `; var header = document.createElement('div'); header.style.cssText = ` padding: 12px; background: ${COLORS.button}; border-bottom: 1px solid ${COLORS.icon}; display: flex; justify-content: space-between; align-items: center; border-radius: 10px 10px 0 0; `; header.innerHTML = `<strong style="color: ${COLORS.icon};">🐛 DEBUG CONSOLE</strong>`; var buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display: flex; gap: 8px;'; var copyBtn = document.createElement('button'); copyBtn.textContent = '📋 Copy'; copyBtn.style.cssText = ` background: ${COLORS.icon}; color: #000; border: none; padding: 6px 12px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 11px; `; copyBtn.onclick = function() { var logText = debugLogs.join('\n'); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(logText).then(function() { copyBtn.textContent = '✅ Copied!'; setTimeout(function() { copyBtn.textContent = '📋 Copy'; }, 2000); }); } else { var textArea = document.createElement('textarea'); textArea.value = logText; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); copyBtn.textContent = '✅ Copied!'; setTimeout(function() { copyBtn.textContent = '📋 Copy'; }, 2000); } }; var clearBtn = document.createElement('button'); clearBtn.textContent = '🗑️ Clear'; clearBtn.style.cssText = ` background: rgba(239, 68, 68, 0.8); color: ${COLORS.text}; border: none; padding: 6px 12px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 11px; `; clearBtn.onclick = function() { debugLogs = []; updateDebugConsole(); }; var closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.style.cssText = ` background: rgba(255, 255, 255, 0.2); color: ${COLORS.text}; border: none; padding: 6px 10px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 11px; `; closeBtn.onclick = function() { debugMode = false; debugConsole.remove(); debugConsole = null; }; buttonContainer.appendChild(copyBtn); buttonContainer.appendChild(clearBtn); buttonContainer.appendChild(closeBtn); header.appendChild(buttonContainer); var logContainer = document.createElement('div'); logContainer.id = 'debug-log-container'; logContainer.style.cssText = ` flex: 1; overflow-y: auto; padding: 12px; line-height: 1.5; `; debugConsole.appendChild(header); debugConsole.appendChild(logContainer); document.body.appendChild(debugConsole); updateDebugConsole(); } function updateDebugConsole() { if (!debugConsole) return; var logContainer = document.getElementById('debug-log-container'); if (!logContainer) return; logContainer.innerHTML = ''; debugLogs.forEach(function(log) { var logLine = document.createElement('div'); logLine.style.cssText = 'margin-bottom: 4px; word-wrap: break-word;'; var color = COLORS.text; if (log.includes('[ERROR]') || log.includes('❌')) { color = '#ef4444'; } else if (log.includes('[SUCCESS]') || log.includes('✅')) { color = '#4ade80'; } else if (log.includes('[INFO]') || log.includes('📥') || log.includes('🔄')) { color = '#3b82f6'; } else if (log.includes('[M3U8]') || log.includes('[FFMPEG]')) { color = COLORS.icon; } logLine.style.color = color; logLine.textContent = log; logContainer.appendChild(logLine); }); logContainer.scrollTop = logContainer.scrollHeight; } function debugLog(message) { var timestamp = new Date().toLocaleTimeString(); var logMessage = `[${timestamp}] ${message}`; debugLogs.push(logMessage); console.log(message); if (debugMode && debugConsole) { updateDebugConsole(); } } // M3U8 Detection (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); } else if (!url.match(/seg-\d+-.*\.ts/i) && !url.endsWith('.ts') && !isBlobUrl(url)) { checkUrlForVideo(url); } if (this.responseText && this.responseText.trim().startsWith("#EXTM3U")) { detectM3U8(url); } } } catch(e) {} }); return originalOpen.apply(this, args); }; 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; }); }; const originalSetAttribute = Element.prototype.setAttribute; Element.prototype.setAttribute = function(name, value) { if (this.tagName === 'VIDEO' || this.tagName === 'SOURCE') { if (name === 'src' && value) { if (isM3U8Url(value)) { detectM3U8(value); } else if (!isBlobUrl(value)) { checkUrlForVideo(value); } } } return originalSetAttribute.call(this, name, value); }; })(); function checkUrlForVideo(url) { try { if (isBlobUrl(url)) { debugLog('[VIDEO CHECK] Ignoring blob URL: ' + url); return; } if (url.match(/seg-\d+-.*\.ts/i)) return; if (url.endsWith('.ts')) return; if (isM3U8Url(url)) { debugLog('[VIDEO CHECK] URL contains m3u8, skipping: ' + url); return; } 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)) { debugLog('[VIDEO] Found video file: ' + fullUrl); allDetectedVideos.set(fullUrl, { url: fullUrl, type: 'video', timestamp: Date.now(), title: 'Video - ' + getFilenameFromUrl(fullUrl) }); checkForVideos(); } } } catch(e) { debugLog('[ERROR] checkUrlForVideo: ' + e.message); } } 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) { if (!videoUrl.endsWith('.ts')) return false; if (!videoUrl.match(/seg-\d+-/i)) return false; const baseUrl = getBaseUrlFromSegment(videoUrl); if (!baseUrl) return false; 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) { 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 (!isMasterPlaylist(url, manifest)) return false; const baseUrl = getBaseUrlFromSegment(url); if (!baseUrl) return false; for (const [otherUrl, data] of allDetectedVideos.entries()) { if (data.type === 'm3u8' && otherUrl !== url) { const otherBase = getBaseUrlFromSegment(otherUrl); if (otherBase === baseUrl && otherUrl.includes('index-')) { debugLog('[M3U8] Filtering master playlist: ' + url); return true; } } } return false; } async function detectM3U8(url) { try { if (isBlobUrl(url)) { debugLog('[M3U8] Ignoring blob URL: ' + url); return; } url = new URL(url, location.href).href; const urlWithoutQuery = url.split('?')[0]; let alreadyHasBetter = false; for (const [existingUrl, existingData] of allDetectedVideos.entries()) { if (existingData.type === 'm3u8') { const existingWithoutQuery = existingUrl.split('?')[0]; if (existingWithoutQuery === urlWithoutQuery) { if (existingData.m3u8Data && existingData.m3u8Data.manifest && existingData.m3u8Data.manifest.segments && existingData.m3u8Data.manifest.segments.length > 0) { debugLog('[M3U8] Already have better version with segments: ' + existingUrl); alreadyHasBetter = true; break; } } } } if (alreadyHasBetter) return; if (detectedM3U8Urls.includes(url)) { debugLog('[M3U8] Already detected: ' + url); return; } detectedM3U8Urls.push(url); debugLog('[M3U8] *** DETECTED M3U8 URL ***: ' + url); debugLog('[M3U8] Fetching manifest...'); 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.playlists && manifest.playlists.length > 0 && (!manifest.segments || manifest.segments.length === 0)) { debugLog('[M3U8] This is a MASTER playlist with ' + manifest.playlists.length + ' variants'); const bestVariant = manifest.playlists[manifest.playlists.length - 1]; debugLog('[M3U8] Selected variant: ' + JSON.stringify(bestVariant)); const baseUrl = url.substring(0, url.lastIndexOf('/') + 1); const variantUrl = bestVariant.uri.startsWith('http') ? bestVariant.uri : baseUrl + bestVariant.uri; debugLog('[M3U8] Fetching variant playlist: ' + variantUrl); const masterIndex = detectedM3U8Urls.indexOf(url); if (masterIndex > -1) { detectedM3U8Urls.splice(masterIndex, 1); } for (const [existingUrl, existingData] of allDetectedVideos.entries()) { if (existingData.type === 'm3u8') { const existingWithoutQuery = existingUrl.split('?')[0]; if (existingWithoutQuery === urlWithoutQuery) { debugLog('[M3U8] Removing existing master playlist: ' + existingUrl); allDetectedVideos.delete(existingUrl); } } } detectM3U8(variantUrl); return; } if (manifest.segments) { for (var s = 0; s < manifest.segments.length; s++) { duration += manifest.segments[s].duration; } debugLog('[M3U8] Found ' + manifest.segments.length + ' segments, duration: ' + duration + 's'); } else { debugLog('[M3U8] No segments found!'); } 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); for (const [existingUrl, existingData] of allDetectedVideos.entries()) { if (existingData.type === 'm3u8') { const existingWithoutQuery = existingUrl.split('?')[0]; if (existingWithoutQuery === urlWithoutQuery) { if ((!existingData.m3u8Data.manifest.segments || existingData.m3u8Data.manifest.segments.length === 0) && manifest.segments && manifest.segments.length > 0) { debugLog('[M3U8] Replacing master with media playlist: ' + existingUrl + ' -> ' + url); allDetectedVideos.delete(existingUrl); } } } } debugLog('[M3U8] *** ADDING TO MAP AS TYPE m3u8 ***'); allDetectedVideos.set(url, { url: url, type: 'm3u8', timestamp: Date.now(), title: m3u8Data.title, m3u8Data: m3u8Data }); checkForVideos(); } catch(e) { debugLog('[ERROR] M3U8 parse failed: ' + e.message); } } function getVideoUrl(videoElement) { if (videoElement.tagName === 'VIDEO') { if (videoElement.currentSrc && !isBlobUrl(videoElement.currentSrc)) return videoElement.currentSrc; if (videoElement.src && !isBlobUrl(videoElement.src)) return videoElement.src; var sources = videoElement.querySelectorAll('source'); for (var i = 0; i < sources.length; i++) { if (sources[i].src && !isBlobUrl(sources[i].src)) return sources[i].src; } } if (videoElement.tagName === 'IFRAME') { return videoElement.src; } return null; } function getUniqueVideos() { var videos = []; var seenUrls = new Set(); debugLog('[GET VIDEOS] Checking allDetectedVideos map...'); debugLog('[GET VIDEOS] Map size: ' + allDetectedVideos.size); var m3u8Videos = []; var regularVideos = []; allDetectedVideos.forEach(function(videoData, url) { debugLog('[GET VIDEOS] Map entry: ' + videoData.type + ' - ' + url); if (isBlobUrl(videoData.url)) { debugLog('[GET VIDEOS] Skipping blob URL: ' + url); return; } if (hasM3U8Playlist() && isSegmentOfPlaylist(videoData.url)) { debugLog('[GET VIDEOS] Skipping segment: ' + url); return; } if (videoData.type === 'm3u8' && shouldFilterM3U8(videoData.url, videoData.m3u8Data.manifest)) { debugLog('[GET VIDEOS] Filtering master playlist: ' + url); return; } if (!seenUrls.has(videoData.url)) { seenUrls.add(videoData.url); if (videoData.type === 'm3u8') { m3u8Videos.push(videoData); debugLog('[GET VIDEOS] Added M3U8: ' + url); } else { regularVideos.push(videoData); debugLog('[GET VIDEOS] Added regular video: ' + url); } } }); videos = m3u8Videos.concat(regularVideos); debugLog('[GET VIDEOS] Checking video elements on 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 && isBlobUrl(url)) { debugLog('[GET VIDEOS] Skipping blob from element: ' + url); continue; } if (url && isM3U8Url(url)) { debugLog('[GET VIDEOS] Found M3U8 in video element, triggering detection: ' + url); detectM3U8(url); continue; } if (url && !seenUrls.has(url) && !allDetectedVideos.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); debugLog('[GET VIDEOS] Added from element: video - ' + url); } } } } 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 && isBlobUrl(url)) continue; if (url && isM3U8Url(url)) { debugLog('[GET VIDEOS] Found M3U8 in iframe, triggering detection: ' + url); detectM3U8(url); continue; } if (url && !seenUrls.has(url) && !allDetectedVideos.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) {} } debugLog('[GET VIDEOS] Final video count: ' + videos.length); debugLog('[GET VIDEOS] M3U8s: ' + m3u8Videos.length + ', Regular: ' + (videos.length - m3u8Videos.length)); 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'; var 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;'; var 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.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'; container.appendChild(floatingButton); floatingButton.progressCircle = circle; floatingButton.addEventListener('mousedown', function(e) { e.preventDefault(); isLongPress = false; longPressStartTime = Date.now(); pressTimer = setTimeout(function() { isLongPress = true; var duration = Date.now() - longPressStartTime; if (duration >= 8000) { debugMode = true; floatingButton.style.background = 'rgba(239, 68, 68, 0.8)'; floatingButton.innerHTML = '🐛'; debugLog('='.repeat(50)); debugLog('[INIT] DEBUG MODE ACTIVATED'); debugLog('='.repeat(50)); } else { floatingButton.style.background = 'rgba(74, 222, 128, 0.85)'; floatingButton.innerHTML = '⎘'; } }, 500); }); floatingButton.addEventListener('mouseup', function(e) { e.preventDefault(); clearTimeout(pressTimer); var pressDuration = Date.now() - longPressStartTime; var videos = getUniqueVideos(); floatingButton.style.background = COLORS.button; floatingButton.innerHTML = getButtonIcon(videos); if (pressDuration >= 8000) { debugMode = true; createDebugConsole(); } else 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; longPressStartTime = Date.now(); pressTimer = setTimeout(function() { isLongPress = true; var duration = Date.now() - longPressStartTime; if (duration >= 8000) { debugMode = true; floatingButton.style.background = 'rgba(239, 68, 68, 0.8)'; floatingButton.innerHTML = '🐛'; navigator.vibrate && navigator.vibrate(200); debugLog('='.repeat(50)); debugLog('[INIT] DEBUG MODE ACTIVATED'); debugLog('='.repeat(50)); } else { floatingButton.style.background = 'rgba(74, 222, 128, 0.85)'; floatingButton.innerHTML = '⎘'; navigator.vibrate && navigator.vibrate(100); } }, 500); }); floatingButton.addEventListener('touchend', function(e) { e.preventDefault(); clearTimeout(pressTimer); var pressDuration = Date.now() - longPressStartTime; var videos = getUniqueVideos(); floatingButton.style.background = COLORS.button; floatingButton.innerHTML = getButtonIcon(videos); if (pressDuration >= 8000) { debugMode = true; createDebugConsole(); } else if (isLongPress) { handleCopy(); } else { handleShare(); } }); document.body.appendChild(container); return floatingButton; } function updateProgress(percent) { if (!floatingButton || !floatingButton.progressCircle) return; var offset = 138 - (138 * percent / 100); floatingButton.progressCircle.setAttribute('stroke-dashoffset', offset); } function resetProgress() { if (!floatingButton || !floatingButton.progressCircle) return; floatingButton.progressCircle.setAttribute('stroke-dashoffset', '138'); } function handleShare() { debugLog('[SHARE] Button clicked'); showNotification('🔍 Checking videos...', 'info'); var videos = getUniqueVideos(); debugLog('[SHARE] Videos found: ' + videos.length); videos.forEach(v => debugLog('[SHARE] - ' + v.type + ': ' + v.url)); if (videos.length === 0) { showNotification('❌ No videos found', 'error'); debugLog('[ERROR] No videos found'); return; } if (videos.length > 1) { debugLog('[SHARE] Multiple videos/playlists, showing selector'); showVideoSelector(videos, 'share'); } else { debugLog('[SHARE] Single video, calling shareVideo'); shareVideo(videos[0]); } } function handleCopy() { debugLog('[COPY] Long press detected'); showNotification('📋 Long press detected...', 'info'); var videos = getUniqueVideos(); debugLog('[COPY] Videos found: ' + videos.length); if (videos.length === 0) { showNotification('❌ No videos found', 'error'); debugLog('[ERROR] No videos found'); return; } if (videos.length > 1) { showVideoSelector(videos, 'copy'); } else { if (videos[0].type === 'm3u8') { debugLog('[COPY] M3U8 detected, forcing download'); downloadM3U8(videos[0], true); } else { copyVideoUrl(videos[0]); } } } 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: 2147483645; 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); 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 || 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 PLAYLIST' : 'VIDEO FILE'; 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: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 700;`; 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); } 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) { debugLog('='.repeat(50)); debugLog('[M3U8] STARTING M3U8 DOWNLOAD & CONVERSION'); debugLog('[M3U8] URL: ' + videoData.url); const cachedBlob = downloadedBlobs.get(videoData.url); if (cachedBlob) { debugLog('[M3U8] Using cached MP4 blob'); showNotification('♻️ Using cached video...', 'info'); await shareOrDownloadBlob(cachedBlob.blob, cachedBlob.filename, videoData); return; } showNotification('📥 Starting M3U8 download...', '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 in playlist!"); } debugLog('[M3U8] Found ' + segments.length + ' segments'); showNotification(`📥 Downloading ${segments.length} segments...`, 'info'); var segmentData = []; var totalSize = 0; 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); if (!response.ok) { throw new Error('Segment ' + (i+1) + ' failed: ' + response.status); } var data = await response.arrayBuffer(); totalSize += data.byteLength; segmentData.push(data); var percent = Math.floor((i + 1) / segments.length * 100); updateProgress(percent); if (percent % 10 === 0 || i === segments.length - 1) { showNotification(`📥 ${percent}% (${(totalSize/1024/1024).toFixed(1)}MB)`, 'info'); } } debugLog('[M3U8] All segments downloaded, merging...'); showNotification('🔄 Merging segments...', 'info'); var merged = new Blob(segmentData, { type: 'video/mp2t' }); debugLog('[M3U8] Merged TS blob size: ' + (merged.size/1024/1024).toFixed(2) + 'MB'); const filename = getFilenameFromPageTitle('mp4'); debugLog('[M3U8] Converting TS to MP4 with filename: ' + filename); const converted = await convertTStoMP4(merged, filename); debugLog('[M3U8] MP4 conversion complete!'); downloadedBlobs.set(videoData.url, { blob: converted.blob, filename: converted.filename }); debugLog('[M3U8] Cached MP4 blob for future use'); await shareOrDownloadBlob(converted.blob, converted.filename, videoData); } catch(e) { debugLog('[ERROR] M3U8 download/conversion failed: ' + e.message); showNotification('❌ Failed: ' + e.message, 'error'); resetProgress(); } } async function shareOrDownloadBlob(blob, filename, videoData) { debugLog('='.repeat(50)); debugLog('[SHARE/DL] SHARE OR DOWNLOAD BLOB'); debugLog('[SHARE/DL] Blob size: ' + (blob.size/1024/1024).toFixed(2) + 'MB'); debugLog('[SHARE/DL] Filename: ' + filename); if (navigator.share) { try { debugLog('[SHARE/DL] Creating File object with video/mp4 MIME type...'); var file = new File([blob], filename, { type: 'video/mp4' }); debugLog('[SHARE/DL] File created: ' + file.name + ', size: ' + file.size + ', type: ' + file.type); var canShareFiles = true; if (navigator.canShare) { canShareFiles = navigator.canShare({ files: [file] }); debugLog('[SHARE/DL] canShare result: ' + canShareFiles); } if (canShareFiles) { showNotification('📤 Opening share...', 'info'); debugLog('[SHARE/DL] Calling navigator.share...'); var shareStartTime = Date.now(); try { await navigator.share({ files: [file], title: filename, text: 'Video file' }); var elapsed = Date.now() - shareStartTime; debugLog('[SHARE/DL] ✅ Share completed successfully (took ' + elapsed + 'ms)'); showNotification('✅ Shared!', 'success'); resetProgress(); return; } catch(shareError) { var elapsed = Date.now() - shareStartTime; debugLog('[SHARE/DL] Share error after ' + elapsed + 'ms: ' + shareError.name + ' - ' + shareError.message); if (elapsed > 2000) { debugLog('[SHARE/DL] Assuming success (dialog was open >2s)'); showNotification('✅ Share completed', 'success'); resetProgress(); return; } debugLog('[SHARE/DL] Share cancelled/failed quickly, falling back to download'); } } else { debugLog('[SHARE/DL] Cannot share files on this device/browser'); } } catch(e) { debugLog('[SHARE/DL] Share setup error: ' + e.message); } } else { debugLog('[SHARE/DL] navigator.share not available'); } debugLog('[SHARE/DL] Initiating download...'); showNotification('💾 Downloading...', 'info'); var blobUrl = URL.createObjectURL(blob); debugLog('[SHARE/DL] Blob URL created: ' + blobUrl.substring(0, 50) + '...'); var a = document.createElement('a'); a.href = blobUrl; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); debugLog('[SHARE/DL] Triggering download click...'); a.click(); setTimeout(function() { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); debugLog('[SHARE/DL] Download cleanup complete'); }, 1000); showNotification('✅ Downloaded: ' + filename, 'success'); resetProgress(); debugLog('[SUCCESS] Download complete!'); } function shareVideo(videoData) { debugLog('='.repeat(50)); debugLog('[SHARE] shareVideo() called'); debugLog('[SHARE] Type: ' + videoData.type); debugLog('[SHARE] URL: ' + videoData.url); if (videoData.type === 'm3u8') { debugLog('[SHARE] *** M3U8 DETECTED - DOWNLOADING AND CONVERTING ***'); debugLog('[SHARE] Will NOT share playlist URL, will download/convert to MP4'); downloadM3U8(videoData, false); return; } debugLog('[SHARE] Regular video file detected'); if (navigator.share) { debugLog('[SHARE] Attempting to share video URL...'); var shareStartTime = Date.now(); navigator.share({ title: document.title, url: videoData.url }).then(function() { debugLog('[SHARE] ✅ URL shared successfully'); showNotification('✅ Shared!', 'success'); }).catch(function(error) { var elapsed = Date.now() - shareStartTime; if (elapsed > 2000) { debugLog('[SHARE] Assuming success (elapsed: ' + elapsed + 'ms)'); showNotification('✅ Share completed', 'success'); } else { debugLog('[SHARE] Share cancelled, trying copy: ' + error.message); copyVideoUrl(videoData); } }); } else { debugLog('[SHARE] navigator.share not available, trying copy'); copyVideoUrl(videoData); } } function downloadVideo(videoData) { showNotification('📂 Opening video...', 'info'); debugLog('[INFO] Opening video in new tab: ' + videoData.url); window.open(videoData.url, '_blank'); } async function copyVideoUrl(videoData) { debugLog('[COPY] Attempting to copy URL: ' + videoData.url); try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(videoData.url); debugLog('[COPY] ✅ URL copied via Clipboard API'); showNotification('✅ URL copied', 'success'); } else { var textArea = document.createElement('textarea'); textArea.value = videoData.url; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.select(); var success = document.execCommand('copy'); document.body.removeChild(textArea); if (success) { debugLog('[COPY] ✅ URL copied via execCommand'); showNotification('✅ URL copied', 'success'); } else { throw new Error('execCommand failed'); } } } catch(e) { debugLog('[COPY] ❌ Failed to copy URL: ' + e.message); showNotification('❌ Copy failed', 'error'); } } function showNotification(message, type) { if (type === undefined) type = 'success'; debugLog('[NOTIF] ' + message); 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: 75px; left: 15px; background: ${bgColor}; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: ${COLORS.text}; padding: 10px 14px; border-radius: 8px; z-index: 2147483646; font-weight: 600; font-size: 13px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); max-width: 280px; word-wrap: break-word; transform: translateX(-350px); 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(-350px)'; setTimeout(function() { notification.remove(); }, 300); } }, 4000); } 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() { debugLog('='.repeat(50)); debugLog('[INIT] Universal Video Share v6.7 - OVERLAY BUTTON'); debugLog('[INIT] Button always on top with highest z-index'); debugLog('[INIT] Semi-transparent design'); debugLog('='.repeat(50)); setTimeout(checkForVideos, 1000); checkInterval = setInterval(checkForVideos, 5000); } init(); })();