Stick YouTube video progress bar to the player bottom
// ==UserScript==
// @name Stick YouTube Progress Bar
// @version 1.0.18
// @match https://www.youtube.com/**
// @author peng-devs
// @namespace https://greasyfork.org/users/57176
// @description Stick YouTube video progress bar to the player bottom
// @icon https://www.youtube.com/s/desktop/c1d331ff/img/favicon_48x48.png
// @grant none
// @allFrames true
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const NAME = "Stick YouTube Progress Bar";
const UPDATE_INTERVAL = 500;
const SMOOTH_ANIMATION = false;
let observer;
let interval;
function main() {
observer?.disconnect();
clearInterval(interval);
document.getElementById("stick_progress")?.remove();
observer = undefined;
interval = undefined;
if (
!location.pathname.startsWith("/watch") &&
!location.pathname.startsWith("/live")
)
return;
observer = observe(document.body, () => {
if (!observer) return;
if (document.querySelector(".ytp-time-display.ytp-live")) {
document.getElementById("stick_progress")?.remove();
observer.disconnect();
console.log(`[${NAME}] canceled in livestream`);
return;
}
const initialized = document.querySelector("video.video-stream");
if (!initialized) return;
const duration = initialized.duration;
if (isNaN(duration)) return;
console.log(`[${NAME}] initializing...`);
init_stick_progress_bar();
observer.disconnect();
console.log(`[${NAME}] loaded`);
});
}
function init_stick_progress_bar() {
const { container, progress_bar, buffer_bar } = create_progress_bar();
const player = document.querySelector("#movie_player");
player.append(container);
let video = document.querySelector("video.video-stream[src]");
interval = setInterval(() => {
if (!video?.isConnected || !video.getAttribute("src")) {
// youtube 有時候會抽風把頁面重新 re-render 生成新的 video
console.debug(`[${NAME}] detect page re-render, reset video`);
video = document.querySelector("video.video-stream[src]");
}
if (!video) return;
if (!video.duration || isNaN(video.duration)) return;
// skip during ads to avoid showing ad progress
if (player.classList.contains("ad-showing")) return;
const progress = video.currentTime / video.duration;
progress_bar.style.transform = `scaleX(${progress})`;
if (video.buffered.length > 0) {
let buf_end = 0;
for (let i = 0; i < video.buffered.length; i++) {
if (
video.currentTime >= video.buffered.start(i) &&
video.currentTime <= video.buffered.end(i)
) {
buf_end = video.buffered.end(i);
break;
}
}
buffer_bar.style.transform = `scaleX(${buf_end / video.duration})`;
}
}, UPDATE_INTERVAL);
}
function create_progress_bar() {
if (!document.querySelector(`style[data-source="${NAME}"]`)) {
inject_custom_style(`
#stick_progress {
display: none;
z-index: 32;
position: absolute;
bottom: 4px;
width: 97.5%;
height: 4px;
margin: 0 1.25%;
background-color: rgba(255, 255, 255, .2);
}
.stick_progress_bar, .stick_buffer_bar {
position: absolute;
width: 100%;
height: 100%;
transform-origin: left;
transform: scaleX(0);
${SMOOTH_ANIMATION ? `transition: all ${UPDATE_INTERVAL - 50}ms linear;` : ""}
}
.stick_buffer_bar {
z-index: 33;
background-color: rgba(255, 255, 255, .4);
}
.stick_progress_bar {
z-index: 34;
background-color: #f00;
}
.ytp-autohide #stick_progress {
display: block;
}
`);
}
const container = document.createElement("div");
container.id = "stick_progress";
const progress_bar = document.createElement("div");
progress_bar.className = "stick_progress_bar";
container.append(progress_bar);
const buffer_bar = document.createElement("div");
buffer_bar.className = "stick_buffer_bar";
container.append(buffer_bar);
return {
container,
progress_bar,
buffer_bar,
};
}
function inject_custom_style(css) {
const style = document.createElement("style");
style.dataset.source = NAME;
style.textContent = css;
document.head.append(style);
}
function observe(dom, callback) {
const observer = new MutationObserver(callback);
observer.observe(dom, { childList: true, subtree: true });
return observer;
}
document.addEventListener("yt-navigate-finish", main, true);
main();
})();