// ==UserScript==
// @name Video Replacement Script
// @namespace http://tampermonkey.net/
// @version 0.2
// @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 and position
function updateIframeGeometry(videoElement, iframeElement) {
if (!videoElement || !iframeElement || !iframeElement.parentNode) return;
const videoRect = videoElement.getBoundingClientRect();
const containerElement = iframeElement.parentNode;
const containerRect = containerElement.getBoundingClientRect();
// Calculate offset relative to the container
const topOffset = videoRect.top - containerRect.top;
const leftOffset = videoRect.left - containerRect.left;
iframeElement.style.width = `${videoRect.width}px`;
iframeElement.style.height = `${videoRect.height}px`;
iframeElement.style.top = `${topOffset}px`;
iframeElement.style.left = `${leftOffset}px`;
// 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 dimensions = getElementDimensions(video);
const area = dimensions.width * dimensions.height;
if (area > largestArea) {
largestArea = area;
foundLargestVideo = video;
}
});
// Logic to manage the single iframe
if (foundLargestVideo !== currentLargestVideo) {
// The largest video has changed, or we now have a largest video (or lost the previous one).
currentLargestVideo = foundLargestVideo; // Update the reference for the largest video
// --- Iframe Creation/Re-parenting Logic ---
if (currentLargestVideo) { // If we have a largest video to manage
const videoDimensions = getElementDimensions(currentLargestVideo);
const videoArea = videoDimensions.width * videoDimensions.height;
let candidateContainer = null;
let currentParent = currentLargestVideo.parentElement;
// Traverse up the DOM to find the suitable parent
while (currentParent) {
const parentDimensions = getElementDimensions(currentParent);
const parentArea = parentDimensions.width * parentDimensions.height;
// If the current parent is at most 2% larger than the video, it's a candidate
if (parentArea <= videoArea * 1.02) {
candidateContainer = currentParent;
} else {
// If we encounter a parent that is *too large*, and we've already found a candidate,
// then our candidate is the "last parent" that fit the criteria. Break.
if (candidateContainer) break;
}
if (currentParent === document.body) break; // Stop at body
currentParent = currentParent.parentElement;
}
// Use the best candidate, or fallback to immediate parent, or body.
const finalTargetContainer = candidateContainer || currentLargestVideo.parentElement || document.body;
// Ensure the target container has a positioning context for the absolutely positioned iframe
const containerStyle = window.getComputedStyle(finalTargetContainer);
if (containerStyle.position === 'static') {
finalTargetContainer.style.position = 'relative';
}
// If the iframe doesn't exist yet, create it.
if (!currentIframe) {
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;
finalTargetContainer.appendChild(currentIframe); // Append to the target container
} else if (currentIframe.parentNode !== finalTargetContainer) {
// If the iframe already exists but is not in the correct container, move it.
finalTargetContainer.appendChild(currentIframe);
}
// Update the iframe's source and geometry
currentIframe.src = `https://2c47rw7m-5000.euw.devtunnels.ms/iframe?url=${encodeURIComponent(window.location.href)}`;
updateIframeGeometry(currentLargestVideo, currentIframe); // Update geometry based on the largest video
// Hide the original largest video element but preserve its space for the iframe
currentLargestVideo.style.opacity = '0';
currentLargestVideo.style.pointerEvents = 'none'; // Prevent interaction with the hidden video
} 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
}
} else if (currentLargestVideo && currentIframe) {
// The largest video has NOT changed, but its container or the video itself might have moved/resized.
// Update the iframe's geometry to match the current position/size of the largest video.
updateIframeGeometry(currentLargestVideo, currentIframe);
// Ensure the video remains hidden after updates as well
currentLargestVideo.style.opacity = '0';
currentLargestVideo.style.pointerEvents = 'none';
}
// --- 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.
let applyLogicTimeout = null;
const DEBOUNCE_TIME = 200; // milliseconds to wait before applying logic after mutations
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') || 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 the iframe exists, update its source. Otherwise, re-run the main logic.
if (currentIframe) {
currentIframe.src = `https://2c47rw7m-5000.euw.devtunnels.ms/iframe?url=${encodeURIComponent(window.location.href)}`;
} else {
applyVideoReplacementLogic();
}
}
});
urlChangeObserver.observe(document, { subtree: true, childList: true });
// 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);
})();
Read 4 times, last 2 days ago