Greasy Fork

Greasy Fork is available in English.

视频网站自动网页全屏

支持哔哩哔哩、B站直播、腾讯视频、优酷视频、爱奇艺、芒果TV、搜狐视频、AcFun弹幕网播放页自动网页全屏,视频网站统一支持快捷键切换:全屏(F)、网页全屏(P)、下一个视频(N)、弹幕开关(D),支持任意视频倍速播放,B站播放完自动退出网页全屏

当前为 2024-12-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         视频网站自动网页全屏
// @license      GPL-3.0
// @author       Feny
// @version      2.0.0
// @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();
})();