Greasy Fork

Greasy Fork is available in English.

Bilibili预加载脚本

预加载Bilibili视频,避免播放顿卡,支持持续预加载

当前为 2025-12-27 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bilibili预加载脚本
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  预加载Bilibili视频,避免播放顿卡,支持持续预加载
// @author       You
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/bangumi/play/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 配置选项
    const config = {
        preloadSeconds: 60, // 默认预加载60秒
        maxTotalPreload: 300, // 最大总预加载时间(300秒)
        checkInterval: 2000, // 检查间隔
        debug: false // 调试模式
    };

    // 视频流信息
    let videoSource = null;
    let videoSegmentUrls = [];
    let preloadedSegments = new Set();

    // 状态变量
    let player = null;
    let preloadTimer = null;
    let isPreloading = false;
    let lastCheckedTime = 0;
    let isUserSeeking = false; // 标记用户是否正在拖动进度条
    let seekStart = 0; // 记录拖动开始时间
    let seekEnd = 0; // 记录拖动结束时间

    // 日志函数
    const log = (message, type = 'info') => {
        if (config.debug) {
            const colors = {
                info: '#0078D7',
                warning: '#FFA200',
                error: '#E81123',
                success: '#107C10'
            };
            console.log(`%c[B站预加载] ${message}`, `color: ${colors[type] || colors.info};`);
        }
    };

    // 检测播放器
    const detectPlayer = () => {
        const videoElements = document.querySelectorAll('video');
        for (let video of videoElements) {
            if (video.src || video.parentElement.className.includes('bpx-player-video-wrap')) {
                player = video;
                log('找到播放器元素', 'success');
                
                // 监听视频源变化
                if (video.src) {
                    videoSource = video.src;
                    log(`获取视频源地址: ${videoSource}`, 'info');
                }
                
                return true;
            }
        }
        return false;
    };

    // 预加载视频片段
    const preloadVideoSegment = async (url) => {
        if (preloadedSegments.has(url)) {
            return; // 已预加载过
        }
        
        try {
            const response = await fetch(url, {
                method: 'GET',
                mode: 'cors',
                cache: 'force-cache'
            });
            
            if (response.ok) {
                preloadedSegments.add(url);
                log(`成功预加载视频片段: ${url.split('/').pop()}`, 'success');
            }
        } catch (error) {
            log(`预加载失败: ${error.message}`, 'error');
        }
    };

    // 分析视频URL并提取预加载URL
const extractPreloadUrls = (baseUrl, startSegment, endSegment) => {
    // 处理不同类型的视频URL(HLS/DASH)
    const urls = [];
    
    if (baseUrl.includes('.m3u8')) {
        // HLS格式,需要解析m3u8文件获取片段列表
        log('检测到HLS格式视频,跳过直接预加载', 'warning');
        return urls;
    } else if (baseUrl.includes('.mp4')) {
        // MP4格式,可以直接预加载特定范围
        urls.push(baseUrl);
    } else if (baseUrl.includes('/upgcxcode/')) {
        // Bilibili分段视频格式
        const segmentRegex = /(\d+)_\d+_(\d+)\.(\w+)$/;
        const match = baseUrl.match(segmentRegex);
        if (match) {
            const prefix = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
            const quality = match[1];
            const fileType = match[3];
            
            for (let i = startSegment; i <= endSegment; i++) {
                const segmentUrl = `${prefix}${quality}_0_${i}.${fileType}`;
                urls.push(segmentUrl);
            }
        }
    }
    
    return urls;
};

// 使用fetch API预加载视频片段
const preloadWithFetch = async (url) => {
    if (preloadedSegments.has(url)) {
        return; // 已预加载过
    }
    
    try {
        const response = await fetch(url, {
            method: 'GET',
            mode: 'cors',
            credentials: 'include',
            headers: {
                'Range': 'bytes=0-', // 预加载整个片段
                'Accept': '*/*',
                'Referer': window.location.href,
                'User-Agent': navigator.userAgent
            },
            cache: 'force-cache'
        });
        
        if (response.ok) {
            // 读取响应内容以确保完全缓存
            const buffer = await response.arrayBuffer();
            preloadedSegments.add(url);
            log(`成功预加载视频片段: ${url.split('/').pop()} (${(buffer.byteLength / 1024).toFixed(2)}KB)`, 'success');
        }
    } catch (error) {
        log(`预加载失败: ${error.message}`, 'error');
    }
};

