// ==UserScript==
// @name 视频网站自动网页全屏
// @author Feny
// @version 2.0.0
// @license GPL-3.0-only
// @namespace http://tampermonkey.net/
// @description 支持哔哩哔哩、B站直播、腾讯视频、优酷视频、爱奇艺、芒果TV、搜狐视频、AcFun弹幕网播放页自动网页全屏,视频网站统一支持快捷键切换:全屏(F)、网页全屏(P)、下一个视频(N)、弹幕开关(D),支持任意视频倍速播放,B站播放完自动退出网页全屏
// @note v2.0.0 新增倍速播放功能,页面可见性监听,倍速播放具体使用说明见脚本主页
// @note *://*/*
// @match *://tv.sohu.com/v/*
// @match *://www.mgtv.com/b/*
// @match *://www.iqiyi.com/v_*
// @match *://haokan.baidu.com/v*
// @match *://v.youku.com/v_show/*
// @match *://v.qq.com/x/page/*
// @match *://v.qq.com/x/cover/*
// @match *://v.qq.com/live/p/newtopic/*
// @match *://www.acfun.cn/v/*
// @match *://live.acfun.cn/live/*
// @match *://www.acfun.cn/bangumi/*
// @match *://live.bilibili.com/*
// @match *://www.bilibili.com/list/*
// @match *://www.bilibili.com/video/*
// @match *://www.bilibili.com/festival/*
// @match *://www.bilibili.com/cheese/play/*
// @match *://www.bilibili.com/bangumi/play/*
// @grant unsafeWindow
// @grant GM_addStyle
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAqdJREFUWEftl91LFFEYxp/3jB9ESZjtSl51F1RUSgRCF/kHlF1IhiFhF65dqEQkBUErdJMStBukGwQre2NZUiCRqUiURkW65mIfqGUFsW6Ii0jY7p4Tc3Rqd5zaGVldAudynve8z28e3jMzh5Dmi1R/V0vQyRRWxgWG6x22SrcnOAhQcQIbwVtXba8y1EANSpS1xzJin5c/Dz+jRDPvGWoErwRw35zuh8ChpcXXFjbwi9k/WADA9viGgovGnxtFs6EmcApMvCdBA3oIIirl4N8NNQngmRYJiwTOE7EHHLERAmXFawQ6AdCQkRbjsZIMUvIFoV0HMSsEDjCgSK8tJqAHAEDAMWLKLOexx8tiVVDEhLLVQAtzRPcwKOUANSWCw1/rsBe6PcFz8dpfAdTFgtF+EmIvBG7pID7mZNl2zkVCFQbahzqHfYerddpNhFpdsnfqauzl8ZoEuO4JXdIKOefynnZlimxXhBbqjTZL/el8pzrAVjTGmKh12Bq1ddJs974abQDXfFMuAhQ6EodwDTHWAf6/BAoK8nD0cDEKtuVhyD+OzvvLXnyWJshyApedJ1F65M9n4tlAAF5fL168fGfJWCu2DDA61GpodLvjCdp8vfjyNWQJJGUAquvMzBzafD0yEc65KZCUAmiOo4FPEqS753VSiFUB0FxbPF244en6J8SqAoTD8zhYcjZ9AP6RCVRWNacHYPD5GJqudmBi8tvaAkxNBeUuuNv5NOkAqgUpm4FIJCrfA+r0z4bnTZmvCKCv+wrsts0JBg8fvZLGY28NfoqToFhOoOJ4CS40lMu2I28mpXFP37DpJ9YXWgZQG+Tm5mBL7qakA2aGakUAZhqbrVkH0BLoB34fzcyml5K6pd/yaicRlQlgV0q6mmwitMOpyfpVKfsFya4w73cz9xQAAAAASUVORK5CYII=
// ==/UserScript==
(function () {
"use strict";
const ACFUN_VIDEO_PAGE_REGEX = /acfun.cn\/v/;
const BILI_VIDEO_PAGE_REGEX = /bilibili.com\/video/;
const BILI_LIVE_PAGE_REGEX = /live.bilibili.com\/(blanc\/)?\d+/;
const isLivePage = () => location.href.includes("live");
const isBiliLive = () => location.host === "live.bilibili.com";
if (isBiliLive() && !BILI_LIVE_PAGE_REGEX.test(location.href)) return;
GM_addStyle(`
.showToast {
color: #fff !important;
font-size: 14px !important;
padding: 5px 15px !important;
border-radius: 5px !important;
position: absolute !important;
z-index: 2147483647 !important;
transition: opacity 500ms ease-in;
background: rgba(0, 0, 0, .75) !important;
}
.showToast .playbackRate {
margin: 0 3px !important;
color: #FF6101 !important;
}
`);
// showToast 位置样式
const positions = {
bottomLeft: "bottom: 20%; left: 10px;",
center: "top: 50%; left: 50%; transform: translate(-50%, -50%);",
};
const selectorConfig = {
"live.bilibili.com": { webfull: "#businessContainerElement", },
"live.acfun.cn": { full: ".fullscreen-screen", webfull: ".fullscreen-web", danmaku: ".danmaku-enabled", },
"tv.sohu.com": { full: ".x-fullscreen-btn", webfull: ".x-pagefs-btn", danmaku: ".tm-tmbtn", next: ".x-next-btn", },
"haokan.baidu.com": { full: ".art-icon-fullscreen", webfull: ".art-control-fullscreenWeb", next: ".art-control-next", },
"www.iqiyi.com": { full: ".iqp-btn-fullscreen", webfull: ".iqp-btn-webscreen", danmaku: "#barrage_switch", next: ".iqp-btn-next", },
"www.mgtv.com": { full: ".fullscreenBtn i", webfull: ".webfullscreenBtn i", danmaku: "div[class*='danmuSwitch']", next: ".icon-next", },
"v.qq.com": { full: ".txp_btn_fullscreen", webfull: "div[aria-label='网页全屏']", danmaku: ".barrage-switch", next: ".txp_btn_next_u", },
"v.pptv.com": { full: ".w-zoom-container > div", webfull: ".w-expand-container > div", danmaku: ".w-barrage", next: ".w-next-container", },
"www.acfun.cn": { full: ".fullscreen-screen", webfull: ".fullscreen-web", danmaku: ".danmaku-enabled", next: ".btn-next-part .control-btn", },
"www.bilibili.com": { full: "div[aria-label='全屏']", webfull: "div[aria-label='网页全屏']", danmaku: ".bui-area", next: ".bpx-player-ctrl-next", },
"v.youku.com": { full: "#fullscreen-icon", webfull: "#webfullscreen-icon", danmaku: "div[class*='switch-img_12hDa turn-']", next: ".kui-next-icon-0", },
}
const ZERO = 0;
const SECOND_MS = 1000;
const DEFAULT_PLAYBACK_RATE = 1;
const PLAYBACK_RATE_STEP = 0.25; // 倍速步进
const SHOW_TOAST_TIME = SECOND_MS * 5; // 提示显示时长
const SHOW_TOAST_POSITION = positions.bottomLeft; // 提示位置
const PLAYBACK_RATE_INCREMENT_SYMBOL = "+";
const PLAYBACK_RATE_DECREMENT_SYMBOL = "-";
const MESSAGE_SOURCE = "FENY_SCRIPTS_AUTO_WEB_FULLSCREEN";
const CACHED_PLAYBACK_RATE_KEY = "FENY_SCRIPTS_V_PLAYBACK_RATE";
const $ = (selector, context) => (context ?? document).querySelector(selector);
const $$ = (selector, context) => (context ?? document).querySelectorAll(selector);
const ScriptsProgram = {
init() {
this.setupKeydownListener();
this.setupMutationObserver();
this.setupUrlChangeListener();
this.setupMouseOverListener();
this.setupPageVisibilityListener();
},
video: null,
getVideo: () => $("video[src]") ?? $("video"),
getElement: () => $(selectorConfig[location.host]?.webfull),
debounce(fn, delay = SECOND_MS) {
let timer;
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, arguments), delay);
};
},
setupUrlChangeListener() {
const _wr = (method) => {
const original = history[method];
history[method] = function () {
original.apply(history, arguments);
window.dispatchEvent(new Event(method));
};
};
const handler = this.debounce(() => this.setupMutationObserver());
["popstate", "pushState", "replaceState"].forEach((t) => _wr(t) & window.addEventListener(t, handler));
},
setupMutationObserver() {
this.videoListenerCycles = 0;
const observer = new MutationObserver(() => {
const video = this.video = this.getVideo();
const element = this.element = this.getElement();
if (video?.play && element) this.webFullScreen() && observer.disconnect();
if (video?.play) this.setupVideoListener();
});
observer.observe(document.body, { /* attributes: true, */ childList: true, subtree: true });
setTimeout(() => observer.disconnect(), SECOND_MS * 10);
},
reacquireVideo: false,
videoListenerCycles: 0,
videoBoundListeners: [],
setupVideoListener() {
if (isLivePage()) return;
if (this.videoListenerCycles >= 5) return;
this.videoListenerCycles++;
this.video = this.getVideo();
this.addVideoEventsListener();
// console.log("setupVideoListener 循环次数:", this.videoListenerCycles);
},
addVideoEventsListener() {
this.removeVideoEventsListener();
for (const type of Object.keys(VideoListenerHandler)) {
const handler = VideoListenerHandler[type];
this.video.addEventListener(type, handler);
this.videoBoundListeners.push([this.video, type, handler]);
}
},
removeVideoEventsListener() {
this.videoBoundListeners.forEach(listener => {
const [target, type, handler] = listener;
target.removeEventListener(type, handler);
});
this.videoBoundListeners = [];
},
rebindVideoEventsListener(video) {
this.video = video;
this.reacquireVideo = true;
this.addVideoEventsListener();
},
setupMouseOverListener() {
document.addEventListener('mouseover', (event) => {
const mouseX = event.clientX;
const mouseY = event.clientY;
const videos = $$("video");
for (const video of videos) {
const rect = video.getBoundingClientRect();
if (mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom) {
if (this.video == video) return;
this.rebindVideoEventsListener(video);
return;
}
}
});
},
setupPageVisibilityListener() {
window.addEventListener('visibilitychange', () => {
const video = this.video;
const state = document.visibilityState;
if (video) Object.is(state, "visible") ? video.play() : video.pause();
});
},
showToast(content, duration = SHOW_TOAST_TIME) {
document.querySelector(".showToast")?.remove();
const toast = document.createElement("div");
if (content instanceof HTMLElement) toast.appendChild(content);
if (Object.is(typeof content, "string")) toast.textContent = content;
toast.setAttribute("class", "showToast");
toast.setAttribute("style", SHOW_TOAST_POSITION);
this.video?.parentElement.parentElement.appendChild(toast);
setTimeout(() => {
toast.style.opacity = ZERO;
setTimeout(() => toast.remove(), SECOND_MS / 2);
}, duration);
}
};
// 视频监听事件逻辑处理
// this指向的是video.addEventListener
const VideoListenerHandler = {
loadedmetadata() {
this.volume = 1;
this.isToast = false;
},
loadeddata() {
// this.isToast = false;
},
timeupdate() {
if (this.duration === 0) return;
const cachePlaybackRate = ScriptsProgram.getCachePlaybackRate();
if (!cachePlaybackRate || DEFAULT_PLAYBACK_RATE === cachePlaybackRate) return;
if (cachePlaybackRate === this.playbackRate) return;
// console.log(`当前播放倍速为:${this.playbackRate},记忆倍速为:${cachePlaybackRate}`);
ScriptsProgram.setPlaybackRate(cachePlaybackRate);
if (this.isToast) return;
ScriptsProgram.tipPlaybackRate();
this.isToast = true;
},
ended() {
this.isToast = false;
const href = location.href;
// if (/[a-zA-z]+:\/\/[^\s]*/.test(href)) return;
if (!BILI_VIDEO_PAGE_REGEX.test(href) && !ACFUN_VIDEO_PAGE_REGEX.test(href)) return;
function exitWebFullScreen() {
const video = ScriptsProgram.video;
if (window.innerWidth === video.offsetWidth) ScriptsProgram.getElement()?.click();
const cancelAutoPlayNextButton = $(".bpx-player-ending-related-item-cancel"); // B站“取消连播”按钮
if (cancelAutoPlayNextButton) cancelAutoPlayNextButton.click();
console.log("已退出网页全屏!!");
}
const switchBtn = $(".video-pod .switch-btn.on");
const podItems = $$(".video-pod .video-pod__item");
// B站视频合集,为最后集播放或关闭了合集连播
if (podItems.length > ZERO) {
const lastPodItem = podItems[podItems.length - 1];
const scrolled = lastPodItem.dataset.scrolled;
if (scrolled === "true" || !switchBtn) exitWebFullScreen();
return;
}
exitWebFullScreen();
},
}
// 快捷键逻辑处理
const KeydownHandler = {
setupKeydownListener() {
const handler = (event) => this.keydownHandler.call(this, event);
window.addEventListener("keydown", handler, true);
window.addEventListener("message", (event) => {
const { data } = event;
if (!data?.source) return;
// console.log("接收到来自父窗口的消息:", data);
if (!data.source.includes(MESSAGE_SOURCE)) return;
// video可能多层iframe嵌套,继续往下派遣键盘事件
if (!this.video) this.postMessageToAllIframes(data);
if (data?.hotKey && this.video) this.execHotKeyActions(data.hotKey);
});
},
keydownHandler(event) {
const activeTagName = document.activeElement.tagName;
if (["INPUT", "TEXTAREA"].includes(activeTagName)) return;
const hotKey = event.key.toUpperCase();
this.execHotKeyActions(hotKey);
// 解决video在iframe中,不聚焦到iframe,倍速设置失败问题
if (window.top === window && !this.video) this.postMessageToAllIframes({ hotKey })
},
execHotKeyActions(key) {
const clickElement = (name, index) => {
if (!isBiliLive()) return $(selectorConfig[location.host]?.[name])?.click();
const control = this.getBiliLiveControlIcons();
if (control) control[index]?.click();
}
const actions = {
N: () => clickElement("next"),
F: () => clickElement("full", ZERO),
P: () => clickElement("webfull", 1),
D: () => clickElement("danmaku", 3),
A: () => this.stepPlaybackRate(PLAYBACK_RATE_INCREMENT_SYMBOL),
S: () => this.stepPlaybackRate(PLAYBACK_RATE_DECREMENT_SYMBOL),
"+": () => this.stepPlaybackRate(PLAYBACK_RATE_INCREMENT_SYMBOL),
"-": () => this.stepPlaybackRate(PLAYBACK_RATE_DECREMENT_SYMBOL),
Z: () => this.setPlaybackRate(DEFAULT_PLAYBACK_RATE) && this.showToast("已恢复正常倍速播放"),
0: () => this.switchVideoPlayStatus(), // 是数字0,不是字母O
"*": () => this.reacquirePlayingVideo(),
};
if (actions[key]) actions[key]();
if (/^[1-9]$/.test(key)) this.setPlaybackRate(key) && this.tipPlaybackRate();
},
switchVideoPlayStatus() {
const video = this.video;
if (video) video.paused ? video.play() : video.pause();
},
reacquirePlayingVideo() {
// 获取当前正在播放的video标签
const videos = $$("video");
for (const video of videos) {
if (!video.paused && this.video !== video) {
this.rebindVideoEventsListener(video);
}
}
},
getBiliLiveControlIcons() {
const video = this.getVideo();
if (!video) return;
this.simulateMouseMove(video);
// 图标是从右到左:全屏、网页全屏、弹幕设置、弹幕开关、小窗模式,即下标[0]取到的是全屏图标
return $$("#web-player-controller-wrap-el .right-area .icon");
},
postMessageToAllIframes(data) {
$$("iframe").forEach(iframe => {
iframe?.contentWindow?.postMessage({ source: MESSAGE_SOURCE, ...data }, '*')
});
},
simulateMouseMove(target) {
const y = target.offsetHeight / 2;
const maxWidth = target.offsetWidth;
const moveEvent = (x) => target.dispatchEvent(new MouseEvent("mousemove", { clientX: x, clientY: y, bubbles: true }));
for (let i = ZERO; i < maxWidth; i += 100) moveEvent(i);
}
}
// 播放倍速逻辑处理
const VideoPlaybackRateHandler = {
setPlaybackRate(playbackRate) {
if (isLivePage()) return;
if (!this.video) return;
// 腾讯视频会有两个video标签
if (!this.reacquireVideo && this.video !== this.getVideo()) return this.setupVideoListener();
if (!this.video?.play) return this.showToast("设置倍速失败");
this.video.playbackRate = playbackRate;
this.cachePlaybackRate();
return true;
},
stepPlaybackRate(v_symbol) {
if (isLivePage()) return;
if (!this.video) return;
if (!this.reacquireVideo && this.video !== this.getVideo()) return this.setupVideoListener();
if (!this.video?.play) return this.showToast("设置倍速失败");
if (PLAYBACK_RATE_INCREMENT_SYMBOL === v_symbol) this.video.playbackRate += PLAYBACK_RATE_STEP;
if (PLAYBACK_RATE_DECREMENT_SYMBOL === v_symbol) this.video.playbackRate -= PLAYBACK_RATE_STEP;
if (ZERO === this.video.playbackRate) this.video.playbackRate = PLAYBACK_RATE_STEP;
this.cachePlaybackRate();
this.tipPlaybackRate();
},
cachePlaybackRate() {
localStorage.setItem(CACHED_PLAYBACK_RATE_KEY, this.video.playbackRate);
},
getCachePlaybackRate() {
const cachePlaybackRate = localStorage.getItem(CACHED_PLAYBACK_RATE_KEY);
return parseFloat(cachePlaybackRate ?? DEFAULT_PLAYBACK_RATE);
},
tipPlaybackRate() {
const span = document.createElement("span");
span.appendChild(document.createTextNode("正在以"));
const child = span.cloneNode(true);
child.textContent = `${this.video.playbackRate}x`;
child.setAttribute("class", "playbackRate");
span.appendChild(child);
span.appendChild(document.createTextNode("倍速播放"));
this.showToast(span);
},
}
// 网页全屏逻辑处理
const WebFullScreenHandler = {
webFullScreen() {
const video = this.video;
if (!video) return false;
const w = video.offsetWidth;
if (ZERO === w) return false;
if (window.innerWidth === w) return true;
if (isBiliLive()) return this.biliLiveWebFullScreen();
this.element.click();
return true;
},
biliLiveWebFullScreen() {
try {
const topWindow = unsafeWindow.top;
topWindow.scrollTo({ top: 70 });
const ctnr = Object.is(topWindow, window) ? $("#player-ctnr") : $(".lite-room", topWindow.document);
topWindow.scrollTo({ top: ctnr?.getBoundingClientRect()?.top ?? 0 });
this.element.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));
localStorage.setItem("FULLSCREEN-GIFT-PANEL-SHOW", 0); // 关闭全屏礼物栏
document.body.classList.add("hide-asida-area", "hide-aside-area"); // 关闭侧边聊天栏
setTimeout(() => {
$("#shop-popover-vm")?.remove(); // 关闭不支持“小橙车”提示
$("#sidebar-vm")?.remove();
}, SECOND_MS / 2);
topWindow?.livePlayer?.volume(100);
topWindow?.livePlayer?.switchQuality("10000"); // 原画画质
} catch (error) {
console.error("B站直播自动网页全屏异常:", error);
}
return true;
},
}
const logicHandlers = [
{ handler: KeydownHandler },
{ handler: WebFullScreenHandler },
{ handler: VideoPlaybackRateHandler },
]
// 使方法内部this指向为ScriptsProgram
logicHandlers.forEach(({ handler }) => {
for (const methodName of Object.keys(handler)) {
ScriptsProgram[methodName] = handler[methodName].bind(ScriptsProgram);
}
});
ScriptsProgram.init();
})();