Greasy Fork is available in English.

Add YouTube Video Progress

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.

Pada tanggal 15 April 2020. Lihat %(latest_version_link).

// ==UserScript==
// @name         Add YouTube Video Progress
// @namespace    https://greasyfork.org/en/users/85671-jcunews
// @version      1.7.28
// @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
// ==/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 = '<span id="curq" style="font-weight:500"></span>\
<span id="curtime" style="display:inline-block;margin-left:1ex"></span>\
<style>#info-text{position:relative;margin-top:1rem}#info-text.floatingProgress{overflow:visible!important}#info-text.floatingProgress>#vidprogress{position:absolute;right:0;bottom:1.7rem}</style>';
      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(a) {
    if (ytpr && window["page-manager"] && (a = window["page-manager"].getCurrentData()) && a.player) {
      return (vplayer = window[a.player.attrs.id]);
    } else 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"] && (c = window["page-manager"].getCurrentData())) {
            if (c.playerResponse && (!ytpr || (c.playerResponse.trackingParams !== ytpr.trackingParams))) {
              ytpr = JSON.parse(c.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();
  }

  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 = `<div id="vidprogress2c" style="position:absolute;${v}height:100%;background:${progressbarBufferColor}"></div>
<div id="vidprogress2b" style="position:absolute;${v}height:100%;background:${progressbarElapsedColor}"></div>`;
      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);
});

})();