// ==UserScript== // @name Universal Video Share Button with M3U8 Support // @namespace http://tampermonkey.net/ // @version 5.3 // @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 with MP4 encoding. // @author Minoa // @license MIT // @match *://*/* // @require https://cdn.jsdelivr.net/npm/m3u8-parser@4.7.1/dist/m3u8-parser.min.js // @require https://cdn.jsdelivr.net/npm/mux.js@6.3.0/dist/mux.min.js // @grant none // @downloadURL 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(); var downloadedBlobs = new Map(); var debugMode = false; var debugConsole = null; var debugLogs = []; var longPressStartTime = 0; // 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: '#55423d', buttonHover: '#6b5651', icon: '#ffc0ad', text: '#fff3ec' }; // 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 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'); } // ============= NEW FUNCTIONS FOR MP4 ENCODING ============= function isPlaylistUrl(url) { if (!url) return false; const lowerUrl = url.toLowerCase(); return lowerUrl.includes('.m3u8') || lowerUrl.includes('.m3u'); } async function parseM3U8(url) { try { debugLog('[PARSE M3U8] Fetching playlist: ' + 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; // If this is a master playlist, get the best variant if (manifest.playlists && manifest.playlists.length > 0 && (!manifest.segments || manifest.segments.length === 0)) { const bestVariant = manifest.playlists[manifest.playlists.length - 1]; const baseUrl = url.substring(0, url.lastIndexOf('/') + 1); const variantUrl = bestVariant.uri.startsWith('http') ? bestVariant.uri : baseUrl + bestVariant.uri; debugLog('[PARSE M3U8] Master playlist detected, parsing variant: ' + variantUrl); return await parseM3U8(variantUrl); } // Get segment URLs const baseUrl = url.substring(0, url.lastIndexOf('/') + 1); const segmentUrls = manifest.segments.map(seg => { return seg.uri.startsWith('http') ? seg.uri : baseUrl + seg.uri; }); debugLog('[PARSE M3U8] Found ' + segmentUrls.length + ' segments'); return segmentUrls; } catch (e) { debugLog('[ERROR] parseM3U8 failed: ' + e.message); throw e; } } async function convertTStoMP4(segmentUrls, onProgress) { try { debugLog('[CONVERT] Starting TS to MP4 conversion'); debugLog('[CONVERT] Segments to process: ' + segmentUrls.length); return new Promise(async (resolve, reject) => { const transmuxer = new muxjs.mp4.Transmuxer(); const mp4Segments = []; transmuxer.on('data', (segment) => { debugLog('[CONVERT] Received transmuxed segment'); if (segment.initSegment) { const initArray = new Uint8Array(segment.initSegment.byteLength); initArray.set(segment.initSegment, 0); mp4Segments.push(initArray); } const dataArray = new Uint8Array(segment.data.byteLength); dataArray.set(segment.data, 0); mp4Segments.push(dataArray); }); transmuxer.on('done', () => { debugLog('[CONVERT] Transmuxing complete, creating blob'); const mp4Blob = new Blob(mp4Segments, { type: 'video/mp4' }); debugLog('[CONVERT] MP4 blob created: ' + (mp4Blob.size / 1024 / 1024).toFixed(2) + 'MB'); resolve(mp4Blob); }); // Download and transmux segments for (let i = 0; i < segmentUrls.length; i++) { const segUrl = segmentUrls[i]; debugLog('[CONVERT] Downloading segment ' + (i + 1) + '/' + segmentUrls.length); try { const response = await fetch(segUrl); if (!response.ok) { throw new Error('Segment download failed: ' + response.status); } const data = await response.arrayBuffer(); transmuxer.push(new Uint8Array(data)); if (onProgress) { onProgress(i + 1, segmentUrls.length); } } catch (e) { debugLog('[ERROR] Segment download failed: ' + e.message); reject(e); return; } } transmuxer.flush(); }); } catch (e) { debugLog('[ERROR] convertTStoMP4 failed: ' + e.message); throw e; } } async function downloadAndConvertM3U8(videoData, forceDownload = false) { debugLog('='.repeat(50)); debugLog('[ENCODE] STARTING M3U8 DOWNLOAD WITH MP4 ENCODING'); debugLog('[ENCODE] URL: ' + videoData.url); debugLog('[ENCODE] Force download: ' + forceDownload); const cachedBlob = downloadedBlobs.get(videoData.url); if (cachedBlob && !forceDownload) { debugLog('[ENCODE] Using cached blob'); showNotification('♻️ Using cached video...', 'info'); const filename = cachedBlob.filename; await shareOrDownloadBlob(cachedBlob.blob, filename, videoData, forceDownload); return; } showNotification('📥 Starting M3U8 download...', 'info'); resetProgress(); try { // Parse M3U8 to get segment URLs const segmentUrls = await parseM3U8(videoData.url); if (!segmentUrls || segmentUrls.length === 0) { throw new Error('No segments found in playlist'); } showNotification(`🔄 Converting ${segmentUrls.length} segments to MP4...`, 'info'); // Convert TS segments to MP4 const mp4Blob = await convertTStoMP4(segmentUrls, (current, total) => { const percent = Math.floor((current / total) * 100); updateProgress(percent); if (percent % 10 === 0 || current === total) { showNotification(`🔄 Encoding: ${percent}% (${current}/${total})`, 'info'); } }); // Generate filename const urlPath = new URL(videoData.url).pathname; const baseName = urlPath.split('/').pop().replace(/\.(m3u8?|ts)$/i, '') || 'video'; const filename = baseName + '.mp4'; debugLog('[ENCODE] MP4 created: ' + filename + ' (' + (mp4Blob.size / 1024 / 1024).toFixed(2) + 'MB)'); // Cache the blob downloadedBlobs.set(videoData.url, { blob: mp4Blob, filename: filename }); showNotification(`✅ Encoded! ${(mp4Blob.size/1024/1024).toFixed(1)}MB`, 'success'); // Share or download await shareOrDownloadBlob(mp4Blob, filename, videoData, forceDownload); } catch (e) { debugLog('[ERROR] M3U8 encoding failed: ' + e.message); debugLog('[ERROR] Stack: ' + e.stack); showNotification('❌ Encoding failed: ' + e.message, 'error'); resetProgress(); // Fallback to copying URL debugLog('[ENCODE] Falling back to URL copy'); copyVideoUrl(videoData); } } // ============= END NEW FUNCTIONS ============= // 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: 99999999; 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 = `🐛 DEBUG CONSOLE`; 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]')) { 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')) { 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')) { 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 { checkUrlForVideo(value); } } } return originalSetAttribute.call(this, name, value); }; })(); function checkUrlForVideo(url) { try { if (url.startsWith('blob:')) 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 (url.startsWith('blob:')) return; url = new URL(url, location.href).href; // Get URL without query params for comparison const urlWithoutQuery = url.split('?')[0]; // Check if we already have a better version of this URL let alreadyHasBetter = false; for (const [existingUrl, existingData] of allDetectedVideos.entries()) { if (existingData.type === 'm3u8') { const existingWithoutQuery = existingUrl.split('?')[0]; if (existingWithoutQuery === urlWithoutQuery) { // Same base URL exists if (existingData.m3u8Data && existingData.m3u8Data.manifest && existingData.m3u8Data.manifest.segments && existingData.m3u8Data.manifest.segments.length > 0) { // Existing one has segments, skip this one 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; // CHECK IF THIS IS A MASTER PLAYLIST 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'); // Get the highest quality variant (usually the first one or last one) const bestVariant = manifest.playlists[manifest.playlists.length - 1]; // Try last one for highest quality debugLog('[M3U8] Selected variant: ' + JSON.stringify(bestVariant)); // Construct the full URL for the variant 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); // Remove this master from detected list const masterIndex = detectedM3U8Urls.indexOf(url); if (masterIndex > -1) { detectedM3U8Urls.splice(masterIndex, 1); } // Remove any existing entry with same base URL 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); } } } // Detect the variant instead detectM3U8(variantUrl); return; } // This is a media playlist with actual segments 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); // Remove any existing entry with same base URL before adding this one for (const [existingUrl, existingData] of allDetectedVideos.entries()) { if (existingData.type === 'm3u8') { const existingWithoutQuery = existingUrl.split('?')[0]; if (existingWithoutQuery === urlWithoutQuery) { // Only remove if the existing one has NO segments and this one has segments 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 && !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(); debugLog('[GET VIDEOS] Checking allDetectedVideos map...'); debugLog('[GET VIDEOS] Map size: ' + allDetectedVideos.size); allDetectedVideos.forEach(function(videoData, url) { debugLog('[GET VIDEOS] Map entry: ' + videoData.type + ' - ' + url); 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); videos.push(videoData); debugLog('[GET VIDEOS] Added to final list: ' + videoData.type + ' - ' + url); } }); 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); // CRITICAL: Check if URL is M3U8 before adding if (url && isM3U8Url(url)) { debugLog('[GET VIDEOS] Found M3U8 in video element, triggering detection: ' + url); detectM3U8(url); continue; // Don't add as video, let detectM3U8 handle it } 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); } else if (url && allDetectedVideos.has(url)) { debugLog('[GET VIDEOS] Already in map, skipping: ' + 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 && 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); 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; longPressStartTime = Date.now(); pressTimer = setTimeout(function() { isLongPress = true; var duration = Date.now() - longPressStartTime; if (duration >= 3000) { debugMode = true; floatingButton.style.background = 'rgba(239, 68, 68, 0.8)'; floatingButton.innerHTML = '🐛'; debugLog('='.repeat(50)); debugLog('[INFO] DEBUG MODE ACTIVATED'); debugLog('[INFO] Platform - iOS: ' + isIOS + ', Safari: ' + isSafari); debugLog('[INFO] User Agent: ' + navigator.userAgent); debugLog('[INFO] Share API: ' + !!navigator.share); debugLog('='.repeat(50)); } else { floatingButton.style.background = 'rgba(74, 222, 128, 0.8)'; floatingButton.innerHTML = '⎘'; } }, 500); setTimeout(function() { if (Date.now() - longPressStartTime >= 2900) { debugMode = true; floatingButton.style.background = 'rgba(239, 68, 68, 0.8)'; floatingButton.innerHTML = '🐛'; } }, 3000); }); 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 (debugMode) { createDebugConsole(); } else if (isLongPress && pressDuration < 3000) { handleCopy(); } else if (!isLongPress) { 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 >= 3000) { debugMode = true; floatingButton.style.background = 'rgba(239, 68, 68, 0.8)'; floatingButton.innerHTML = '🐛'; navigator.vibrate && navigator.vibrate(200); debugLog('='.repeat(50)); debugLog('[INFO] DEBUG MODE ACTIVATED'); debugLog('[INFO] Platform - iOS: ' + isIOS + ', Safari: ' + isSafari); debugLog('[INFO] User Agent: ' + navigator.userAgent); debugLog('[INFO] Share API: ' + !!navigator.share); debugLog('='.repeat(50)); } else { floatingButton.style.background = 'rgba(74, 222, 128, 0.8)'; floatingButton.innerHTML = '⎘'; navigator.vibrate && navigator.vibrate(100); } }, 500); setTimeout(function() { if (Date.now() - longPressStartTime >= 2900) { debugMode = true; floatingButton.style.background = 'rgba(239, 68, 68, 0.8)'; floatingButton.innerHTML = '🐛'; navigator.vibrate && navigator.vibrate(200); } }, 3000); }); 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 (debugMode) { createDebugConsole(); } else if (isLongPress && pressDuration < 3000) { handleCopy(); } else if (!isLongPress) { 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'); } // MODIFIED: Made async to support await async 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] Single video, calling shareVideo'); await shareVideo(videos[0]); } else { debugLog('[SHARE] Multiple videos, showing selector'); showVideoSelector(videos, 'share'); } } 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) { if (videos[0].type === 'm3u8') { debugLog('[COPY] M3U8 detected, forcing download'); showNotification('📥 Downloading M3U8...', 'info'); downloadM3U8(videos[0], true); } 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); 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 = `
` + currentVideoData.title + `
` + currentVideoData.url + '
'; videoItem.appendChild(videoInfo); // MODIFIED: Made async to support await videoItem.addEventListener('click', async function() { popup.remove(); if (action === 'share') { await 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'); debugLog('[M3U8] URL: ' + videoData.url); debugLog('[M3U8] Force download: ' + forceDownload); debugLog('[M3U8] Platform - iOS: ' + isIOS + ', Safari: ' + isSafari); const cachedBlob = downloadedBlobs.get(videoData.url); if (cachedBlob && !forceDownload) { debugLog('[M3U8] Using cached blob'); showNotification('♻️ Using cached video...', 'info'); const filename = cachedBlob.filename; await shareOrDownloadBlob(cachedBlob.blob, filename, videoData, forceDownload); return; } showNotification('📥 Starting M3U8 download...', 'info'); resetProgress(); try { const manifest = videoData.m3u8Data.manifest; debugLog('[M3U8] Manifest playlists: ' + (manifest.playlists ? manifest.playlists.length : 0)); debugLog('[M3U8] Manifest segments: ' + (manifest.segments ? manifest.segments.length : 0)); const baseUrl = videoData.url.substring(0, videoData.url.lastIndexOf('/') + 1); debugLog('[M3U8] Base URL: ' + baseUrl); const segments = manifest.segments; if (!segments || segments.length === 0) { const errorMsg = "No segments in playlist! This is a master playlist."; debugLog('[ERROR] ' + errorMsg); showNotification('❌ ' + errorMsg, 'error'); throw new Error(errorMsg); } 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; debugLog('[M3U8] Segment ' + (i+1) + '/' + segments.length + ': ' + segUrl); 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); debugLog('[M3U8] Progress: ' + percent + '%, Size: ' + (totalSize/1024/1024).toFixed(2) + 'MB'); 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 blob size: ' + (merged.size/1024/1024).toFixed(2) + 'MB'); var urlPath = new URL(videoData.url).pathname; var baseName = urlPath.split('/').pop().replace(/\.(m3u8?|ts)$/i, '') || 'video'; var filename = baseName + '-noenc.ts'; debugLog('[M3U8] Filename: ' + filename); downloadedBlobs.set(videoData.url, { blob: merged, filename: filename }); showNotification(`✅ Merged! ${(merged.size/1024/1024).toFixed(1)}MB`, 'info'); await shareOrDownloadBlob(merged, filename, videoData, forceDownload); } catch(e) { debugLog('[ERROR] M3U8 download failed: ' + e.message); debugLog('[ERROR] Stack: ' + e.stack); showNotification('❌ Failed: ' + e.message, 'error'); resetProgress(); } } async function shareOrDownloadBlob(blob, filename, videoData, forceDownload = false) { 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); debugLog('[SHARE/DL] Force download: ' + forceDownload); debugLog('[SHARE/DL] navigator.share exists: ' + !!navigator.share); debugLog('[SHARE/DL] navigator.canShare exists: ' + !!navigator.canShare); const processed = processedVideos.get(videoData.url); if (processed && !forceDownload) { debugLog('[SHARE/DL] Already processed, forcing download'); forceDownload = true; showNotification('🔄 Re-downloading...', 'info'); } if (!forceDownload && navigator.share) { debugLog('[SHARE/DL] Attempting to share...'); try { var file = new File([blob], filename, { type: blob.type }); debugLog('[SHARE/DL] Created File object: ' + file.name + ' (' + file.size + ' bytes)'); var canShareFiles = false; if (navigator.canShare) { canShareFiles = navigator.canShare({ files: [file] }); debugLog('[SHARE/DL] canShare result: ' + canShareFiles); } else { debugLog('[SHARE/DL] canShare not available, trying anyway'); canShareFiles = true; } 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 (took ' + elapsed + 'ms)'); processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() }); showNotification('✅ Shared!', 'success'); resetProgress(); return; } catch(e) { var elapsed = Date.now() - shareStartTime; debugLog('[SHARE/DL] Share error after ' + elapsed + 'ms: ' + e.name + ' - ' + e.message); if (elapsed > 2000) { debugLog('[SHARE/DL] Assuming success (>2s)'); processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() }); showNotification('✅ Share completed', 'success'); resetProgress(); return; } debugLog('[SHARE/DL] Share failed/cancelled, will download'); showNotification('⚠️ Share cancelled', 'info'); } } else { debugLog('[SHARE/DL] Cannot share files'); showNotification('⚠️ Share not supported', 'info'); } } catch(e) { debugLog('[ERROR] File/Share error: ' + e.message); showNotification('⚠️ Share error', 'info'); } } else { debugLog('[SHARE/DL] Skipping share (force=' + forceDownload + ', hasAPI=' + !!navigator.share + ')'); } debugLog('[SHARE/DL] Initiating download...'); showNotification('💾 Downloading...', 'info'); var blobUrl = URL.createObjectURL(blob); debugLog('[SHARE/DL] Blob URL: ' + blobUrl); var a = document.createElement('a'); a.href = blobUrl; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); debugLog('[SHARE/DL] Clicking download link...'); a.click(); setTimeout(function() { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); debugLog('[SHARE/DL] Cleanup complete'); }, 1000); processedVideos.set(videoData.url, { action: 'downloaded', timestamp: Date.now() }); showNotification('✅ Downloaded: ' + filename, 'success'); resetProgress(); debugLog('[SUCCESS] Download complete!'); } // MODIFIED: Added playlist detection and MP4 conversion async function shareVideo(videoData) { debugLog('='.repeat(50)); debugLog('[SHARE] shareVideo() called'); debugLog('[SHARE] Type: ' + videoData.type); debugLog('[SHARE] URL: ' + videoData.url); // Check if this is a playlist URL using the new function if (isPlaylistUrl(videoData.url) || videoData.type === 'm3u8') { debugLog('[SHARE] *** PLAYLIST DETECTED - CONVERTING TO MP4 ***'); showNotification('🎬 Converting M3U8 to MP4...', 'info'); try { // Use the new encoding function await downloadAndConvertM3U8(videoData, false); return; } catch (e) { debugLog('[ERROR] Conversion failed, falling back to URL copy: ' + e.message); showNotification('⚠️ Conversion failed, copying URL...', 'info'); copyVideoUrl(videoData); return; } } debugLog('[SHARE] Regular video, sharing URL'); 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'); debugLog('[SUCCESS] URL shared'); }).catch(function(error) { if (Date.now() - shareStartTime > 2000) { processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() }); showNotification('✅ Share completed', 'success'); debugLog('[SUCCESS] Share completed'); } else { debugLog('[INFO] Share cancelled: ' + error); copyVideoUrl(videoData); } }); } else { copyVideoUrl(videoData); } } function downloadVideo(videoData) { showNotification('📂 Opening video...', 'info'); debugLog('[INFO] Opening video in new tab: ' + videoData.url); 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'); debugLog('[SUCCESS] URL copied to clipboard'); }).catch(function(error) { if (Date.now() - copyStartTime > 2000) { processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() }); showNotification('✅ URL copied', 'success'); debugLog('[SUCCESS] URL copied'); } 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'); debugLog('[SUCCESS] URL copied (fallback)'); } else { showNotification('❌ Copy failed', 'error'); debugLog('[ERROR] Copy failed'); } document.body.removeChild(textArea); } catch (err) { showNotification('❌ Copy failed', 'error'); debugLog('[ERROR] Copy failed: ' + err.message); } } // MODIFIED: Added explicit 'info' type support with requested color 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)' : type === 'info' ? 'rgba(52, 152, 219, 0.95)' : '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: 10px 14px; border-radius: 8px; z-index: 999998; 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 v5.2 with MP4 Encoding'); debugLog('[INIT] License: MIT'); debugLog('[INIT] Platform - iOS: ' + isIOS + ', Safari: ' + isSafari + ', Mobile: ' + isMobile); debugLog('[INIT] User Agent: ' + navigator.userAgent); debugLog('[INIT] Share API: ' + !!navigator.share); debugLog('[INIT] Mux.js loaded: ' + (typeof muxjs !== 'undefined')); debugLog('[INIT] Hold button 3+ seconds for debug console'); debugLog('='.repeat(50)); setTimeout(checkForVideos, 1000); checkInterval = setInterval(checkForVideos, 5000); } init(); })();