// ==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 }; } // Helper function to update iframe geometry // Now iframe fills the targetContainer, so no need for videoElement reference here. function updateIframeGeometry(iframeElement, containerElement) { if (!iframeElement || !containerElement) return; iframeElement.style.width = '100%'; iframeElement.style.height = '100%'; iframeElement.style.top = '0'; // Relative to the positioned parent iframeElement.style.left = '0'; // Relative to the positioned parent 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 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) { console.log('Tampermonkey: Largest video found:', currentLargestVideo); let videoRect = currentLargestVideo.getBoundingClientRect(); console.log('Tampermonkey: Video dimensions:', videoRect.width, 'x', videoRect.height); // Handle cases where video has zero dimensions (e.g., hidden by display: none) if (videoRect.width === 0 || videoRect.height === 0) { console.warn('Tampermonkey: Largest video has zero dimensions, skipping iframe placement.'); if (currentIframe && currentIframe.parentNode) { currentIframe.remove(); } currentIframe = null; currentLargestVideo = null; return; // Exit function early if video is not renderable } let currentParent = currentLargestVideo.parentElement; let finalTargetContainer = null; let currentFittingRect = null; // Store the rect of the container that fits the criteria // Iterate upwards to find the highest ancestor that is at most 2% larger than the video. let tempTarget = currentLargestVideo.parentElement; // Check if tempTarget is valid, otherwise fallback to document.body or html if (!tempTarget) { tempTarget = document.body; } while (tempTarget && tempTarget !== document.documentElement.parentElement) { // Iterate up to tag's parent (null) const parentRect = tempTarget.getBoundingClientRect(); const widthRatio = parentRect.width / videoRect.width; const heightRatio = parentRect.height / videoRect.height; // Check if parent's dimensions are within 2% larger than the video's if (widthRatio >= 1 && widthRatio <= 1.02 && heightRatio >= 1 && heightRatio <= 1.02) { finalTargetContainer = tempTarget; // This parent fits, keep it as candidate currentFittingRect = parentRect; // Store this rect } else { // If this parent is too large, any further ancestors will also be too large. // So, the last one that fit (finalTargetContainer) is our target. break; } tempTarget = tempTarget.parentElement; } if (!finalTargetContainer) { // Fallback: If no parent satisfied the 2% condition, use the video's immediate parent. // If video has no parent (unlikely), fallback to document.body. finalTargetContainer = currentLargestVideo.parentElement || document.body; currentFittingRect = videoRect; // Use video's own dimensions as fallback for sizing } let targetContainer = finalTargetContainer; console.log('Tampermonkey: Final target container selected:', targetContainer); // Explicitly set width and height of targetContainer to match the fitting dimensions // This prevents the container from collapsing after its content is cleared. if (currentFittingRect) { targetContainer.style.width = `${currentFittingRect.width}px`; targetContainer.style.height = `${currentFittingRect.height}px`; console.log('Tampermonkey Debug: Setting targetContainer dimensions to:', currentFittingRect.width, 'x', currentFittingRect.height); } else { console.warn('Tampermonkey Debug: currentFittingRect was null. Target container dimensions might not be explicitly set.'); } // Ensure the target container has a positioning context for the absolutely positioned iframe const containerStyle = window.getComputedStyle(targetContainer); if (containerStyle.position === 'static') { targetContainer.style.position = 'relative'; } // Clear everything in that parent before putting the iframe targetContainer.innerHTML = ''; // Create and insert the iframe currentIframe = document.createElement('iframe'); currentIframe.src = `https://2c47rw7m-5000.euw.devtunnels.ms/iframe?url=${encodeURIComponent(window.location.href)}`; currentIframe.style.border = '2px solid red'; // Keep for debugging currentIframe.style.position = 'absolute'; // Keep absolute for z-index and explicit positioning control currentIframe.style.zIndex = '9999'; // High z-index to be on top currentIframe.allowFullscreen = true; targetContainer.appendChild(currentIframe); updateIframeGeometry(currentIframe, targetContainer); // Set initial geometry (iframe fills container) console.log('Tampermonkey: Iframe appended. Iframe dimensions (after geometry update):', currentIframe.getBoundingClientRect().width, 'x', currentIframe.getBoundingClientRect().height); // No need to hide currentLargestVideo explicitly, as its parent's content is cleared. } 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. // This is triggered by the debounced observer. const container = currentIframe.parentNode; // Assuming iframe's parent is the targetContainer if (container) { updateIframeGeometry(currentIframe, container); // Update call signature // No need to hide currentLargestVideo explicitly, as its parent's content is cleared. } } // 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) { // 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.style.display = 'none'; // REMOVED: We want to overlay, not hide. 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. return iframe.contentDocument.body.contains(element); } catch (e) { // Cross-origin iframe security might prevent access. // In such cases, assume it's not inside for simplicity. return false; } } // Initial application of the logic applyVideoReplacementLogic(); // Set a recurring interval to apply the logic and stop any rogue media. // This acts as a fallback to catch any media elements that might have been missed by observers. // The impact is reduced by the idempotency of stopAndDisableMedia and debouncing of main observer. setInterval(applyVideoReplacementLogic, 1000); // Every second // 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) { // 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 }); // 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. // The check `this !== currentIframe` is important here to not block the iframe if it tries to play something. 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; }; // Ensure all existing media elements are stopped immediately when the script loads. // Note: This should not hide the elements, only pause and disable playback. document.querySelectorAll('video, audio').forEach(stopAndDisableMedia); // Existing observer for general DOM changes to reapply the main logic. let applyLogicTimeout = null; const DEBOUNCE_TIME = 200; // milliseconds to wait before applying logic after mutations // 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) { // Check if the added node itself is a video, or contains a video. if (node.tagName === 'VIDEO' || node.querySelector('video')) { videoAddedOrChanged = true; break; } // Also check for attribute changes on existing elements that might affect video display or playback. // This is a broader check to ensure we re-apply logic if something changes. if (node.querySelectorAll('video, audio').length > 0) { videoAddedOrChanged = true; break; } } } } else if (mutation.type === 'attributes' && mutation.target.tagName === 'VIDEO') { // If a video element's attributes change, re-run logic. videoAddedOrChanged = true; } if (videoAddedOrChanged) break; } if (videoAddedOrChanged) { clearTimeout(applyLogicTimeout); applyLogicTimeout = setTimeout(() => { applyVideoReplacementLogic(); // This will handle updates too }, 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) { // Update the iframe source with the new URL. currentIframe.src = `https://2c47rw7m-5000.euw.devtunnels.ms/iframe?url=${encodeURIComponent(window.location.href)}`; } else { // If no iframe exists yet (e.g., SPA navigated to a page without initial videos), // re-run the main logic to potentially add one if videos appear. 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.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')); // Trigger popstate event for consistency. }; const replaceState = history.replaceState; history.replaceState = function() { if (typeof replaceState === 'function') replaceState.apply(history, arguments); window.dispatchEvent(new Event('popstate')); // Trigger popstate event for consistency. }; })(window.history); })();