// ==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 <html> 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);
})();
Read 4 times, last 2 days ago