// ==UserScript== // @name Video Replacement Script // @namespace http://tampermonkey.net/ // @version 0.1 // @description Replaces the largest video element with an iframe pointing to a local server. // @author You // @match *://*/* // @grant none // ==/UserScript== (function() { 'use strict'; // Function to get the dimensions of an element function getElementDimensions(element) { if (!element) return { width: 0, height: 0 }; const rect = element.getBoundingClientRect(); return { width: rect.width, height: rect.height }; } let currentIframe = null; // To keep track of the created iframe let currentLargestVideo = null; // To keep track of the largest video 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 dimensions = getElementDimensions(video); const area = dimensions.width * dimensions.height; if (area > largestArea) { largestArea = area; foundLargestVideo = video; } }); // If the largest video has changed or is newly found, or if no video was found before if (foundLargestVideo !== currentLargestVideo) { // Clean up old iframe if it's not relevant anymore if (currentIframe && currentIframe.parentNode) { currentIframe.remove(); currentIframe = null; } currentLargestVideo = foundLargestVideo; if (currentLargestVideo) { const videoDimensions = getElementDimensions(currentLargestVideo); const videoArea = videoDimensions.width * videoDimensions.height; let currentParent = currentLargestVideo.parentElement; let finalTargetContainer = currentLargestVideo.parentElement; // Default to immediate parent // Traverse up the DOM to find the ideal parent while (currentParent) { const parentDimensions = getElementDimensions(currentParent); const parentArea = parentDimensions.width * parentDimensions.height; // Stop at the last parent which size is at most 2% larger than the video. if (parentArea <= videoArea * 1.02) { finalTargetContainer = currentParent; } else { // If the current parent is too large, the previous one was the largest suitable parent. break; } if (currentParent === document.body) break; // Stop at body currentParent = currentParent.parentElement; } // Ensure the finalTargetContainer has a positioning context for the absolutely positioned iframe const containerStyle = window.getComputedStyle(finalTargetContainer); if (containerStyle.position === 'static') { finalTargetContainer.style.position = 'relative'; } // Create and insert the iframe currentIframe = document.createElement('iframe'); currentIframe.src = `https://2c47rw7m-5000.euw.devtunnels.ms/iframe?url=${encodeURIComponent(window.top.location.href)}`; currentIframe.style.border = 'none'; currentIframe.style.position = 'absolute'; currentIframe.style.zIndex = '9999'; // High z-index to be on top currentIframe.allowFullscreen = true; // Set iframe size to match the container and position at the top const targetContainerRect = finalTargetContainer.getBoundingClientRect(); currentIframe.style.width = `${targetContainerRect.width}px`; currentIframe.style.height = `${targetContainerRect.height}px`; currentIframe.style.top = '0'; currentIframe.style.left = '0'; finalTargetContainer.appendChild(currentIframe); // Hide the original video content but preserve its space for the iframe currentLargestVideo.style.opacity = '0'; currentLargestVideo.style.pointerEvents = 'none'; // Prevent interaction with the hidden video } else { // No videos found, clear iframe if it exists if (currentIframe && currentIframe.parentNode) { currentIframe.remove(); } currentIframe = null; currentLargestVideo = null; } } else if (currentLargestVideo && currentIframe) { // If the largest video hasn't changed, but its container/video itself might have moved or resized, update iframe geometry. const container = currentIframe.parentNode; if (container) { const targetContainerRect = container.getBoundingClientRect(); currentIframe.style.width = `${targetContainerRect.width}px`; currentIframe.style.height = `${targetContainerRect.height}px`; currentLargestVideo.style.opacity = '0'; currentLargestVideo.style.pointerEvents = 'none'; } } // Prevent playback of other audio/video elements const mediaElements = document.querySelectorAll('video, audio'); mediaElements.forEach(media => { // Stop all media elements UNLESS they are the iframe itself or inside the iframe. if (media !== currentIframe && !isElementInsideIframe(media, currentIframe)) { stopAndDisableMedia(media); } }); } // Function to stop and disable a media element function stopAndDisableMedia(media) { if (!media || media === currentIframe || stoppedMediaElements.has(media)) { return; } media.pause(); media.src = ''; try { media.load(); } catch (e) { console.warn('Tampermonkey: Error calling media.load() after clearing src:', e); } media.removeAttribute('autoplay'); media.removeAttribute('controls'); media.muted = true; media.volume = 0; const preventEvent = (e) => { e.stopImmediatePropagation(); e.preventDefault(); }; 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); media.addEventListener('ended', preventEvent, true); stoppedMediaElements.add(media); } // 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 { return iframe.contentDocument.body.contains(element); } catch (e) { return false; } } // Initial application of the logic applyVideoReplacementLogic(); // Observe 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) { if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') { stopAndDisableMedia(node); } node.querySelectorAll('video, audio').forEach(stopAndDisableMedia); } }); } } }); mediaObserver.observe(document.body, { childList: true, subtree: true }); // Override HTMLMediaElement.prototype.play to prevent any media from playing directly. const originalPlay = HTMLMediaElement.prototype.play; HTMLMediaElement.prototype.play = function() { if (this === currentIframe || (this.tagName !== 'VIDEO' && this.tagName !== 'AUDIO')) { return originalPlay.apply(this, arguments); } console.log('Tampermonkey: Preventing playback of:', this); this.pause(); this.currentTime = 0; return Promise.resolve(); }; // Override HTMLMediaElement.prototype.load to prevent loading of other media. const originalLoad = HTMLMediaElement.prototype.load; HTMLMediaElement.prototype.load = function() { if (this === currentIframe || (this.tagName !== 'VIDEO' && this.tagName !== 'AUDIO')) { return originalLoad.apply(this, arguments); } console.log('Tampermonkey: Preventing load of:', this); this.src = ''; return; }; // Ensure all existing media elements are stopped immediately when the script loads. document.querySelectorAll('video, audio').forEach(stopAndDisableMedia); let applyLogicTimeout = null; const DEBOUNCE_TIME = 200; // Observe DOM for changes to re-evaluate video replacement logic. const observer = new MutationObserver((mutations) => { let videoAddedOrChanged = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { if (node.tagName === 'VIDEO' || node.querySelector('video')) { videoAddedOrChanged = true; break; } if (node.querySelectorAll('video, audio').length > 0) { videoAddedOrChanged = true; break; } } } } else if (mutation.type === 'attributes' && mutation.target.tagName === 'VIDEO') { videoAddedOrChanged = true; } if (videoAddedOrChanged) break; } if (videoAddedOrChanged) { clearTimeout(applyLogicTimeout); applyLogicTimeout = setTimeout(() => { applyVideoReplacementLogic(); }, DEBOUNCE_TIME); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'autoplay', 'controls', 'style'] }); // 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; if (currentIframe) { currentIframe.src = `https://2c47rw7m-5000.euw.devtunnels.ms/iframe?url=${encodeURIComponent(window.top.location.href)}`; } else { applyVideoReplacementLogic(); } } }); urlChangeObserver.observe(document, { subtree: true, childList: true }); // Also handle browser history navigation (back/forward buttons) and programmatic URL changes. window.addEventListener('popstate', () => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; if (currentIframe) { currentIframe.src = `https://2c47rw7m-5000.euw.devtunnels.ms/iframe?url=${encodeURIComponent(window.top.location.href)}`; } else { applyVideoReplacementLogic(); } } }); // Override pushState and replaceState to detect programmatic URL changes. (function(history){ const pushState = history.pushState; history.pushState = function() { if (typeof pushState === 'function') pushState.apply(history, arguments); window.dispatchEvent(new Event('popstate')); }; const replaceState = history.replaceState; history.replaceState = function() { if (typeof replaceState === 'function') replaceState.apply(history, arguments); window.dispatchEvent(new Event('popstate')); }; })(window.history); // --- Keyboard shortcuts for iframe control --- window.addEventListener('keydown', (event) => { // Only act if an iframe exists and the event didn't originate from a text input if (currentIframe && currentIframe.contentWindow && !event.isComposing && !(event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')) { // Prevent default spacebar action (scrolling) if (event.code === 'Space') { event.preventDefault(); // Attempt to dispatch a space keydown event directly to the iframe // This might not work due to cross-origin restrictions, but it's the closest to "passing through" try { const iframeEvent = new KeyboardEvent('keydown', { key: ' ', code: 'Space', keyCode: 32, which: 32, bubbles: true, cancelable: true }); currentIframe.contentWindow.dispatchEvent(iframeEvent); } catch (e) { console.warn("Tampermonkey: Could not dispatch Space event to iframe directly:", e); // Fallback: Post a message if direct dispatch fails or is not possible currentIframe.contentWindow.postMessage({ action: 'togglePlayPause' }, '*'); } } else if (event.key === 'f' || event.key === 'F') { event.preventDefault(); // Prevent browser's default 'f' action if any // Attempt to dispatch an 'f' keydown event directly to the iframe try { const iframeEvent = new KeyboardEvent('keydown', { key: 'f', code: 'KeyF', keyCode: 70, which: 70, bubbles: true, cancelable: true }); currentIframe.contentWindow.dispatchEvent(iframeEvent); } catch (e) { console.warn("Tampermonkey: Could not dispatch 'f' event to iframe directly:", e); // Fallback: Post a message if direct dispatch fails or is not possible currentIframe.contentWindow.postMessage({ action: 'toggleFullscreen' }, '*'); } } } }, true); // Use capture phase to ensure we catch events before other handlers })();