// 持续预加载逻辑
const continuousPreload = () => {
    if (!player || player.paused || isPreloading) return;
    
    const currentTime = player.currentTime;
    const buffered = player.buffered;
    if (!buffered || buffered.length === 0) return;
    
    const bufferedEnd = buffered.end(buffered.length - 1);
    const totalBuffered = bufferedEnd;
    const remainingCache = Math.min(config.maxTotalPreload - (bufferedEnd - currentTime), config.preloadSeconds);
    
    // 检查是否需要继续预加载
    if (totalBuffered < player.duration && remainingCache > 0) {
        log(`当前已缓存至: ${totalBuffered.toFixed(2)}s, 开始预加载下一段`, 'info');
        
        isPreloading = true;
        
        // 使用fetch API直接预加载,避免修改currentTime
        const preloadTarget = Math.min(totalBuffered + remainingCache, player.duration);
        
        // 获取视频源URL
        const mediaSource = player.src || (player.currentSrc ? player.currentSrc : '');
        
        if (mediaSource) {
            // 根据视频URL类型采用不同的预加载策略
            if (mediaSource.includes('.m3u8')) {
                // HLS格式 - 使用间接方式预加载
                // 这种方式可能仍然会有轻微的时间跳转,但比之前更优化
                const wasPlaying = !player.paused;
                const originalTime = currentTime;
                
                // 使用更小的时间增量,减少视觉影响
                player.currentTime = Math.min(originalTime + 0.1, player.duration);
                
                requestAnimationFrame(() => {
                    player.currentTime = originalTime;
                    if (wasPlaying && player.paused) {
                        player.play().catch(e => log(`恢复播放失败: ${e.message}`, 'error'));
                    }
                    isPreloading = false;
                });
            } else {
                // 其他格式 - 尝试提取片段URL直接预加载
                const urls = extractPreloadUrls(mediaSource, Math.floor(totalBuffered / 10), Math.floor(preloadTarget / 10));
                
                if (urls.length > 0) {
                    // 并发预加载最多5个片段
                    const batchUrls = urls.slice(0, 5);
                    Promise.all(batchUrls.map(url => preloadWithFetch(url)))
                        .finally(() => {
                            isPreloading = false;
                        });
                } else {
                    // 回退到间接预加载方式,但优化时间处理
                    const wasPlaying = !player.paused;
                    const originalTime = currentTime;
                    
                    // 使用AudioContext创建一个不可见的音频元素来预加载
                    const audio = new Audio(mediaSource);
                    audio.volume = 0;
                    audio.currentTime = preloadTarget - 0.1;
                    audio.addEventListener('loadedmetadata', () => {
                        audio.currentTime = preloadTarget;
                        audio.addEventListener('canplaythrough', () => {
                            audio.pause();
                            audio.src = '';
                            isPreloading = false;
                        }, { once: true });
                    });
                }
            }
        } else {
            isPreloading = false;
        }
    }
};

    // 预加载长度直接使用配置值
    const getPreloadLength = () => {
        return config.preloadSeconds;
    };

    // 检查并执行预加载
    const checkAndPreload = () => {
        if (!player || player.paused) return;

        const currentTime = player.currentTime;
        const buffered = player.buffered;
        if (!buffered || buffered.length === 0) return;

        // 避免在视频刚开始播放时执行预加载(< 3秒)
        if (currentTime < 3) return;
        
        // 避免在用户正在拖动进度条时执行预加载
        if (isUserSeeking) {
            log('用户正在拖动进度条,跳过预加载检查', 'info');
            return;
        }
        
        // 避免在用户刚拖动完进度条后立即执行预加载(等待1秒)
        if (seekEnd > 0 && (Date.now() - seekEnd) < 1000) {
            log('用户刚完成拖动操作,等待后再预加载', 'info');
            return;
        }

        const bufferedEnd = buffered.end(buffered.length - 1);
        const bufferNeeded = config.preloadSeconds;
        
        log(`当前播放时间: ${currentTime.toFixed(2)}s, 已缓存至: ${bufferedEnd.toFixed(2)}s, 已缓存时长: ${(bufferedEnd - currentTime).toFixed(2)}s`, 'info');

        // 计算已缓存的总时长(从当前播放位置到缓存结束)
        const currentBuffered = bufferedEnd - currentTime;
        const totalBuffered = bufferedEnd;
        
        // 如果已缓存的内容不足,或者需要继续预加载以达到最大预加载限制
        if (currentBuffered < bufferNeeded * 0.8 && (totalBuffered - currentTime) < config.maxTotalPreload) {
            continuousPreload();
        }
    };

    // 初始化脚本
    const initScript = () => {
        log('初始化Bilibili智能预加载脚本', 'info');
        
        // 等待播放器加载
        const playerCheckInterval = setInterval(() => {
            if (detectPlayer()) {
                clearInterval(playerCheckInterval);
                
                
                
                // 视频开始播放后,延迟2秒再开始预加载检查
                setTimeout(() => {
                    preloadTimer = setInterval(() => {
                        checkAndPreload();
                    }, config.checkInterval);
                }, 2000);
                
                // 添加事件监听器
                player.addEventListener('playing', () => {
                    log('视频开始播放', 'info');
                });
                
                // 监听视频源变化
                player.addEventListener('loadstart', () => {
                    if (player.src) {
                        videoSource = player.src;
                        log(`视频源地址更新: ${videoSource}`, 'info');
                        preloadedSegments.clear(); // 清空已预加载记录
                    }
                });
                
                player.addEventListener('pause', () => {
                    log('视频暂停', 'info');
                });
                
                // 监听用户拖动进度条
                player.addEventListener('seeking', () => {
                    if (!isUserSeeking) {
                        isUserSeeking = true;
                        seekStart = Date.now();
                        log('用户开始拖动进度条', 'info');
                    }
                });
                
                player.addEventListener('seeked', () => {
                    isUserSeeking = false;
                    seekEnd = Date.now();
                    log('用户完成拖动进度条,耗时: ' + (seekEnd - seekStart) + 'ms', 'info');
                    
                    // 用户拖动后,重置预加载检查延迟
                    clearInterval(preloadTimer);
                    setTimeout(() => {
                        preloadTimer = setInterval(() => {
                            checkAndPreload();
                        }, config.checkInterval);
                    }, 2000);
                });
                
                // 添加用户控制面板
                addControlPanel();
            }
        }, 1000);
    };

    // 添加用户控制面板
    const addControlPanel = () => {
        const panel = document.createElement('div');
        panel.style.cssText = `
            position: fixed;
            top: 100px;
            right: 20px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            padding: 10px;
            border-radius: 5px;
            z-index: 9999;
            font-size: 12px;
            max-width: 200px;
        `;
        
        panel.innerHTML = `
            <div style="margin-bottom: 5px;"><strong>B站预加载控制</strong></div>
            <div style="margin-bottom: 5px;">
                预加载长度: <input type="number" id="preloadSeconds" value="${config.preloadSeconds}" min="5" max="${config.maxPreloadSeconds}" style="width: 50px;">
                秒
            </div>
            <div style="margin-bottom: 5px;">
                <label><input type="checkbox" id="debugMode" ${config.debug ? 'checked' : ''}> 调试模式</label>
            </div>
            <div>
                <button id="applySettings" style="width: 100%; padding: 5px;">应用设置</button>
            </div>
        `;
        
        document.body.appendChild(panel);
        
        // 添加事件监听器
        document.getElementById('applySettings').addEventListener('click', () => {
            const newSeconds = parseInt(document.getElementById('preloadSeconds').value);
            config.preloadSeconds = Math.max(5, Math.min(newSeconds, config.maxTotalPreload));
            config.debug = document.getElementById('debugMode').checked;
            log(`已更新设置: 预加载${config.preloadSeconds}秒, 调试模式${config.debug ? '开启' : '关闭'}`, 'success');
        });
    };

    // 导出到全局,方便手动控制
    window.bilibiliPreload = {
        config: config,
        manualPreload: (seconds) => {
            if (player && !isPreloading) {
                const originalPreload = config.preloadSeconds;
                config.preloadSeconds = seconds;
                checkAndPreload();
                setTimeout(() => {
                    config.preloadSeconds = originalPreload;
                }, 500);
            }
        },
        toggleDebug: () => {
            config.debug = !config.debug;
            log(`调试模式已${config.debug ? '开启' : '关闭'}`, 'info');
        }
    };

    // 页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initScript);
    } else {
        initScript();
    }

})();