Bilibili 音频模式(audio-only)

悬浮按钮一键切换“仅音频播放,不解码视频轨道”,降低CPU/GPU占用;

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Bilibili 音频模式(audio-only)
// @namespace    bilibili-audio-only-floating
// @version      2.0.1
// @description  悬浮按钮一键切换“仅音频播放,不解码视频轨道”,降低CPU/GPU占用;
// @author       you
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/list/*
// @match        https://www.bilibili.com/bangumi/play/*
// @run-at       document-idle
// @grant        unsafeWindow
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const sleep = (ms) => new Promise(r => setTimeout(r, ms));
    const log = (...a) => console.log('[Bili-AudioOnly]', ...a);

    // 读取 dash 播放信息
    function readPlayInfo() {
        try {
            if (unsafeWindow && unsafeWindow.__playinfo__) return unsafeWindow.__playinfo__;
        } catch { }
        const scripts = Array.from(document.scripts || []);
        for (const s of scripts) {
            const txt = s.textContent || '';
            if (txt.includes('"dash"') && (txt.includes('"audio"') || txt.includes('"video"'))) {
                try {
                    const m = txt.match(/__playinfo__\s*=\s*(\{.+?\})\s*;?/s);
                    if (m) return JSON.parse(m[1]);
                    if (txt.trim().startsWith('{') && txt.trim().endsWith('}')) {
                        return JSON.parse(txt.trim());
                    }
                } catch { }
            }
        }
        return null;
    }
    function pickBestAudioUrl(playInfo) {
        if (!playInfo?.data?.dash?.audio) return null;
        const audios = playInfo.data.dash.audio.slice().sort((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0));
        const first = audios[0];
        return first?.baseUrl || first?.backupUrl?.[0] || null;
    }
    function queryPlayerVideo() {
        const list = document.querySelectorAll('video');
        for (const v of list) if (v && typeof v.play === 'function') return v;
        return null;
    }

    // 悬浮按钮样式
    function ensureStyle() {
        if (document.getElementById('bao-float-style')) return;
        const css = `
      #bao-float {
        position: fixed;
        z-index: 2147483647;
        left: 0; top: 96px;
        user-select: none;
      }
      #bao-float .bao-btn {
        display: inline-flex; align-items: center; gap: .4em;
        padding: .42em .88em; border-radius: .7em;
        background: rgba(0,0,0,.55); color: #fff;
        font-size: 13px; cursor: pointer;
        border: 1px solid rgba(255,255,255,.18);
        box-shadow: 0 4px 14px rgba(0,0,0,.25);
        transition: background .18s ease, transform .08s ease;
      }
      #bao-float .bao-btn:hover { background: rgba(0,0,0,.7); }
      #bao-float .bao-btn:active { transform: translateY(1px); }
      #bao-float .bao-badge {
        font-size: 11px; padding: 0 .44em;
        border: 1px solid rgba(255,255,255,.32);
        border-radius: .4em; opacity: .88;
      }
      .bao-hidden { visibility: hidden !important; }
    `;
        const style = document.createElement('style');
        style.id = 'bao-float-style';
        style.textContent = css;
        document.head.appendChild(style);
    }

    // 悬浮按钮
    let floatWrap, theBtn;
    function createFloatingButton() {
        ensureStyle();
        if (document.getElementById('bao-float')) return document.getElementById('bao-float');

        floatWrap = document.createElement('div');
        floatWrap.id = 'bao-float';
        theBtn = document.createElement('button');
        theBtn.className = 'bao-btn';
        theBtn.title = '切换仅音频播放(不解码视频)';
        theBtn.textContent = '🎧 音频模式';
        const badge = document.createElement('span');
        badge.className = 'bao-badge';
        badge.textContent = 'OFF';
        theBtn.appendChild(badge);

        floatWrap.appendChild(theBtn);
        document.body.appendChild(floatWrap);

        // 拖拽
        let dragging = false, sx = 0, sy = 0, ox = 0, oy = 0;
        floatWrap.addEventListener('mousedown', (e) => {
            dragging = true; sx = e.clientX; sy = e.clientY;
            const rect = floatWrap.getBoundingClientRect(); ox = rect.left; oy = rect.top;
            e.preventDefault();
        });
        window.addEventListener('mousemove', (e) => {
            if (!dragging) return;
            const nx = ox + (e.clientX - sx);
            const ny = oy + (e.clientY - sy);
            floatWrap.style.left = Math.max(0, nx) + 'px';
            floatWrap.style.top = Math.max(0, ny) + 'px';
        });
        window.addEventListener('mouseup', () => dragging = false);

        return floatWrap;
    }

    function updateButton(on) {
        if (!theBtn) return;
        const badge = theBtn.querySelector('.bao-badge');
        if (badge) {
            badge.textContent = on ? 'ON' : 'OFF';
            badge.style.background = on ? 'rgba(76,175,80,.25)' : 'transparent';
        }
        theBtn.title = on ? '当前仅音频播放;点击恢复视频' : '点击切换到仅音频播放';
    }

    let audioMode = false;

    function queryMainVideo() {
        const vs = Array.from(document.querySelectorAll('video'));
        // 过滤掉宽高为0或不可见的幽灵节点
        const cand = vs.filter(v => typeof v.play === 'function');
        // B站通常有两个video,选有声音输出的那个;退化就取第一个
        return cand.find(v => !v.muted) || cand[0] || null;
    }

    // 暂停/静音其他 video,避免双音频
    function silenceOtherVideos(main) {
        const vs = Array.from(document.querySelectorAll('video'));
        for (const v of vs) {
            if (v === main) continue;
            try { v.pause(); } catch { }
            v.muted = true;
            v.style.visibility = 'hidden'; // 防止叠层闪烁
        }
    }

    // 启/禁视频轨道(不破坏 MSE)
    function setVideoTrackEnabled(video, enabled) {
        try {
            if (video.videoTracks && video.videoTracks.length) {
                for (const t of video.videoTracks) t.enabled = enabled;
                return true;
            }
        } catch { }
        return false;
    }

    async function applyAudioOnly(enable) {
        const video = queryMainVideo();
        if (!video) { log('未找到主 <video>'); return; }

        // 确保只保留一个媒体在播
        silenceOtherVideos(video);

        if (enable) {
            // 禁用视频轨道(关键:阻断视频解码/渲染)
            const ok = setVideoTrackEnabled(video, false);

            // 退化处理:若浏览器无 videoTracks,就只隐藏画面(仍可能解码,但不渲染)
            if (!ok) {
                video.style.visibility = 'hidden';
                // 也可选:video.style.opacity = '0'; video.style.width='1px'; video.style.height='1px';
            }

            // 确保有声音输出
            try { video.muted = false; } catch { }
            // try { await video.play(); } catch (e) { log('video play() 失败:', e); }

            audioMode = true;
            localStorage.setItem('bao_audio_mode', '1');
            updateButton(true);

        } else {
            // 恢复正常播放
            // 重新启用视频轨道
            setVideoTrackEnabled(video, true);

            // 恢复可见
            video.style.visibility = '';

            // 继续播放
            try { await video.play(); } catch (e) { log('video play() 失败:', e); }

            audioMode = false;
            localStorage.removeItem('bao_audio_mode');
            updateButton(false);
        }
    }


    // SPA 导航适配
    function hookNavigation(callback) {
        const pushState = history.pushState;
        const replaceState = history.replaceState;
        history.pushState = function () {
            const ret = pushState.apply(this, arguments);
            setTimeout(callback, 0); return ret;
        };
        history.replaceState = function () {
            const ret = replaceState.apply(this, arguments);
            setTimeout(callback, 0); return ret;
        };
        window.addEventListener('popstate', () => setTimeout(callback, 0));
    }

    // 初始化启动 
    async function boot() {
        createFloatingButton();
        // updateButton(true);

        // 等待视频节点出现
        for (let i = 0; i < 30; i++) {
            if (queryPlayerVideo()) break;
            await sleep(200);
        }

        // 点击事件
        theBtn.addEventListener('click', async () => {
            try { await applyAudioOnly(!audioMode); } catch (e) { log('切换失败:', e); }
        });
    }

    hookNavigation(() => {
        // 切到新视频时保持按钮在左上角,不进入播放器容器
        setTimeout(boot, 200);
    });

    boot();

    setInterval(() => {
        // 避免下一个视频的时候又出现视频画面
        if (audioMode) {
            applyAudioOnly(true);
        }
    }, 500)
})();