// ==UserScript==
// @name Firefox 播放器
// @namespace http://tampermonkey.net/
// @version 1.21
// @description 采用非侵入式UI注入,精准保留原始布局。
// @author Xion.Ai
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const hideNativeControlsStyle = document.createElement('style');
hideNativeControlsStyle.id = '__mp_hide_native_controls';
hideNativeControlsStyle.textContent = `
video[controls]:not([data-__mp_attached]),
audio[controls]:not([data-__mp_attached]) {
background-color: #000; /* Prevent white flash before player loads */
visibility: hidden !important;
}
`;
(document.head || document.documentElement).appendChild(hideNativeControlsStyle);
// Re-add the centering logic for local file playback.
if (window.location.protocol === 'file:') {
document.addEventListener('DOMContentLoaded', () => {
if (document.body && document.body.childElementCount === 1 && document.body.firstElementChild && (document.body.firstElementChild.tagName === 'VIDEO' || document.body.firstElementChild.tagName === 'AUDIO')) {
const styleId = '__mp_center_style';
if (document.getElementById(styleId)) return;
const centerCss = `
html, body {
height: 100%;
margin: 0;
}
body {
display: grid;
place-items: center;
background-color: #000;
}
`;
const styleNode = document.createElement('style');
styleNode.id = styleId;
styleNode.textContent = centerCss;
document.head.appendChild(styleNode);
}
});
}
const css = `
/* Wrapper for audio elements - from v1.1 */
.__mp_wrapper {
position: relative;
display: inline-block;
vertical-align: bottom;
}
.__mp_wrapper > audio {
display: block;
width: 100%;
}
.__mp_wrapper.__mp_audio_wrapper {
height: 44px;
background-color: transparent;
}
/* Base UI for both video and audio */
.__mp_ui {
position: absolute;
height: 44px;
display: flex !important;
visibility: visible !important;
align-items: center;
gap: 8px;
pointer-events: auto;
opacity: 0;
transition: opacity 160ms ease, transform 160ms ease;
transform: translateY(6px);
z-index: 2147483647;
}
.__mp_ui.__show {
opacity: 1 !important;
transform: translateY(0);
}
/* Common button/progress/time styles */
.__mp_btn {
width: 36px; height: 36px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
background: rgba(255,255,255,0.08); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,0.06); color: rgba(255,255,255,0.95);
cursor: pointer; user-select: none; box-shadow: 0 2px 8px rgba(0,0,0,0.25); flex-shrink: 0;
}
.__mp_btn:hover { transform: scale(1.06); transition: transform 120ms; }
.__mp_progress_container {
position: relative;
flex: 1 1 auto;
margin: 0 8px;
height: 20px;
display: flex;
align-items: center;
cursor: pointer;
}
.__mp_progress {
position: relative;
-webkit-appearance: none; appearance: none; height: 4px; border-radius: 999px;
background: linear-gradient(to right, #fff var(--play-percent, 0%), #aaa var(--play-percent, 0%), #aaa var(--buffer-percent, 0%), rgba(255,255,255,0.18) var(--buffer-percent, 0%));
outline: none;
width: 100%;
margin: 0;
}
.__mp_progress::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 10px; height: 10px; border-radius: 50%;
background: transparent; border: none; box-shadow: none;
cursor: pointer; transition: background .15s ease;
}
.__mp_progress:hover::-webkit-slider-thumb {
background-color: rgba(255, 255, 255, 0.6) !important;
background-image: none !important;
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
}
.__mp_progress::-moz-range-thumb {
width: 10px; height: 10px; border-radius: 50%;
background: transparent; border: none; box-shadow: none;
cursor: pointer; transition: background .15s ease;
}
.__mp_progress:hover::-moz-range-thumb {
background-color: rgba(255, 255, 255, 0.6) !important;
background-image: none !important;
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
}
.__mp_time {
color: rgba(255,255,255,0.95);
font-size: 12px;
font-family: monospace;
user-select: none;
flex-shrink: 0;
}
video, audio { -webkit-user-select: none; -moz-user-select: none; user-select: none; }
`;
const styleNode = document.createElement('style');
styleNode.textContent = css;
document.head.appendChild(styleNode);
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const formatTime = (seconds) => {
if (isNaN(seconds) || seconds < 0) return '00:00';
const date = new Date(seconds * 1000);
const hh = date.getUTCHours();
const mm = date.getUTCMinutes();
const ss = date.getUTCSeconds().toString().padStart(2, '0');
if (hh) {
return `${hh}:${mm.toString().padStart(2, '0')}:${ss}`;
}
return `${mm}:${ss}`;
};
function attachUIToMedia(media) {
if (!media || media.dataset.__mp_attached) return;
media.dataset.__mp_attached = '1';
const isAudio = media.tagName === 'AUDIO';
const ui = document.createElement('div');
ui.className = '__mp_ui';
ui.innerHTML = `
<div class="__mp_btn __mp_play" title="Play/Pause"><svg width="14" height="14" viewBox="0 0 24 24"><path d="M8 5v14l11-7L8 5z" fill="currentColor"/></svg></div>
<div class="__mp_progress_container">
<input class="__mp_progress" type="range" min="0" max="100" value="0" step="0.01">
</div>
<div class="__mp_time">00:00 / 00:00</div>
<div class="__mp_btn __mp_mute" title="Mute/Unmute (Scroll to adjust volume)"><svg class="mp_icon_vol_svg" width="14" height="14" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" fill="currentColor"/></svg></div>
`;
let wrapper, originalMargin, resizeObserver;
let updateUIPosition;
if (isAudio) {
// --- AUDIO PATH (v1.1 wrapper logic) ---
const originalStyle = window.getComputedStyle(media);
originalMargin = originalStyle.margin;
wrapper = document.createElement('div');
wrapper.className = '__mp_wrapper __mp_audio_wrapper';
wrapper.style.display = originalStyle.display === 'inline' ? 'inline-block' : originalStyle.display;
if (media.offsetWidth > 0) {
wrapper.style.width = media.offsetWidth + 'px';
} else {
wrapper.style.width = '300px';
}
wrapper.style.margin = originalMargin;
media.style.margin = '0';
media.parentElement.insertBefore(wrapper, media);
wrapper.appendChild(media);
wrapper.appendChild(ui);
ui.classList.add('__show');
} else {
// --- VIDEO PATH (v1.01 absolute positioning logic) ---
document.body.appendChild(ui);
updateUIPosition = () => {
const videoRect = media.getBoundingClientRect();
ui.style.left = `${videoRect.left + window.scrollX + 8}px`;
ui.style.top = `${videoRect.top + window.scrollY + videoRect.height - 44 - 8}px`;
ui.style.width = `${videoRect.width - 16}px`;
};
updateUIPosition();
resizeObserver = new ResizeObserver(updateUIPosition);
resizeObserver.observe(media);
window.addEventListener('scroll', updateUIPosition, { passive: true, capture: true });
window.addEventListener('resize', updateUIPosition, { passive: true });
let hideTimeout;
const showUI = () => { clearTimeout(hideTimeout); ui.classList.add('__show'); };
const hideUI = () => { hideTimeout = setTimeout(() => ui.classList.remove('__show'), 1000); };
media.addEventListener('mouseenter', showUI);
ui.addEventListener('mouseenter', showUI);
media.addEventListener('mouseleave', hideUI);
ui.addEventListener('mouseleave', hideUI);
media.addEventListener('mousemove', showUI);
media.addEventListener('play', () => { showUI(); hideUI(); });
media.addEventListener('pause', showUI);
}
media.controls = false;
const playBtn = ui.querySelector('.__mp_play');
const progress = ui.querySelector('.__mp_progress');
const progressContainer = ui.querySelector('.__mp_progress_container');
const timeDisplay = ui.querySelector('.__mp_time');
progressContainer.addEventListener('click', e => {
const rect = progressContainer.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const percentage = clamp(clickX / progressContainer.offsetWidth, 0, 1);
if (!isNaN(media.duration)) media.currentTime = media.duration * percentage;
});
const muteBtn = ui.querySelector('.__mp_mute');
const volumeIcon = ui.querySelector('.mp_icon_vol_svg');
const updatePlayIcon = () => {
const isPaused = media.paused || media.ended;
playBtn.querySelector('svg path').setAttribute('d', isPaused ? 'M8 5v14l11-7L8 5z' : 'M6 5h4v14H6zM14 5h4v14h-4z');
};
const togglePlay = (e) => {
if (ui.contains(e.target) && e.target !== playBtn && !playBtn.contains(e.target)) return;
media.paused || media.ended ? media.play() : media.pause();
};
(isAudio ? wrapper : media).addEventListener('click', togglePlay);
playBtn.addEventListener('click', (e) => { e.stopPropagation(); togglePlay(e); });
if (!isAudio) {
media.addEventListener('dblclick', e => {
if (ui.contains(e.target)) return;
if (!document.fullscreenElement) media.requestFullscreen().catch(err => console.error(`[MP] Fullscreen Error: ${err.message}`));
else document.exitFullscreen();
});
}
const updateVolumeUI = () => {
const vol = media.volume, muted = media.muted;
if (muted || vol === 0) volumeIcon.innerHTML = `<path d="M16.5 12c0-1.77-.77-3.37-2-4.47V16.47c1.23-1.1 2-2.7 2-4.47zM5 9v6h4l5 4V5L9 9H5z" fill="currentColor"/>`;
else if (vol < 0.5) volumeIcon.innerHTML = `<path d="M5 9v6h4l5 4V5L9 9H5z" fill="currentColor"/>`;
else volumeIcon.innerHTML = `<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" fill="currentColor"/>`;
};
muteBtn.addEventListener('click', e => { e.stopPropagation(); media.muted = !media.muted; });
muteBtn.addEventListener('wheel', e => {
e.preventDefault(); e.stopPropagation();
const delta = Math.sign(e.deltaY);
media.volume = clamp(media.volume - delta * 0.05, 0, 1);
if (media.volume > 0) media.muted = false;
}, { passive: false });
let rafId;
const tickProgress = () => {
if (!media.isConnected) { cancelAnimationFrame(rafId); return; }
if (!isNaN(media.duration)) {
const val = (media.currentTime / media.duration) * 100;
progress.value = clamp(val, 0, 100).toString();
progress.style.setProperty('--play-percent', `${progress.value}%`);
timeDisplay.textContent = `${formatTime(media.currentTime)} / ${formatTime(media.duration)}`;
}
rafId = requestAnimationFrame(tickProgress);
};
const updateBufferProgress = () => {
if (!media.buffered || media.buffered.length === 0 || isNaN(media.duration)) return;
const bufferEnd = media.buffered.end(media.buffered.length - 1);
const bufferPercent = (bufferEnd / media.duration) * 100;
progress.style.setProperty('--buffer-percent', `${clamp(bufferPercent, 0, 100)}%`);
};
media.addEventListener('play', updatePlayIcon);
media.addEventListener('pause', updatePlayIcon);
media.addEventListener('ended', updatePlayIcon);
media.addEventListener('volumechange', updateVolumeUI);
media.addEventListener('progress', updateBufferProgress);
const onMetadataLoaded = () => {
updateVolumeUI();
updateBufferProgress();
if (!isNaN(media.duration)) timeDisplay.textContent = `${formatTime(media.currentTime)} / ${formatTime(media.duration)}`;
if (!rafId) rafId = requestAnimationFrame(tickProgress);
};
media.addEventListener('loadedmetadata', onMetadataLoaded);
if (media.readyState >= 1) onMetadataLoaded();
progress.addEventListener('input', e => { if (!isNaN(media.duration)) media.currentTime = clamp(media.duration * (parseFloat(e.target.value) / 100), 0, media.duration); });
const cleanup = () => {
if (rafId) cancelAnimationFrame(rafId);
if (resizeObserver) resizeObserver.disconnect();
if (isAudio) {
if (wrapper && wrapper.parentElement) {
wrapper.parentElement.insertBefore(media, wrapper);
wrapper.remove();
}
media.style.margin = originalMargin;
} else {
window.removeEventListener('scroll', updateUIPosition, true);
window.removeEventListener('resize', updateUIPosition);
if (ui.parentElement) ui.remove();
}
media.controls = true;
delete media.dataset.__mp_attached;
if (attachedMedia.has(media)) attachedMedia.delete(media);
};
const intersectionObserver = new IntersectionObserver((entries) => {
const entry = entries[0];
ui.style.visibility = entry.isIntersecting ? 'visible' : 'hidden';
});
intersectionObserver.observe(media);
const disconnectObserver = new MutationObserver(() => {
if (!media.isConnected) {
cleanup();
disconnectObserver.disconnect();
intersectionObserver.disconnect();
}
});
if (media.parentElement) {
disconnectObserver.observe(media.parentElement, { childList: true, subtree: true });
}
updatePlayIcon();
updateVolumeUI();
return () => {
cleanup();
disconnectObserver.disconnect();
intersectionObserver.disconnect();
};
}
const attachedMedia = new WeakMap();
function processMedia(media) {
if (media.hasAttribute('controls')) {
if (!attachedMedia.has(media)) {
try {
const cleanup = attachUIToMedia(media);
if (cleanup) attachedMedia.set(media, cleanup);
} catch (e) {
console.error('[MP] Error attaching to media:', e);
}
}
} else {
if (attachedMedia.has(media)) {
const cleanup = attachedMedia.get(media);
if (cleanup) cleanup();
attachedMedia.delete(media);
}
}
}
function scanAndProcess() {
document.querySelectorAll('video[controls]:not([data-__mp_attached]), audio[controls]:not([data-__mp_attached])').forEach(processMedia);
}
// Initial scan
scanAndProcess();
// Global observer for discovering new media
const globalObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.addedNodes) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.matches('video[controls], audio[controls]')) {
processMedia(node);
}
node.querySelectorAll('video[controls], audio[controls]').forEach(processMedia);
}
});
}
});
});
globalObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
})();