// ==UserScript== // @name Add YouTube Video Progress // @namespace https://greasyfork.org/en/users/85671-jcunews // @version 1.7.24 // @license GNU AGPLv3 // @author jcunews // @description Add progress bars (or dots) at bottom of YouTube video and progress text on the video page. On the progress text, the current video quality will have a "+" suffix if there's a higher one available. Hovering the mouse cursor onto the video quality text will show the current and available video quality IDs and short description. // @match https://www.youtube.com/* // @grant none // @run-at document-start // @downloadURL none // ==/UserScript== /* Notes: - For new YouTube layout only. - For videos which have 60fps and/or HDR video qualities, the listed available video qualities only includes the highest quality ones. The video player may present a lower video quality depending on the network speed, web browser capability, and user account setting. */ (() => { //===== CONFIGURATION BEGIN ===== var progressbarDotStyle = false; //show graphical progress as dots instead of bars var progressbarDotStyleSize = 4; //Dot size (for width & height) in pixels if dot style is enabled var progressbarHeight = 2; //in pixels var progressbarColor = "rgb(0, 0, 0, 0.3)"; //e.g. opaque: "#fff" or "#e0e0e0" or "cyan"; or semi-transparent: "rgb(0, 0, 0, 0.3)" (i.e. 30% opaque) var progressbarElapsedColor = "#f00"; var progressbarBufferColor = "#37f"; var contentLoadProcessDelay = 0; //number of milliseconds before processing dynamically loaded contents (increase if slow network/browser) var progressTextStyles = "display:inline-block;vertical-align:top;margin-left:10px;border:1px solid #ccc;border-radius:4px;padding:2px 0px 2px 2px;min-width:25ex;background:#eee;text-align:center;white-space:nowrap;font-size:9pt;line-height:normal"; //styles override for progress text in YouTube Dark Mode var progressTextStylesDark = "border:1px solid #bbb;background:#111;color:#bbb"; //===== CONFIGURATION END ===== var timerWaitInfo, timerProgressMonitor, timerWaitPlayer, timerDoubleCheck, vplayer, eleProgressText, eleProgressTextWidth = null, fmtMaps = {}; var resNums = { "light" : "144p", //(old ID) "tiny" : "144p", "small" : "240p", "medium" : "360p", //nHD "large" : "480p", //WNTSC "hd720" : "720p", //HD 1K "hd1080" : "1080p", //FHD 2K "hd1440" : "1440p", //QHD "hd2160" : "2160p", //UHD 4K "hd2880" : "2880p", //UHD+ 5K "highres": "4320p", //FUHD 8K (YouTube's highest resolution [2019 April]) "hd6480" : "6480p", //(fictional ID for 12K. Just in case...) "hd8640" : "8640p" //(fictional ID for QUHD 16K. Just in case...) }; var resDescs = { "light" : "light\xa0(144p ~QCIF)", //(old ID) "tiny" : "tiny\xa0(144p ~QCIF)", "small" : "small\xa0(240p ~SIF)", "medium" : "medium\xa0(360p nHD)", "large" : "large\xa0(480p WNTSC)", "hd720" : "hd720\xa0(720p HD 1K)", "hd1080" : "hd1080\xa0(1080p FHD 2K)", "hd1440" : "hd1440\xa0(1440p QHD)", "hd2160" : "hd2160\xa0(2160p UHD 4K)", "hd2880" : "hd2880\xa0(2880p UHD+ 5K)", "highres": "highres\xa0(4320p FUHD 8K)", //YouTube's highest resolution [2019 April] "hd6480" : "hd6480\xa0(6480p 12K)", //fictional ID for 12K. Just in case... "hd8640" : "hd8640\xa0(8640p QUHD 16K)" //fictional ID for QUHD 16K. Just in case... }; var fmts = [ ['3GP', 'MP4V', [13,17,36]], ['FLV', 'H263', [5,6]], ['FLV', 'H264', [34,35]], ['MP4', 'H264', [18,22,37,38,59,78,82,83,84,85,91,92,93,94,95,96,132,133,134,135,136,137,138,151,160,212,264,266,298,299]], ['WebM', 'VP8', [43,44,45,46,100,101,102,167,168,169,170,218,219]], ['WebM', 'VP9', [242,243,244,245,246,247,248,271,272,278,302,303,308,313,315]], ['WebM', 'VP9.2', [330,331,332,333,334,335,336,337,338,339]], //HDR. 388 & 339 for 2880p & 4320p are assumed. may actually be incorrect. ['M4A', 'AAC', [139,140,141,256,258]], ['M4A', 'DTS-ES', [325]], ['M4A', 'AC-3', [328]], ['WebM', 'Vorbis', [171,172]], ['WebM', 'Opus', [249,250,251]] ]; fmts.forEach(a => a[2].forEach(f => fmtMaps[f] = [a[0], a[1]])); function updProgressTextPos(a) { if (eleProgressTextWidth === null) return; (a = document.querySelector("#info-text")).classList.remove("floatingProgress"); vidprogress.style.display = "none"; if ((a.parentNode.lastElementChild.offsetLeft - a.offsetLeft - a.offsetWidth) < (eleProgressTextWidth + 10)) { a.classList.add("floatingProgress"); vidprogress.style.display = "inline-block"; } } var ytpr, ql, resDescs2; function processInfo(ev) { ql = null; if (window.vidprogress || (location.pathname !== "/watch")) return; if (!ev) ytpr = JSON.parse(window.ytplayer.config.args.player_response); clearTimeout(timerWaitInfo); (function waitInfo(a) { if (a = document.querySelector("#info-text #date")) { eleProgressText = document.createElement("DIV"); eleProgressText.id = "vidprogress"; eleProgressText.innerHTML = '\ \ '; eleProgressText.style.cssText = progressTextStyles + (document.documentElement.attributes["dark"] ? progressTextStylesDark : ""); a.parentNode.insertBefore(eleProgressText, a.nextSibling); addEventListener("resize", updProgressTextPos); addEventListener("yt-navigate-finish", updProgressTextPos); } else timerWaitInfo = setTimeout(waitInfo, 200); })(); } function processPlayer(ev) { function zerolead(n){ return n > 9 ? n : "0" + n; } function sec2hms(sec) { var c = sec % 60, d = Math.floor(sec / 60); return (d >= 60 ? zerolead(Math.floor(d / 60)) + ":" : "") + zerolead(d % 60) + ":" + zerolead(c); } function getPlayer() { return (vplayer = document.querySelector(".html5-video-player")); } function updProgress(a, b, c, d, e, f, g, l){ if (window.vidprogress) eleProgressText.style.cssText = progressTextStyles + ";" + (document.documentElement.attributes["dark"] ? progressTextStylesDark : ""); a = getPlayer(); if (a && window.vidprogress2b && a.getCurrentTime) try { if (window.curtime) try { if (eleProgressText.offsetWidth !== eleProgressTextWidth) { eleProgressTextWidth = eleProgressText.offsetWidth; updProgressTextPos(); } b = a.getPlaybackQuality(); if (!ql) { if (window["page-manager"]) { if (window["page-manager"].getCurrentData().playerResponse.trackingParams !== ytpr.trackingParams) { ytpr = JSON.parse(window["page-manager"].getCurrentData().player.args.player_response); } } if (ytpr) { ql = {}; resDescs2 = {}; ytpr.streamingData.adaptiveFormats.forEach((o, i) => { if (!o.audioQuality) { if (ql[o.quality]) { if (o.qualityLabel) { if (o.bitrate >= ql[o.quality][2]) { ql[o.quality][1] = o.qualityLabel; ql[o.quality][2] = o.bitrate; } } } else ql[o.quality] = [o.quality, o.qualityLabel, o.bitrate]; } }); Object.keys(ql).forEach(k => { resDescs2[k] = resDescs[k].replace("(" + resNums[k], "(" + (ql[k] = (ql[k][1] || ql[k][0]))); }); } } if (ql) { c = ql[b] || b; } else c = resNums[b] || b; (d = a.getAvailableQualityLevels()).pop(); curq.textContent = c + (d.indexOf(b) > 0 ? "+" : ""); e = a.getVideoStats(); g = fmtMaps[e.afmt] || ("a" + e.afmt); if (e.fmt) { //has video if (f = fmtMaps[e.fmt]) { f = `${f[0]} ${f[1]}`; } else f = "vid" + e.fmt; if (e.afmt) { //video & audio if (g = fmtMaps[e.afmt]) { e = ` [${f} ${g[1]}]`; } else e = ` [${f} aud${e.afmt}]`; } else { //no audio. video only e = ` [${f}]`; } } else if (e.afmt) { //no video. audio only if (f = fmtMaps[e.afmt]) { e = ` [${f[0]} ${f[1]}]`; } else e = ` [aud${e.afmt}]`; } else e = ""; if (ql) { curq.title = `Current: ${resDescs2[b] || b}${e} (${a.offsetWidth}x${a.offsetHeight} viewport)\nAvailable: ${d.map(b => resDescs2[b] || b).join(", ")}`; } else curq.title = `Current: ${resDescs[b] || b}${e} (${a.offsetWidth}x${a.offsetHeight} viewport)\nAvailable: ${d.map(b => resDescs[b] || b).join(", ")}`; } catch(b) { curq.textContent = "???"; curq.title = ""; } b = a.getCurrentTime(); if (b >= 0) { l = a.getDuration(); if (!a.getVideoData().isLive) { if (window.curtime) { curtime.textContent = sec2hms(Math.floor(b)) + " / " + sec2hms(Math.floor(l)) + " (" + Math.floor(b * 100 / l) + "%)"; } if (progressbarDotStyle) { vidprogress2b.style.left = Math.ceil((b / l) * vidprogress2.offsetWidth) + "px"; vidprogress2c.style.left = Math.ceil((a.getVideoBytesLoaded() / a.getVideoBytesTotal()) * vidprogress2.offsetWidth) + "px"; } else { vidprogress2b.style.width = Math.ceil((b / l) * vidprogress2.offsetWidth) + "px"; vidprogress2c.style.width = Math.ceil((a.getVideoBytesLoaded() / a.getVideoBytesTotal()) * vidprogress2.offsetWidth) + "px"; } } else { if (window.curtime) curtime.textContent = "LIVE"; if (progressbarDotStyle) { vidprogress2b.style.left = "100%"; } else vidprogress2b.style.width = "100%"; } } else throw 0; } catch(a) { if (window.curtime) curtime.textContent = "???"; if (progressbarDotStyle) { vidprogress2b.style.left = "0px"; vidprogress2c.style.left = "0px"; } else { vidprogress2b.style.width = "0px"; vidprogress2c.style.width = "0px"; } } } function resumeProgressMonitor() { if (timerProgressMonitor) return; updProgress(); timerProgressMonitor = setInterval(updProgress, 200); } function pauseProgressMonitor() { clearInterval(timerProgressMonitor); timerProgressMonitor = 0; updProgress(); } if (ev) console.log(ev); clearInterval(timerProgressMonitor); timerProgressMonitor = 0; clearTimeout(timerWaitPlayer); timerWaitPlayer = 0; clearInterval(timerDoubleCheck); timerDoubleCheck = 0; (function waitPlayer(v) { if (!window.vidprogress2 && getPlayer() && (a = vplayer.parentNode.querySelector("video"))) { b = document.createElement("DIV"); b.id = "vidprogress2"; b.style.cssText = `opacity:.66;position:absolute;z-index:10;bottom:0;width:100%;height:${ progressbarDotStyle ? progressbarDotStyleSize : progressbarHeight}px;background:${progressbarColor}`; v = progressbarDotStyle ? "width:" + progressbarDotStyleSize + "px;margin-left:-" + Math.floor(progressbarDotStyleSize / 2) + "px;" : ""; b.innerHTML = `
`; vplayer.appendChild(b); if (vplayer.getPlayerState() === 1) resumeProgressMonitor(); //useful: onLoadedMetadata(), onStateChange(state), onPlayVideo(info), onReady(playerApi), onVideoAreaChange(), onVideoDataChange(info) //states: -1=notReady, 0=ended, 1=playing, 2=paused, 3=ready, 4=???, 5=notAvailable? vplayer.addEventListener("onLoadedMetadata", resumeProgressMonitor); vplayer.addEventListener("onStateChange", function(state) { if (state === 1) { resumeProgressMonitor(); } else pauseProgressMonitor(); }); } else timerWaitPlayer = setTimeout(waitPlayer, 200); })(); function doubleCheck() { if (getPlayer() && vplayer.getPlayerState) { if (vplayer.getPlayerState() === 1) { resumeProgressMonitor(); } else pauseProgressMonitor(); } } if (!timerDoubleCheck) timerDoubleCheck = setInterval(doubleCheck, 500); } addEventListener("yt-page-data-updated", processInfo); addEventListener("yt-player-released", processPlayer); addEventListener("load", function() { processInfo(); processPlayer(); }); addEventListener("spfprocess", function(ev) { setTimeout(function() { processInfo(); processPlayer(); }, contentLoadProcessDelay); }); })();