YouTube Permanent ProgressBar

Keeps YouTube progress bar visible all the time.

< Feedback on YouTube Permanent ProgressBar

Question/comment

Hi! I’ve refactored the script to reduce idle CPU usage and added a configurable frame-rate:

  • Replaced the previous double loop (setTimeout + requestAnimationFrame) with an event-driven requestAnimationFrame loop that runs only while the video element is playing and stops on pause/ended or when the tab is hidden.
  • Added options.TARGET_FPS (default 15 is smooth in most case). If set to 0, the progress bar updates every animation frame; otherwise it throttles to the specified FPS.
  • Cached chapter widths to avoid recalculating offsetWidth each frame.
  • Skip updates while controls are visible (.ytp-autohide absent) so YouTube’s own code handles them.
  • No functional regressions observed; CPU usage (single core) drops from > 80 % to ~20 % in my tests.
(function() {
    "use strict";

    var style = document.createElement('style');
    var to = { createHTML: s => s },
        tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to,
        html = s => tp.createHTML(s)
    style.type = 'text/css';
    style.innerHTML = html(
        '.ytp-autohide .ytp-chrome-bottom{opacity:1!important;display:block!important}' +
        '.ytp-autohide .ytp-chrome-bottom .ytp-progress-bar-container{bottom:-1px!important}' +
        '.ytp-autohide .ytp-chrome-bottom .ytp-chrome-controls{opacity:0!important}' +
        '.ytp-progress-bar-container:not(:hover) .ytp-scrubber-container{display:none!important}'
    );
    document.getElementsByTagName('head')[0].appendChild(style);

    var permanentProgressBar = {
        options: {
            // To change bar update rate, go to line 161.
            UPDATE_VIDEO_TIMER: true,
            UPDATE_PROGRESSBAR: true,
            UPDATE_BUFFERBAR: true,
            TARGET_FPS: 15, // frame per second. Set 0 to update on each rAF.
        },

        // cache chapter metrics
        chapterCache: {len:0,widths:[],totalWidth:0,ratio:1},

        // Converts current video time to a human-readable format.
        prettifyVideoTime: function(video) {
            let seconds = "" + Math.floor(video.currentTime % 60);
            let minutes = "" + Math.floor((video.currentTime % 3600) / 60);
            let hours = "" + Math.floor(video.currentTime / 3600);
            if (video.currentTime / 60 > 60) {
                return `${hours}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}`;
            } else {
                return `${minutes}:${seconds.padStart(2, '0')}`;
            }
        },

        // For progress mode, return current time; for buffer mode, return the end of the buffered range that contains currentTime.
        getDuration: function(video, type) {
            if (type === "PROGRESSBAR") {
                return video.currentTime;
            } else if (type === "BUFFERBAR") {
                if (video.buffered.length > 0) {
                    for (let i = 0; i < video.buffered.length; i++) {
                        if (video.currentTime >= video.buffered.start(i) && video.currentTime <= video.buffered.end(i)) {
                            return video.buffered.end(i);
                        }
                    }
                }
                return 0;
            }
        },

        // Updates the current time display on the player.
        updateCurrentTimeField: function(player) {
            const video = player.querySelector("video");
            const currentTimeEl = player.querySelector(".ytp-time-current");
            if (!video || !currentTimeEl) return;
            currentTimeEl.innerText = permanentProgressBar.prettifyVideoTime(video);
        },

        // For non-chaptered videos, update the progress and buffer bars directly.
        updateOverallProgressBar: function(player) {
            const video = player.querySelector("video");
            const progressBar = player.querySelector(".ytp-play-progress");
            const bufferBar = player.querySelector(".ytp-load-progress");
            if (!video || !progressBar || !bufferBar) return;
            if (!video.duration) return;

            let progressRatio = video.currentTime / video.duration;
            let bufferRatio = this.getDuration(video, "BUFFERBAR") / video.duration;

            progressBar.style.transform = `scaleX(${Math.min(1, progressRatio.toFixed(5))})`;
            bufferBar.style.transform = `scaleX(${Math.min(1, bufferRatio.toFixed(5))})`;
        },

        // For chaptered videos, update each chapter element directly based on current time.
        updateProgressBarWithChapters: function(player, type) {
            const video = player.querySelector("video");
            if (!video || isNaN(video.duration)) return;

            // Get the chapter elements and corresponding progress elements.
            const chapterElements = player.getElementsByClassName("ytp-progress-bar-padding");
            let chapterProgressEls;
            if (type === "PROGRESSBAR") {
                chapterProgressEls = player.getElementsByClassName("ytp-play-progress");
            } else if (type === "BUFFERBAR") {
                chapterProgressEls = player.getElementsByClassName("ytp-load-progress");
            }
            if (!chapterElements || !chapterProgressEls) return;

            // Compute chapter metrics only if cache invalid
            if (this.chapterCache.len !== chapterElements.length || !this.chapterCache.totalWidth) {
                this.chapterCache.len = chapterElements.length;
                this.chapterCache.widths = Array.from(chapterElements).map(el => el.offsetWidth);
                this.chapterCache.totalWidth = this.chapterCache.widths.reduce((a,b)=>a+b,0);
                this.chapterCache.ratio = video.duration / this.chapterCache.totalWidth;
            }
            const widths = this.chapterCache.widths;
            const durationWidthRatio = this.chapterCache.ratio;

            let accumulatedWidth = 0;
            for (let i = 0; i < chapterElements.length; i++) {
                const chapterWidth = widths[i];
                const chapterEndTime = durationWidthRatio * (accumulatedWidth + chapterWidth);
                const chapterStartTime = durationWidthRatio * accumulatedWidth;
                let currentTimeForType = this.getDuration(video, type);
                let ratio;
                if (currentTimeForType >= chapterEndTime) {
                    ratio = 1;
                } else if (currentTimeForType < chapterStartTime) {
                    ratio = 0;
                } else {
                    ratio = (currentTimeForType - chapterStartTime) / (chapterEndTime - chapterStartTime);
                }
                chapterProgressEls[i].style.transform = `scaleX(${Math.min(1, ratio.toFixed(5))})`;
                accumulatedWidth += chapterWidth;
            }
        },

        // The main update function which selects chapter-mode or overall mode.
        update: function() {
            const player = document.querySelector(".html5-video-player");
            if (!player) return;
            // Skip updates while controls are visible
            if (!player.classList.contains('ytp-autohide')) return;

            if (this.options.UPDATE_VIDEO_TIMER) {
                this.updateCurrentTimeField(player);
            }

            // If chapter elements exist, update chapter-mode; otherwise use overall mode.
            let chapterElements = player.getElementsByClassName("ytp-progress-bar-padding");
            if (chapterElements.length > 0) {
                if (this.options.UPDATE_PROGRESSBAR) {
                    this.updateProgressBarWithChapters(player, "PROGRESSBAR");
                }
                if (this.options.UPDATE_BUFFERBAR) {
                    this.updateProgressBarWithChapters(player, "BUFFERBAR");
                }
            } else {
                this.updateOverallProgressBar(player);
            }
        },

        /* ------------------ Performance-friendly update loop ------------------ */
        rafId: 0,

        /* Starts a requestAnimationFrame loop only while the <video> is playing. */
        attachLoop: function(video) {
            let last = 0;
            const loop = (ts) => {
                if (video.paused || document.hidden) {
                    this.rafId = requestAnimationFrame(loop);
                    return;
                }
                if (!this.options.TARGET_FPS || ts - last >= 1000 / this.options.TARGET_FPS) {
                    this.update();
                    last = ts;
                }
                this.rafId = requestAnimationFrame(loop);
            };
            this.rafId = requestAnimationFrame(loop);
        },

        /* Locate the player & video element and wire play/pause events. */
        waitForVideo: function() {
            const tryAttach = () => {
                const player = document.querySelector('.html5-video-player');
                const video  = player?.querySelector('video');
                if (!video) {
                    setTimeout(tryAttach, 1000);
                    return;
                }

                const startLoop = () => {
                    cancelAnimationFrame(this.rafId);
                    this.attachLoop(video);
                };
                const stopLoop = () => cancelAnimationFrame(this.rafId);

                video.addEventListener('play',   startLoop);
                video.addEventListener('playing',startLoop);
                video.addEventListener('pause',  stopLoop);
                video.addEventListener('ended',  stopLoop);

                // Auto-start if video already playing
                if (!video.paused) startLoop();
            };
            tryAttach();
        },

        start: function() {
            this.waitForVideo();
        }
    };

    permanentProgressBar.start();
})();

Post reply

Sign in to post a reply.

长期地址
遇到问题?请前往 GitHub 提 Issues。