// ==UserScript== // @name Video Replacement Script // @namespace http://tampermonkey.net/ // @version 0.3 // @description Replaces the largest video element with an iframe pointing to a local server, hovering it absolutely. // @author You // @match *://*/* // @grant none // ==/UserScript== (function() { 'use strict'; // Function to get the dimensions and position of an element function getElementRect(element) { if (!element) return { width: 0, height: 0, top: 0, left: 0 }; return element.getBoundingClientRect(); } // Helper function to update iframe geometry and position absolutely over the video function updateIframeGeometryAbsolute(videoElement, iframeElement) { if (!videoElement || !iframeElement) return; const videoRect = getElementRect(videoElement); iframeElement.style.width = `${videoRect.width}px`; iframeElement.style.height = `${videoRect.height}px`; iframeElement.style.top = `${videoRect.top + window.scrollY}px`; // Account for scroll position iframeElement.style.left = `${videoRect.left + window.scrollX}px`; // Account for scroll position // iframeElement.style.border = '2px solid red'; // Temporary border for debugging visibility } let currentIframe = null; // To keep track of the created iframe let currentLargestVideo = null; // To keep track of the largest video currently being managed let stoppedMediaElements = new Set(); // To keep track of media elements that have been permanently stopped function applyVideoReplacementLogic() { const videoElements = document.querySelectorAll('video'); let foundLargestVideo = null; let largestArea = 0; videoElements.forEach(video => { const rect = getElementRect(video); const area = rect.width * rect.height; if (area > largestArea) { largestArea = area; foundLargestVideo = video; } }); // Logic to manage the single iframe if (foundLargestVideo) { // A largest video is found. if (!currentIframe) { // If the iframe doesn't exist yet, create it and append to body. currentIframe = document.createElement('iframe'); currentIframe.style.border = 'none'; currentIframe.style.position = 'absolute'; currentIframe.style.zIndex = '9999'; // High z-index to be on top currentIframe.allowFullscreen = true; document.body.appendChild(currentIframe); } // Update iframe source only if it's new or the URL changed const newIframeSrc = `https://2c47rw7m-5000.euw.devtunnels.ms/iframe?url=${encodeURIComponent(window.location.href)}`; if (currentIframe.src !== newIframeSrc) { currentIframe.src = newIframeSrc; } // Always update geometry to match the video, as its position/size might change. updateIframeGeometryAbsolute(foundLargestVideo, currentIframe); // Hide the original largest video element but preserve its space for the iframe // Only if it's a *new* largest video or it was hidden before. if (foundLargestVideo !== currentLargestVideo) { if (currentLargestVideo) { // Make sure the old largest video is visible again if it's no longer managed. currentLargestVideo.style.opacity = ''; currentLargestVideo.style.pointerEvents = ''; } currentLargestVideo = foundLargestVideo; // Update the reference for the largest video currentLargestVideo.style.opacity = '0'; currentLargestVideo.style.pointerEvents = 'none'; // Prevent interaction with the hidden video } else if (currentLargestVideo.style.opacity !== '0') { // Ensure it stays hidden if it's still the current largest video. currentLargestVideo.style.opacity = '0'; currentLargestVideo.style.pointerEvents = 'none'; } } else { // No largest video found // If no largest video is found, remove the iframe if it exists. if (currentIframe && currentIframe.parentNode) { currentIframe.remove(); } currentIframe = null; // Reset iframe reference // If there was a previously managed video, make it visible again if (currentLargestVideo) { currentLargestVideo.style.opacity = ''; currentLargestVideo.style.pointerEvents = ''; currentLargestVideo = null; } } // --- Media Playback Prevention Logic --- // Stop all other audio/video elements on the page. const mediaElements = document.querySelectorAll('video, audio'); mediaElements.forEach(media => { // Stop media UNLESS it is the iframe itself, or an element *inside* the iframe. if (media !== currentIframe && !isElementInsideIframe(media, currentIframe)) { stopAndDisableMedia(media); } }); } // Function to stop and disable a media element function stopAndDisableMedia(media) { // Ensure we don't try to stop the iframe itself or null/undefined elements, // and only process elements that haven't been stopped yet. if (!media || media === currentIframe || stoppedMediaElements.has(media)) { return; } media.pause(); media.src = ''; // Clear src to prevent future playback try { media.load(); // Reload to apply src change. This might throw an error if src is already invalid. } catch (e) { console.warn('Tampermonkey: Error calling media.load() after clearing src:', e); } media.removeAttribute('autoplay'); media.removeAttribute('controls'); media.muted = true; // Mute it forcefully media.volume = 0; // Set volume to 0 // Prevent event listeners from re-enabling playback by adding more listeners and using preventDefault. const preventEvent = (e) => { e.stopImmediatePropagation(); e.preventDefault(); }; // Add listeners for common media events that might trigger playback. media.addEventListener('play', preventEvent, true); media.addEventListener('playing', preventEvent, true); media.addEventListener('canplay', preventEvent, true); media.addEventListener('canplaythrough', preventEvent, true); media.addEventListener('loadeddata', preventEvent, true); media.addEventListener('loadedmetadata', preventEvent, true); media.addEventListener('progress', preventEvent, true); media.addEventListener('seeking', preventEvent, true); media.addEventListener('seeked', preventEvent, true); media.addEventListener('timeupdate', preventEvent, true); // Added for more robustness media.addEventListener('ended', preventEvent, true); // Added for more robustness stoppedMediaElements.add(media); // Mark this media element as stopped } // Helper function to check if an element is inside the iframe's document function isElementInsideIframe(element, iframe) { if (!iframe || !iframe.contentDocument || !iframe.contentDocument.body) { return false; } try { // Check if the element is the iframe itself, or a descendant of the iframe's document body. // This will throw if cross-origin. return iframe.contentDocument.body.contains(element); } catch (e) { // Cross-origin iframe security might prevent access. // In such cases, if we can't access, we assume it's not inside for safety (to prevent stopping the iframe's media). return false; } } // --- Initial Setup --- // Ensure all existing media elements are stopped immediately when the script loads. document.querySelectorAll('video, audio').forEach(stopAndDisableMedia); // Apply the main logic once initially. applyVideoReplacementLogic(); // --- Mutation Observers and Event Listeners --- // Interval to re-apply logic and stop rogue media. This is a fallback. setInterval(applyVideoReplacementLogic, 1000); // Every second // Observer for new media elements being added to the DOM. const mediaObserver = new MutationObserver(mutationsList => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { // Element node if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') { stopAndDisableMedia(node); } // Also check for media elements within newly added subtrees. node.querySelectorAll('video, audio').forEach(stopAndDisableMedia); } }); } } }); mediaObserver.observe(document.body, { childList: true, subtree: true }); // Observer for general DOM changes to re-evaluate video replacement logic. // This includes changes to attributes that might affect video size/position. let applyLogicTimeout = null; const DEBOUNCE_TIME = 100; // milliseconds to wait before applying logic after mutations const observer = new MutationObserver((mutations) => { let videoChangeDetected = false; for (const mutation of mutations) { if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) { // If nodes are added or removed, it might affect video presence or layout. videoChangeDetected = true; } else if (mutation.type === 'attributes') { // If attributes on a video element or its parents change, it might affect size/position. if (mutation.target.tagName === 'VIDEO' || (mutation.target.closest('video') && mutation.attributeName === 'style')) { videoChangeDetected = true; } else if (mutation.target.tagName === 'BODY' || mutation.target.tagName === 'HTML' || mutation.target.contains(currentLargestVideo)) { // Also consider changes to body/html or parents of the current largest video. videoChangeDetected = true; } } if (videoChangeDetected) break; } if (videoChangeDetected) { clearTimeout(applyLogicTimeout); applyLogicTimeout = setTimeout(() => { applyVideoReplacementLogic(); }, DEBOUNCE_TIME); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'src', 'autoplay', 'controls', 'width', 'height'] }); // Detect URL changes for single-page applications. let lastUrl = window.location.href; const urlChangeObserver = new MutationObserver(() => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; applyVideoReplacementLogic(); // Re-evaluate everything on URL change. } }); urlChangeObserver.observe(document, { subtree: true, childList: true }); // Observe document for broad changes in SPAs // Handle browser history navigation (back/forward buttons) and programmatic URL changes. window.addEventListener('popstate', () => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; applyVideoReplacementLogic(); } }); window.addEventListener('hashchange', () => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; applyVideoReplacementLogic(); } }); // Override HTMLMediaElement.prototype.play to prevent any media from playing directly. const originalPlay = HTMLMediaElement.prototype.play; HTMLMediaElement.prototype.play = function() { // Allow play only if it's the iframe itself, or if it's not a video/audio element we are actively managing. if (this === currentIframe || (this.tagName !== 'VIDEO' && this.tagName !== 'AUDIO')) { return originalPlay.apply(this, arguments); } // For other media elements, prevent playback. console.log('Tampermonkey: Preventing playback of:', this); this.pause(); this.currentTime = 0; // Reset playback position return Promise.resolve(); // Return a resolved promise to mimic play() behavior. }; // Override HTMLMediaElement.prototype.load to prevent loading of other media. const originalLoad = HTMLMediaElement.prototype.load; HTMLMediaElement.prototype.load = function() { // Allow load only if it's the iframe itself, or if it's not a video/audio element we are actively managing. if (this === currentIframe || (this.tagName !== 'VIDEO' && this.tagName !== 'AUDIO')) { return originalLoad.apply(this, arguments); } console.log('Tampermonkey: Preventing load of:', this); this.src = ''; // Clear src if an attempt to load occurs. return; }; // Listen for scroll events to reposition the iframe window.addEventListener('scroll', () => { if (currentIframe && currentLargestVideo) { updateIframeGeometryAbsolute(currentLargestVideo, currentIframe); } }, { passive: true }); // Listen for resize events to adjust iframe size/position window.addEventListener('resize', () => { clearTimeout(applyLogicTimeout); // Clear any pending DOM mutation debounce applyLogicTimeout = setTimeout(() => { applyVideoReplacementLogic(); }, DEBOUNCE_TIME); }); // Override pushState and replaceState to detect programmatic URL changes. (function(history){ const pushState = history.pushState; history.pushState = function() { const originalResult = pushState.apply(history, arguments); window.dispatchEvent(new Event('popstate')); // Trigger popstate event for consistency. return originalResult; }; const replaceState = history.replaceState; history.replaceState = function() { const originalResult = replaceState.apply(history, arguments); window.dispatchEvent(new Event('popstate')); // Trigger popstate event for consistency. return originalResult; }; })(window.history); })();