// ==UserScript==
// @name           YouTube Links
// @namespace      http://www.smallapple.net/labs/YouTubeLinks/
// @description    Download YouTube videos. Video formats are listed at the top of the watch page. Video links are tagged so that they can be downloaded easily.
// @author         Ng Hun Yang
// @include        http://*.youtube.com/*
// @include        http://youtube.com/*
// @include        https://*.youtube.com/*
// @include        https://youtube.com/*
// @match          *://*.youtube.com/*
// @match          *://*.googlevideo.com/*
// @match          *://s.ytimg.com/yts/jsbin/*
// @grant          GM_xmlhttpRequest
// @grant          GM.xmlHttpRequest
// @connect        googlevideo.com
// @connect        s.ytimg.com
// @version        2.47
// ==/UserScript==
/* This is based on YouTube HD Suite 3.4.1 */
/* Tested on Firefox 5.0, Chrome 13 and Opera 11.50 */
(function() {
// =============================================================================
if(window.trustedTypes && window.trustedTypes.createPolicy) {
  window.trustedTypes.createPolicy("default", {
    createHTML: (string) => string,
    createScript: string => string
    });
  }
var win = typeof(unsafeWindow) !== "undefined" ? unsafeWindow : window;
var doc = win.document;
var loc = win.location;
if(win.top != win.self)
  return;
var unsafeWin = win;
// Hack to get unsafe window in Chrome
(function() {
var isChrome = navigator.userAgent.toLowerCase().indexOf("chrome") >= 0;
if(!isChrome)
  return;
// Chrome 27 fixed this exploit, but luckily, its unsafeWin now works for us
try {
  var div = doc.createElement("div");
  div.setAttribute("onclick", "return window;");
  unsafeWin = div.onclick();
} catch(e) {
  }
}) ();
var ua = navigator.userAgent || "";
var isEdgeBrowser = ua.match(/ Edge\//);
// =============================================================================
if(typeof GM == "object" && GM.xmlHttpRequest && typeof GM_xmlhttpRequest == "undefined") {
  GM_xmlhttpRequest = async function(opts) {
    await GM.xmlHttpRequest(opts);
    }
  }
// =============================================================================
var SCRIPT_NAME = "YouTube Links";
var relInfo = {
  ver: 24700,
  ts: 2024101500,
  desc: "Hide DRC audio by default"
  };
var SCRIPT_UPDATE_LINK = loc.protocol + "//greasyfork.org/scripts/5565-youtube-links-updater/code/YouTube Links Updater.user.js";
var SCRIPT_LINK = loc.protocol + "//greasyfork.org/scripts/5566-youtube-links/code/YouTube Links.user.js";
// =============================================================================
var dom = {};
dom.gE = function(id) {
  return doc.getElementById(id);
  };
dom.gT = function(dom, tag) {
  if(arguments.length == 1) {
    tag = dom;
    dom = doc;
    }
  return dom.getElementsByTagName(tag);
  };
dom.cE = function(tag) {
  return document.createElement(tag);
  };
dom.cT = function(s) {
  return doc.createTextNode(s);
  };
dom.attr = function(obj, k, v) {
  if(arguments.length == 2)
    return obj.getAttribute(k);
  obj.setAttribute(k, v);
  };
dom.prepend = function(obj, child) {
  obj.insertBefore(child, obj.firstChild);
  };
dom.append = function(obj, child) {
  obj.appendChild(child);
  };
dom.offset = function(obj) {
  var x = 0;
  var y = 0;
  if(obj.getBoundingClientRect) {
    var box = obj.getBoundingClientRect();
    var owner = obj.ownerDocument;
    x = box.left + Math.max(owner.documentElement.scrollLeft, owner.body.scrollLeft) - owner.documentElement.clientLeft;
    y = box.top + Math.max(owner.documentElement.scrollTop, owner.body.scrollTop) - owner.documentElement.clientTop;
    return { left: x, top: y };
    }
  if(obj.offsetParent) {
    do {
      x += obj.offsetLeft - obj.scrollLeft;
      y += obj.offsetTop - obj.scrollTop;
      obj = obj.offsetParent;
    } while(obj);
    }
  return { left: x, top: y };
  };
dom.inViewport = function(el) {
  var rect = el.getBoundingClientRect();
  if(rect.width == 0 && rect.height == 0)
    return false;
  return rect.bottom >= 0 &&
   rect.right >= 0 &&
   rect.top < (win.innerHeight || doc.documentElement.clientHeight) &&
   rect.left < (win.innerWidth || doc.documentElement.clientWidth);
  };
dom.html = function(obj, s) {
  if(arguments.length == 1)
    return obj.innerHTML;
  obj.innerHTML = s;
  };
dom.emitHtml = function(tag, attrs, body) {
  if(arguments.length == 2) {
    if(typeof(attrs) == "string") {
      body = attrs;
      attrs = {};
      }
    }
  var list = [];
  for(var k in attrs) {
    if(attrs[k] != null)
      list.push(k + "='" + attrs[k].replace(/'/g, "'") + "'");
    }
  var s = "<" + tag + " " + list.join(" ") + ">";
  if(body != null)
    s += body + "</" + tag + ">";
  return s;
  };
dom.emitCssStyles = function(styles) {
  var list = [];
  for(var k in styles) {
    list.push(k + ": " + styles[k] + ";");
    }
  return " { " + list.join(" ") + " }";
  };
dom.ajax = function(opts) {
  function newXhr() {
    if(window.ActiveXObject) {
      try {
        return new ActiveXObject("Msxml2.XMLHTTP");
        } catch(e) {
          }
      try {
        return new ActiveXObject("Microsoft.XMLHTTP");
        } catch(e) {
          return null;
          }
      }
    if(window.XMLHttpRequest)
      return new XMLHttpRequest();
    return null;
    }
  function nop() {
    }
  // Entry point
  var xhr = newXhr();
  opts = addProp({
    type: "GET",
    async: true,
    success: nop,
    error: nop,
    complete: nop
    }, opts);
  xhr.open(opts.type, opts.url, opts.async);
  xhr.onreadystatechange = function() {
    if(xhr.readyState == 4) {
      var status = +xhr.status;
      if(status >= 200 && status < 300) {
        opts.success(xhr.responseText, "success", xhr);
        }
      else {
        opts.error(xhr, "error");
        }
      opts.complete(xhr);
      }
    };
  xhr.send("");
  };
dom.crossAjax = function(opts) {
  function wrapXhr(xhr) {
    var headers = xhr.responseHeaders.replace("\r", "").split("\n");
    var obj = {};
    forEach(headers, function(idx, elm) {
      var nv = elm.split(":");
      if(nv[1] != null)
        obj[nv[0].toLowerCase()] = nv[1].replace(/^\s+/, "").replace(/\s+$/, "");
      });
    var responseXML = null;
    if(opts.dataType == "xml")
      responseXML = new DOMParser().parseFromString(xhr.responseText, "text/xml");
    return {
      responseText: xhr.responseText,
      responseXML: responseXML,
      status: xhr.status,
      getAllResponseHeaders: function() {
        return xhr.responseHeaders;
        },
      getResponseHeader: function(name) {
        return obj[name.toLowerCase()];
        }
      };
    }
  function nop() {
    }
  // Entry point
  opts = addProp({
    type: "GET",
    async: true,
    success: nop,
    error: nop,
    complete: nop
    }, opts);
  if(typeof GM_xmlhttpRequest === "undefined") {
    setTimeout(function() {
      var xhr = {};
      opts.error(xhr, "error");
      opts.complete(xhr);
      }, 0);
    return;
    }
  // TamperMonkey does not handle URLs starting with //
  var url;
  if(opts.url.match(/^\/\//))
    url = loc.protocol + opts.url;
  else
    url = opts.url;
  GM_xmlhttpRequest({
    method: opts.type,
    url: url,
    synchronous: !opts.async,
    onload: function(xhr) {
      xhr = wrapXhr(xhr);
      if(xhr.status >= 200 && xhr.status < 300)
        opts.success(xhr.responseXML || xhr.responseText, "success", xhr);
      else
        opts.error(xhr, "error");
      opts.complete(xhr);
      },
    onerror: function(xhr) {
      xhr = wrapXhr(xhr);
      opts.error(xhr, "error");
      opts.complete(xhr);
      }
    });
  };
dom.addEvent = function(e, type, fn) {
  function mouseEvent(evt) {
    if(this != evt.relatedTarget && !dom.isAChildOf(this, evt.relatedTarget))
      fn.call(this, evt);
    }
  // Entry point
  if(e.addEventListener) {
    var effFn = fn;
    if(type == "mouseenter") {
      type = "mouseover";
      effFn = mouseEvent;
      }
    else if(type == "mouseleave") {
      type = "mouseout";
      effFn = mouseEvent;
      }
    e.addEventListener(type, effFn, /*capturePhase*/ false);
    }
  else
    e.attachEvent("on" + type, function() { fn(win.event); });
  };
dom.insertCss = function (styles) {
  var ss = dom.cE("style");
  dom.attr(ss, "type", "text/css");
  var hh = dom.gT("head") [0];
  dom.append(hh, ss);
  dom.append(ss, dom.cT(styles));
  };
dom.isAChildOf = function(parent, child) {
  if(parent === child)
    return false;
  while(child && child !== parent) {
    child = child.parentNode;
    }
  return child === parent;
  };
// -----------------------------------------------------------------------------
function timeNowInSec() {
  return Math.round(+new Date() / 1000);
  }
function forLoop(opts, fn) {
  opts = addProp({ start: 0, inc: 1 }, opts);
  for(var idx = opts.start; idx < opts.num; idx += opts.inc) {
    if(fn.call(opts, idx, opts) === false)
      break;
    }
  }
function forEach(list, fn) {
  forLoop({ num: list.length }, function(idx) {
    return fn.call(list[idx], idx, list[idx]);
    });
  }
function addProp(dest, src) {
  for(var k in src) {
    if(src[k] != null)
      dest[k] = src[k];
    }
  return dest;
  }
function inArray(elm, array) {
  for(var i = 0; i < array.length; ++i) {
    if(array[i] === elm)
      return i;
    }
  return -1;
  }
function unescHtmlEntities(s) {
  return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
  }
function logMsg(s) {
  win.console.log(s);
  }
function cnvSafeFname(s) {
  return s.replace(/:/g, "-").replace(/"/g, "'").replace(/[\\/|*?]/g, "_");
  }
function encodeSafeFname(s) {
  return encodeURIComponent(cnvSafeFname(s)).replace(/'/g, "%27");
  }
function getVideoName(s) {
  var list = [
    { name: "3GP", codec: "video\\/3gpp" },
    { name: "FLV", codec: "video\\/x-flv" },
    { name: "M4V", codec: "video\\/x-m4v" },
    { name: "MP3", codec: "audio\\/mpeg" },
    { name: "MP4", codec: "video\\/mp4" },
    { name: "M4A", codec: "audio\\/mp4" },
    { name: "QT", codec: "video\\/quicktime" },
    { name: "WEBM", codec: "audio\\/webm" },
    { name: "WEBM", codec: "video\\/webm" },
    { name: "WMV", codec: "video\\/ms-wmv" }
    ];
  var spCodecs = {
    "av01": "AV1",
    "opus": "OPUS",
    "vorbis": "VOR",
    "vp9": "VP9"
    };
  if(s.match(/;\s*\+?codecs=\"([a-zA-Z0-9]+)/)) {
    var str = RegExp.$1;
    if(spCodecs[str])
      return spCodecs[str];
    }
  var name = "?";
  forEach(list, function(idx, elm) {
    if(s.match("^" + elm.codec)) {
      name = elm.name;
      return false;
      }
    });
  return name;
  }
function getAspectRatio(wd, ht) {
  return Math.round(wd / ht * 100) / 100;
  }
function cnvResName(res) {
  var resMap = {
    "audio": "Audio"
    };
  if(resMap[res])
    return resMap[res];
  if(!res.match(/^(\d+)x(\d+)/))
    return res;
  var wd = +RegExp.$1;
  var ht = +RegExp.$2;
  if(wd < ht) {
    var t = wd;
    wd = ht;
    ht = t;
    }
  var horzResAr = [
    [ 16000, "16K" ],
    [ 14000, "14K" ],
    [ 12000, "12K" ],
    [ 10000, "10K" ],
    [ 8000, "8K" ],
    [ 6000, "6K" ],
    [ 5000, "5K" ],
    [ 4000, "4K" ],
    [ 3000, "3K" ],
    [ 2048, "2K" ]
    ];
  var vertResAr = [
    [ 4320, "8K" ],
    [ 3160, "6K" ],
    [ 2880, "5K" ],
    [ 2160, "4K" ],
    [ 1728, "3K" ],
    [ 1536, "2K" ],
    [ 240, "240v" ],
    [ 144, "144v" ]
    ];
  var aspectRatio = getAspectRatio(wd, ht);
  var name;
  do {
    forEach(horzResAr, function(idx, elm) {
      var tolerance = elm[0] * 0.05;
      if(wd >= elm[0] * 0.95) {
        name = elm[1];
        return false;
        }
      });
    if(name)
      break;
    if(aspectRatio >= WIDE_AR_CUTOFF)
      ht = Math.round(wd * 9 / 16);
    forEach(vertResAr, function(idx, elm) {
      var tolerance = elm[0] * 0.05;
      if(ht >= elm[0] - tolerance && ht < elm[0] + tolerance) {
        name = elm[1];
        return false;
        }
      });
    if(name)
      break;
    // Snap to std vert res
    var vertResList = [ 4320, 3160, 2880, 2160, 1536, 1080, 720, 480, 360, 240, 144 ];
    forEach(vertResList, function(idx, elm) {
      var tolerance = elm * 0.05;
      if(ht >= elm - tolerance && ht < elm + tolerance) {
        ht = elm;
        return false;
        }
      });
    name = String(ht) + (aspectRatio < FULL_AR_CUTOFF ? "f" : "p");
  } while(false);
  if(aspectRatio >= ULTRA_WIDE_AR_CUTOFF)
    name = "u" + name;
  else if(aspectRatio >= WIDE_AR_CUTOFF)
    name = "w" + name;
  return name;
  }
function mapResToQuality(res) {
  if(!res.match(/^(\d+)x(\d+)/))
    return res;
  var wd = +RegExp.$1;
  var ht = +RegExp.$2;
  if(wd < ht) {
    var t = wd;
    wd = ht;
    ht = t;
    }
  var resList = [
    { res: 3160, q : "ultrahighres" },
    { res: 1536, q : "highres" },
    { res: 1200, q: "hd2k" },
    { res: 1080, q: "hd1080" },
    { res: 720, q : "hd720" },
    { res: 480, q : "large" },
    { res: 360, q : "medium" }
    ];
  var q;
  forEach(resList, function(idx, elm) {
    if(ht >= elm.res) {
      q = elm.q;
      return false;
      }
    });
  return q || "small";
  }
function getQualityIdx(quality) {
  var list = [ "small", "medium", "large", "hd720", "hd1080", "hd2k", "highres", "ultrahighres" ];
  for(var i = 0; i < list.length; ++i) {
    if(list[i] == quality)
      return i;
    }
  return -1;
  }
// =============================================================================
RegExp.escape = function(s) {
  return String(s).replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
  };
var decryptSig = {
  store: {}
  };
(function () {
var SIG_STORE_ID = "ujsYtLinksSig";
var CHK_SIG_INTERVAL = 3 * 86400;
decryptSig.load = function() {
  var obj = localStorage[SIG_STORE_ID];
  if(obj == null)
    return;
  decryptSig.store = JSON.parse(obj);
  };
decryptSig.save = function() {
  localStorage[SIG_STORE_ID] = JSON.stringify(decryptSig.store);
  };
decryptSig.extractScriptUrl = function(data) {
  if(data.match(/ytplayer.config\s*=.*"assets"\s*:\s*\{.*"js"\s*:\s*(".+?")[,}]/))
    return JSON.parse(RegExp.$1);
  else if(data.match(/ytplayer.web_player_context_config\s*=\s*\{.*"rootElementId":"movie_player","jsUrl":(".+?")[,}]/))
    return JSON.parse(RegExp.$1);
  else if(data.match(/,"WEB_PLAYER_CONTEXT_CONFIGS":{.*"rootElementId":"movie_player","jsUrl":(".+?")[,}]/))
    return JSON.parse(RegExp.$1);
  else
    return false;
  };
decryptSig.getScriptName = function(url) {
  if(url.match(/\/yts\/jsbin\/player-(.*)\/[a-zA-Z0-9_]+\.js$/))
    return RegExp.$1;
  if(url.match(/\/yts\/jsbin\/html5player-(.*)\/html5player\.js$/))
    return RegExp.$1;
  if(url.match(/\/html5player-(.*)\.js$/))
    return RegExp.$1;
  return url;
  };
decryptSig.fetchScript = function(scriptName, url) {
  function success(data) {
    data = data.replace(/\n|\r/g, "");
    var sigFn;
    forEach([
      /\.signature\s*=\s*(\w+)\(\w+\)/,
      /\.set\(\"signature\",([\w$]+)\(\w+\)\)/,
      /\/yt\.akamaized\.net\/\)\s*\|\|\s*\w+\.set\s*\(.*?\)\s*;\s*\w+\s*&&\s*\w+\.set\s*\(\s*\w+\s*,\s*(?:encodeURIComponent\s*\()?([\w$]+)\s*\(/,
      /\b([a-zA-Z0-9$]{,3})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)/,
      /([a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)\s*;\s*\w+\.\w+\s*\(/,
      /([a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)/,
      /;\s*\w+\s*&&\s*\w+\.set\(\w+\s*,\s*(?:encodeURIComponent\s*\()?([\w$]+)\s*\(/,
      /;\s*\w+\s*&&\s*\w+\.set\(\w+\s*,\s*\([^)]*\)\s*\(\s*([\w$]+)\s*\(/
      ], function(idx, regex) {
        if(data.match(regex)) {
          sigFn = RegExp.$1;
          return false;
          }
        });
    if(sigFn == null)
      return;
    //console.log(scriptName + " sig fn: " + sigFn);
    var fnArgBody = '\\s*\\((\\w+)\\)\\s*{(\\w+=\\w+\\.split\\(""\\);.+?;return \\w+\\.join\\(""\\))';
    if(!data.match(new RegExp("function " + RegExp.escape(sigFn) + fnArgBody)) &&
     !data.match(new RegExp("(?:var |[,;]\\s*|^\\s*)" + RegExp.escape(sigFn) + "\\s*=\\s*function" + fnArgBody)))
      return;
    var fnParam = RegExp.$1;
    var fnBody = RegExp.$2;
    var fnHlp = {};
    var objHlp = {};
    //console.log("param: " + fnParam);
    //console.log(fnBody);
    fnBody = fnBody.split(";");
    forEach(fnBody, function(idx, elm) {
      // its own property
      if(elm.match(new RegExp("^" + fnParam + "=" + fnParam + "\\.")))
        return;
      // global fn
      if(elm.match(new RegExp("^" + fnParam + "=([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
        var name = RegExp.$1;
        //console.log("fnHlp: " + name);
        if(fnHlp[name])
          return;
        if(data.match(new RegExp("(function " + RegExp.escape(RegExp.$1) + ".+?;return \\w+})")))
          fnHlp[name] = RegExp.$1;
        return;
        }
      // object fn
      if(elm.match(new RegExp("^([a-zA-Z_$][a-zA-Z0-9_$]*)\.([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
        var name = RegExp.$1;
        //console.log("objHlp: " + name);
        if(objHlp[name])
          return;
        if(data.match(new RegExp("(var " + RegExp.escape(RegExp.$1) + "={.+?};)")))
          objHlp[name] = RegExp.$1;
        return;
        }
      });
    //console.log(fnHlp);
    //console.log(objHlp);
    var fnHlpStr = "";
    for(var k in fnHlp)
      fnHlpStr += fnHlp[k];
    for(var k in objHlp)
      fnHlpStr += objHlp[k];
    var fullFn = "function(" + fnParam + "){" + fnHlpStr + fnBody.join(";") + "}";
    //console.log(fullFn);
    decryptSig.store[scriptName] = { ver: relInfo.ver, ts: timeNowInSec(), fn: fullFn };
    //console.log(decryptSig);
    decryptSig.save();
    }
  // Entry point
  dom.ajax({ url: url, success: success });
  };
decryptSig.condFetchScript = function(url) {
  var scriptName = decryptSig.getScriptName(url);
  var store = decryptSig.store[scriptName];
  var now = timeNowInSec();
  if(store && now - store.ts < CHK_SIG_INTERVAL && store.ver == relInfo.ver)
    return;
  decryptSig.fetchScript(scriptName, url);
  };
}) ();
function deobfuscateVideoSig(scriptName, sig) {
  if(!decryptSig.store[scriptName])
    return sig;
  //console.log(decryptSig.store[scriptName].fn);
  try {
    sig = eval("(" + decryptSig.store[scriptName].fn + ") (\"" + sig + "\")");
  } catch(e) {
    }
  return sig;
  }
// =============================================================================
function deobfuscateSigInObj(map, obj) {
  if(obj.s == null || obj.sig != null)
    return;
  var sig = deobfuscateVideoSig(map.scriptName, obj.s);
  if(sig != obj.s) {
    obj.sig = sig;
    delete obj.s;
    }
  }
function parseStreamMap(map, value) {
  var fmtUrlList = [];
  forEach(value.split(","), function(idx, elm) {
    var elms = elm.replace(/\\\//g, "/").replace(/\\u0026/g, "&").split("&");
    var obj = {};
    forEach(elms, function(idx, elm) {
      var kv = elm.split("=");
      obj[kv[0]] = decodeURIComponent(kv[1]);
      });
    obj.itag = +obj.itag;
    if(obj.conn != null && obj.conn.match(/^rtmpe:\/\//))
      obj.isDrm = true;
    if(obj.s != null && obj.sig == null) {
      var sig = deobfuscateVideoSig(map.scriptName, obj.s);
      if(sig != obj.s) {
        obj.sig = sig;
        delete obj.s;
        }
      }
    fmtUrlList.push(obj);
    });
  //logMsg(fmtUrlList);
  map.fmtUrlList = fmtUrlList;
  }
function parseAdaptiveStreamMap(map, value) {
  var fmtUrlList = [];
  forEach(value.split(","), function(idx, elm) {
    var elms = elm.replace(/\\\//g, "/").replace(/\\u0026/g, "&").split("&");
    var obj = {};
    forEach(elms, function(idx, elm) {
      var kv = elm.split("=");
      obj[kv[0]] = decodeURIComponent(kv[1]);
      });
    obj.itag = +obj.itag;
    if(obj.bitrate != null)
      obj.bitrate = +obj.bitrate;
    if(obj.clen != null)
      obj.clen = +obj.clen;
    if(obj.fps != null)
      obj.fps = +obj.fps;
    //logMsg(obj);
    //logMsg(map.videoId + ": " + obj.index + " " + obj.init + " " + obj.itag + " " + obj.size + " " + obj.bitrate + " " + obj.type);
    if(obj.type.match(/^video\/mp4/) && !obj.type.match(/;\s*\+?codecs="av01\./))
      obj.effType = "video/x-m4v";
    if(obj.type.match(/^audio\//))
      obj.size = "audio";
    obj.quality = mapResToQuality(obj.size);
    if(!map.adaptiveAR && obj.size.match(/^(\d+)x(\d+)/))
      map.adaptiveAR = +RegExp.$1 / +RegExp.$2;
    deobfuscateSigInObj(map, obj);
    fmtUrlList.push(obj);
    map.fmtMap[obj.itag] = { res: cnvResName(obj.size) };
    });
  //logMsg(fmtUrlList);
  map.fmtUrlList = map.fmtUrlList.concat(fmtUrlList);
  }
function parseFmtList(map, value) {
  var list = value.split(",");
  forEach(list, function(idx, elm) {
    var elms = elm.replace(/\\\//g, "/").split("/");
    var fmtId = elms[0];
    var res = elms[1];
    elms.splice(/*idx*/ 0, /*rm*/ 2);
    if(map.adaptiveAR && res.match(/^(\d+)x(\d+)/))
      res = Math.round(+RegExp.$2 * map.adaptiveAR) + "x" + RegExp.$2;
    map.fmtMap[fmtId] = { res: cnvResName(res), vars: elms };
    });
  //logMsg(map.fmtMap);
  }
function parseNewFormatsMap(map, str, unescSlashFlag) {
  if(unescSlashFlag)
    str = str.replace(/\\\//g, "/").replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
  var list = JSON.parse(str);
  forEach(list, function(idx, elm) {
    var obj = {
      bitrate: elm.bitrate,
      fps: elm.fps,
      drc: elm.isDrc,
      itag: elm.itag,
      type: elm.mimeType,
      url: elm.url // no longer present (2020-06)
      };
    // Distinguish between AV1, M4V and MP4
    if(elm.audioQuality == null && obj.type.match(/^video\/mp4/) && !obj.type.match(/;\s*\+?codecs="av01\./))
      obj.effType = "video/x-m4v";
    if(elm.contentLength != null)
      obj.clen = +elm.contentLength;
    if(obj.type.match(/^audio\//))
      obj.size = "audio";
    else
      obj.size = elm.width + "x" + elm.height;
    obj.quality = mapResToQuality(obj.size);
    var cipher = elm.cipher || elm.signatureCipher;
    if(cipher) {
      forEach(cipher.split("&"), function(idx, elm) {
        var kv = elm.split("=");
        obj[kv[0]] = decodeURIComponent(kv[1]);
        });
      deobfuscateSigInObj(map, obj);
      }
    map.fmtUrlList.push(obj);
    if(map.fmtMap[obj.itag] == null)
      map.fmtMap[obj.itag] = { res: cnvResName(obj.size) };
    });
  }
function getVideoInfo(url, callback) {
  function getVideoNameByType(elm) {
    return getVideoName(elm.effType || elm.type);
    }
  function success(data) {
    var map = {};
    if(data.match(/<div\s+id="verify-details">/)) {
      logMsg("Skipping " + url);
      return;
      }
    if(data.match(/<h1\s+id="unavailable-message">/)) {
      logMsg("Not avail " + url);
      return;
      }
    if(data.match(/"t":\s?"(.+?)"/))
      map.t = RegExp.$1;
    if(data.match(/"(?:video_id|videoId)":\s?"(.+?)"/))
      map.videoId = RegExp.$1;
    else if(data.match(/\\"videoId\\":\s?\\"(.+?)\\"/))
      map.videoId = RegExp.$1;
    else if(data.match(/'VIDEO_ID':\s?"(.+?)",/))
      map.videoId = RegExp.$1;
    if(!map.videoId) {
      logMsg("No videoId; skipping " + url);
      return;
      }
    map.scriptUrl = decryptSig.extractScriptUrl(data);
    if(map.scriptUrl) {
      //logMsg(map.videoId + " script: " + map.scriptUrl);
      map.scriptName = decryptSig.getScriptName(map.scriptUrl);
      decryptSig.condFetchScript(map.scriptUrl);
      }
    if(data.match(/<meta\s+itemprop="name"\s*content="(.+?)"\s*>\s*\n/))
      map.title = unescHtmlEntities(RegExp.$1);
    if(map.title == null && data.match(/<meta\s+name="title"\s*content="(.+?)"\s*>/))
      map.title = unescHtmlEntities(RegExp.$1);
    var titleStream;
    if(map.title == null && data.match(/"videoDetails":{(.*?)}[,}]/))
      titleStream = RegExp.$1;
    else
      titleStream = data;
    // Edge replaces & with \u0026
    if(map.title == null && titleStream.match(/[,{]"title":("[^"]+")[,}]/))
      map.title = unescHtmlEntities(JSON.parse(RegExp.$1));
    // Edge fails the previous regex if \" exists
    if(map.title == null && titleStream.match(/[,{]"title":(".*?")[,}]"/))
      map.title = unescHtmlEntities(JSON.parse(RegExp.$1));
    if(data.match(/[,{]\\"isLiveContent\\":\s*true[,}]/))
      map.isLive = true;
    map.fmtUrlList = [];
    var oldFmtFlag;
    var newFmtFlag;
    if(data.match(/[,{]"url_encoded_fmt_stream_map":\s?"([^"]+)"[,}]/)) {
      parseStreamMap(map, RegExp.$1);
      oldFmtFlag = true;
      }
    map.fmtMap = {};
    if(data.match(/[,{]"adaptive_fmts":\s?"(.+?)"[,}]/)) {
      parseAdaptiveStreamMap(map, RegExp.$1);
      oldFmtFlag = true;
      }
    if(data.match(/[,{]"fmt_list":\s?"([^"]+)"[,}]/))
      parseFmtList(map, RegExp.$1);
    // Is part of 'player_response' and is escaped
    if(!oldFmtFlag && data.match(/\\"formats\\":(\[{[^\]]*}\])[},]/)) {
      parseNewFormatsMap(map, RegExp.$1, /*unescSlash*/ true);
      newFmtFlag = true;
      }
    if(!oldFmtFlag && data.match(/\\"adaptiveFormats\\":(\[{[^\]]*}\])[},]/)) {
      parseNewFormatsMap(map, RegExp.$1, /*unescSlash*/ true);
      newFmtFlag = true;
      }
    // Is part of 'ytInitialPlayerResponse' and is not escaped
    if(!oldFmtFlag && !newFmtFlag) {
      if(data.match(/[,{]"formats":(\[{[^\]]*}\])[},]/))
        parseNewFormatsMap(map, RegExp.$1);
      if(data.match(/[,{]"adaptiveFormats":(\[{[^\]]*}\])[},]/))
        parseNewFormatsMap(map, RegExp.$1);
      }
    if(data.match(/[,{]"dashmpd":\s?"(.+?)"[,}]/))
      map.dashmpd = decodeURIComponent(RegExp.$1.replace(/\\\//g, "/"));
    else if(data.match(/[,{]\\"dashManifestUrl\\":\s?\\"(.+?)\\"[,}]/))
      map.dashmpd = decodeURIComponent(RegExp.$1.replace(/\\\//g, "/"));
    if(userConfig.filteredFormats.length > 0) {
      for(var i = 0; i < map.fmtUrlList.length; ++i) {
        if(inArray(getVideoNameByType(map.fmtUrlList[i]), userConfig.filteredFormats) >= 0) {
          map.fmtUrlList.splice(i, /*len*/ 1);
          --i;
          continue;
          }
        }
      }
    var hasHd = false;
    var hasHighRes = false;
    var hasUltraHighRes = false;
    var hasHighAudio = false;
    var HIGH_AUDIO_BPS = 96 * 1024;
    forEach(map.fmtUrlList, function(idx, elm) {
      hasHd |= elm.quality == "hd720" || elm.quality == "hd1080";
      hasHighRes |= elm.quality == "hd2k" || elm.quality == "highres";
      hasUltraHighRes |= elm.quality == "ultrahighres";
      if(elm.quality == "audio")
        hasHighAudio |= elm.bitrate >= HIGH_AUDIO_BPS;
      });
    var excludeFmts = [];
    if(hasHd) excludeFmts.push("small");
    if(hasHighRes) excludeFmts.push("medium");
    if(hasUltraHighRes) excludeFmts.push("large");
    if(excludeFmts.length > 0) {
      for(var i = 0; i < map.fmtUrlList.length; ++i) {
        if(inArray(getVideoNameByType(map.fmtUrlList[i]), userConfig.keepFormats) >= 0)
          continue;
        if(excludeFmts.indexOf(map.fmtUrlList[i].quality) >= 0) {
          map.fmtUrlList.splice(i, /*len*/ 1);
          --i;
          continue;
          }
        }
      }
    if(hasHighAudio) {
      for(var i = 0; i < map.fmtUrlList.length; ++i) {
        if(inArray(getVideoNameByType(map.fmtUrlList[i]), userConfig.keepFormats) >= 0)
          continue;
        if(map.fmtUrlList[i].quality == "audio" && map.fmtUrlList[i].bitrate < HIGH_AUDIO_BPS) {
          map.fmtUrlList.splice(i, /*len*/ 1);
          --i;
          continue;
          }
        }
      }
    if(userConfig.filterDrc) {
      for(var i = 0; i < map.fmtUrlList.length; ++i) {
        if(map.fmtUrlList[i].quality == "audio" && map.fmtUrlList[i].drc) {
          map.fmtUrlList.splice(i, /*len*/ 1);
          --i;
          continue;
          }
        }
    }
    map.fmtUrlList.sort(cmpUrlList);
    callback(map);
    }
  // Entry point
  dom.ajax({ url: url, success: success });
  }
function cmpUrlList(a, b) {
  var diff = getQualityIdx(b.quality) - getQualityIdx(a.quality);
  if(diff != 0)
    return diff;
  var aRes = (a.size || "").match(/^(\d+)x(\d+)/);
  var bRes = (b.size || "").match(/^(\d+)x(\d+)/);
  if(aRes == null) aRes = [ 0, 0, 0 ];
  if(bRes == null) bRes = [ 0, 0, 0 ];
  diff = +bRes[2] - +aRes[2];
  if(diff != 0)
    return diff;
  var aFps = a.fps || 0;
  var bFps = b.fps || 0;
  return bFps - aFps;
  }
// -----------------------------------------------------------------------------
var CSS_PREFIX = "ujs-";
var HDR_LINKS_HTML_ID = CSS_PREFIX + "hdr-links-div";
var LINKS_HTML_ID = CSS_PREFIX + "links-cls";
var LINKS_TP_HTML_ID = CSS_PREFIX + "links-tp-div";
var UPDATE_HTML_ID = CSS_PREFIX + "update-div";
var VID_FMT_BTN_ID = CSS_PREFIX + "vid-fmt-btn";
/* The !important attr is to override the page's specificity. */
var CSS_STYLES =
  "#" + VID_FMT_BTN_ID + dom.emitCssStyles({
    "cursor": "pointer",
    "margin": "0 0.333em",
    "padding": "0.5em"
    }) + "\n" +
  "#" + UPDATE_HTML_ID + dom.emitCssStyles({
    "background-color": "#f00",
    "border-radius": "2px",
    "color": "#fff",
    "padding": "5px",
    "text-align": "center",
    "text-decoration": "none",
    "position": "fixed",
    "top": "0.5em",
    "right": "0.5em",
    "z-index": "1000"
    }) + "\n" +
  "#" + UPDATE_HTML_ID + ":hover" + dom.emitCssStyles({
    "background-color": "#0d0"
    }) + "\n" +
  "#page-container #" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
    "font-size": "90%"
    }) + "\n" +
  "#page-manager #" + HDR_LINKS_HTML_ID + dom.emitCssStyles({ // 2017 Material Design
    "font-size": "1.2em"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
    "background-color": "#f8f8f8",
    "border": "#eee 1px solid",
    //"border-radius": "3px",
    "color": "#333",
    "margin": "5px",
    "padding": "5px"
    }) + "\n" +
  "html[dark] #" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
    "background-color": "#222",
    "border": "none"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " ." + CSS_PREFIX + "group" + dom.emitCssStyles({
    "background-color": "#fff",
    "color": "#000 !important",
    "border": "#ccc 1px solid",
    "border-radius": "3px",
    "display": "inline-block",
    "margin": "3px",
    }) + "\n" +
  "html[dark] #" + HDR_LINKS_HTML_ID + " ." + CSS_PREFIX + "group" + dom.emitCssStyles({
    "background-color": "#444",
    "color": "#fff !important",
    "border": "none"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " a" + dom.emitCssStyles({
    "display": "table-cell",
    "padding": "3px",
    "text-decoration": "none"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " a:hover" + dom.emitCssStyles({
    "background-color": "#d1e1fa"
    }) + "\n" +
  "div." + LINKS_HTML_ID + dom.emitCssStyles({
    "border-radius": "3px",
    "cursor": "default",
    "line-height": "1em",
    "position": "absolute",
    "left": "0",
    "top": "0",
    "z-index": "1000"
    }) + "\n" +
  "#page-manager div." + LINKS_HTML_ID + dom.emitCssStyles({ // 2017 Material Design
    "font-size": "1.2em",
    "padding": "2px 4px"
    }) + "\n" +
  "div." + LINKS_HTML_ID + ".layout2017" + dom.emitCssStyles({ // 2017 Material Design
    "font-size": "1.2em"
    }) + "\n" +
  "#" + LINKS_TP_HTML_ID + dom.emitCssStyles({
    "background-color": "#f0f0f0",
    "border": "#aaa 1px solid",
    "padding": "3px 0",
    "text-decoration": "none",
    "white-space": "nowrap",
    "z-index": "1100"
    }) + "\n" +
  "html[dark] #" + LINKS_TP_HTML_ID + dom.emitCssStyles({
    "background-color": "#222"
    }) + "\n" +
  "div." + LINKS_HTML_ID + " a" + dom.emitCssStyles({
    "display": "inline-block",
    "margin": "1px",
    "text-decoration": "none"
    }) + "\n" +
  "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "video" + dom.emitCssStyles({
    "display": "inline-block",
    "text-align": "center",
    "width": "3.5em"
    }) + "\n" +
  "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "quality" + dom.emitCssStyles({
    "display": "inline-block",
    "text-align": "center",
    "width": "5.5em"
    }) + "\n" +
  "." + CSS_PREFIX + "video" + dom.emitCssStyles({
    "color": "#fff !important",
    "padding": "1px 3px",
    "text-align": "center"
    }) + "\n" +
  "." + CSS_PREFIX + "quality" + dom.emitCssStyles({
    "color": "#000 !important",
    "display": "table-cell",
    "min-width": "1.5em",
    "padding": "1px 3px",
    "text-align": "center",
    "vertical-align": "middle"
    }) + "\n" +
  "html[dark] ." + CSS_PREFIX + "quality" + dom.emitCssStyles({
    "color": "#fff !important"
    }) + "\n" +
  "." + CSS_PREFIX + "filesize" + dom.emitCssStyles({
    "font-size": "90%",
    "margin-top": "2px",
    "padding": "1px 3px",
    "text-align": "center"
    }) + "\n" +
  "html[dark] ." + CSS_PREFIX + "filesize" + dom.emitCssStyles({
    "color": "#999"
    }) + "\n" +
  "." + CSS_PREFIX + "filesize-err" + dom.emitCssStyles({
    "color": "#f00",
    "font-size": "90%",
    "margin-top": "2px",
    "padding": "1px 3px",
    "text-align": "center"
    }) + "\n" +
  "." + CSS_PREFIX + "not-avail" + dom.emitCssStyles({
    "background-color": "#700",
    "color": "#fff",
    "padding": "3px",
    }) + "\n" +
  "." + CSS_PREFIX + "3gp" + dom.emitCssStyles({
    "background-color": "#bbb"
    }) + "\n" +
  "." + CSS_PREFIX + "av1" + dom.emitCssStyles({
    "background-color": "#f5f"
    }) + "\n" +
  "." + CSS_PREFIX + "flv" + dom.emitCssStyles({
    "background-color": "#0dd"
    }) + "\n" +
  "." + CSS_PREFIX + "m4a" + dom.emitCssStyles({
    "background-color": "#07e"
    }) + "\n" +
  "." + CSS_PREFIX + "m4v" + dom.emitCssStyles({
    "background-color": "#07e"
    }) + "\n" +
  "." + CSS_PREFIX + "mp3" + dom.emitCssStyles({
    "background-color": "#7ba"
    }) + "\n" +
  "." + CSS_PREFIX + "mp4" + dom.emitCssStyles({
    "background-color": "#777"
    }) + "\n" +
  "." + CSS_PREFIX + "opus" + dom.emitCssStyles({
    "background-color": "#e0e"
    }) + "\n" +
  "." + CSS_PREFIX + "qt" + dom.emitCssStyles({
    "background-color": "#f08"
    }) + "\n" +
  "." + CSS_PREFIX + "vor" + dom.emitCssStyles({
    "background-color": "#e0e"
    }) + "\n" +
  "." + CSS_PREFIX + "vp9" + dom.emitCssStyles({
    "background-color": "#e0e"
    }) + "\n" +
  "." + CSS_PREFIX + "webm" + dom.emitCssStyles({
    "background-color": "#d4d"
    }) + "\n" +
  "." + CSS_PREFIX + "wmv" + dom.emitCssStyles({
    "background-color": "#c75"
    }) + "\n" +
  "." + CSS_PREFIX + "small" + dom.emitCssStyles({
    "color": "#888 !important",
    }) + "\n" +
  "." + CSS_PREFIX + "medium" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#0d0"
    }) + "\n" +
  "." + CSS_PREFIX + "large" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#00d",
    "background-image": "linear-gradient(to right, #00d, #00a)"
    }) + "\n" +
  "." + CSS_PREFIX + "hd720" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#f90",
    "background-image": "linear-gradient(to right, #f90, #d70)"
    }) + "\n" +
  "." + CSS_PREFIX + "hd1080" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#f00",
    "background-image": "linear-gradient(to right, #f00, #c00)"
    }) + "\n" +
  "." + CSS_PREFIX + "hd2k" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#f55",
    "background-image": "linear-gradient(to right, #f55, #c55)"
    }) + "\n" +
  "." + CSS_PREFIX + "highres" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#c0f",
    "background-image": "linear-gradient(to right, #c0f, #90f)"
    }) + "\n" +
  "." + CSS_PREFIX + "ultrahighres" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#ffe42b",
    "background-image": "linear-gradient(to right, #ffe42b, #dfb200)"
    }) + "\n" +
  "." + CSS_PREFIX + "pos-rel" + dom.emitCssStyles({
    "position": "relative"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " a.flash:hover" + dom.emitCssStyles({
    "background-color": "#ffa",
    "transition": "background-color 0.25s linear"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " a.flash-out:hover" + dom.emitCssStyles({
    "transition": "background-color 0.25s linear"
    }) + "\n" +
  "div." + LINKS_HTML_ID + " a.flash div" + dom.emitCssStyles({
    "background-color": "#ffa",
    "transition": "background-color 0.25s linear"
    }) + "\n" +
  "div." + LINKS_HTML_ID + " a.flash-out div" + dom.emitCssStyles({
    "transition": "background-color 0.25s linear"
    }) + "\n" +
  "";
function condInsertHdr(divId) {
  if(dom.gE(HDR_LINKS_HTML_ID))
    return true;
  var insertPtNode = dom.gE(divId);
  if(!insertPtNode)
    return false;
  var divNode = dom.cE("div");
  divNode.id = HDR_LINKS_HTML_ID;
  insertPtNode.parentNode.insertBefore(divNode, insertPtNode);
  return true;
  }
function condRemoveHdr() {
  var node = dom.gE(HDR_LINKS_HTML_ID);
  if(node)
    node.parentNode.removeChild(node);
  }
function condInsertTooltip() {
  if(dom.gE(LINKS_TP_HTML_ID))
    return true;
  var toolTipNode = dom.cE("div");
  toolTipNode.id = LINKS_TP_HTML_ID;
  var cls = [ LINKS_HTML_ID ];
  if(dom.gE("page-manager"))
    cls.push("layout2017");
  dom.attr(toolTipNode, "class", cls.join(" "));
  dom.attr(toolTipNode, "style", "display: none;");
  dom.append(doc.body, toolTipNode);
  dom.addEvent(toolTipNode, "mouseleave", function(evt) {
    //logMsg("mouse leave");
    dom.attr(toolTipNode, "style", "display: none;");
    stopChkMouseInPopup();
    });
  }
function condInsertUpdateIcon() {
  if(dom.gE(UPDATE_HTML_ID))
    return;
  var divNode = dom.cE("a");
  divNode.id = UPDATE_HTML_ID;
  dom.append(doc.body, divNode);
  }
// -----------------------------------------------------------------------------
var STORE_ID = "ujsYtLinks";
var JSONP_ID = "ujsYtLinks";
// User settings can be saved in localStorage. Refer to documentation for details.
var userConfig = {
  copyToClipboard: true,
  filterDrc: true,
  filteredFormats: [],
  keepFormats: [],
  showVideoFormats: true,
  showVideoSize: true,
  tagLinks: true,
  useDecUnits: true
  };
var videoInfoCache = {};
var TAG_LINK_NUM_PER_BATCH = 5;
var INI_TAG_LINK_DELAY_MS = 200;
var SUB_TAG_LINK_DELAY_MS = 350;
// -----------------------------------------------------------------------------
var FULL_AR_CUTOFF = 1.5;
var WIDE_AR_CUTOFF = 2.0;
var ULTRA_WIDE_AR_CUTOFF = 2.3;
var HFR_CUTOFF = 45;
var fmtSizeSuffix = [ " kB", " MB", " GB" ];
var fmtSizeUnit = 1000;
function Links() {
  }
Links.prototype.init = function() {
  for(var k in userConfig) {
    try {
      var v = localStorage.getItem(STORE_ID + ".cfg." + k);
      if(v != null)
        userConfig[k] = JSON.parse(v);
    } catch(e) {
      logMsg(k + ": unable to parse '" + v + "'");
      }
    }
  };
Links.prototype.getPreferredFmt = function(map) {
  var selElm = map.fmtUrlList[0];
  forEach(map.fmtUrlList, function(idx, elm) {
    if(getVideoName(elm.type).toLowerCase() != "webm") {
      selElm = elm;
      return false;
      }
    });
  return selElm;
  };
Links.prototype.parseDashManifest = function(map, callback) {
  function parse(xml) {
    //logMsg(xml);
    var dashList = [];
    var adaptationSetDom = xml.getElementsByTagName("AdaptationSet");
    //logMsg(adaptationSetDom);
    forEach(adaptationSetDom, function(i, adaptationElm) {
      var mimeType = adaptationElm.getAttribute("mimeType");
      //logMsg(i + " " + mimeType);
      var representationDom = adaptationElm.getElementsByTagName("Representation");
      forEach(representationDom, function(j, repElm) {
        var dashElm = { mimeType: mimeType };
        forEach([ "codecs" ], function(idx, elm) {
          var v = repElm.getAttribute(elm);
          if(v != null)
            dashElm[elm] = v;
          });
        forEach([ "audioSamplingRate", "bandwidth", "frameRate", "height", "id", "width" ], function(idx, elm) {
          var v = repElm.getAttribute(elm);
          if(v != null)
            dashElm[elm] = +v;
          });
        var baseUrlDom = repElm.getElementsByTagName("BaseURL");
        dashElm.len = +baseUrlDom[0].getAttribute("yt:contentLength");
        dashElm.url = baseUrlDom[0].textContent;
        var segList = repElm.getElementsByTagName("SegmentList");
        if(segList.length > 0)
          dashElm.numSegments = segList[0].childNodes.length;
        dashList.push(dashElm);
        });
      });
    //logMsg(map);
    //logMsg(dashList);
    var maxBitRateMap = {};
    forEach(dashList, function(idx, dashElm) {
      if(dashElm.mimeType != "video/mp4" && dashElm.mimeType != "video/webm")
        return;
      var id = [ dashElm.mimeType, dashElm.width, dashElm.height, dashElm.frameRate ].join("|");
      if(maxBitRateMap[id] == null || maxBitRateMap[id] < dashElm.bandwidth)
        maxBitRateMap[id] = dashElm.bandwidth;
      });
    forEach(dashList, function(idx, dashElm) {
      var foundIdx;
      forEach(map.fmtUrlList, function(idx, mapElm) {
        if(dashElm.id == mapElm.itag) {
          foundIdx = idx;
          return false;
          }
        });
      if(foundIdx != null) {
        if(dashElm.numSegments != null)
          map.fmtUrlList[foundIdx].numSegments = dashElm.numSegments;
        return;
        }
      //logMsg(dashElm);
      if((dashElm.mimeType == "video/mp4" || dashElm.mimeType == "video/webm") && (dashElm.width >= 1000 || dashElm.height >= 1000)) {
        var id = [ dashElm.mimeType, dashElm.width, dashElm.height, dashElm.frameRate ].join("|");
        if(maxBitRateMap[id] == null || dashElm.bandwidth < maxBitRateMap[id])
          return;
        var size = dashElm.width + "x" + dashElm.height;
        if(map.fmtMap[dashElm.id] == null)
          map.fmtMap[dashElm.id] = { res: cnvResName(size) };
        map.fmtUrlList.push({
          bitrate: dashElm.bandwidth,
          effType: dashElm.mimeType == "video/mp4" ? "video/x-m4v" : null,
          filesize: dashElm.len,
          fps: dashElm.frameRate,
          itag: dashElm.id,
          quality: mapResToQuality(size),
          size: size,
          type: dashElm.mimeType + ";+codecs=\"" + dashElm.codecs + "\"",
          url: dashElm.url,
          numSegments: dashElm.numSegments
          });
        }
      else if(dashElm.mimeType == "audio/mp4" && dashElm.audioSamplingRate >= 44100) {
        if(map.fmtMap[dashElm.id] == null) {
          map.fmtMap[dashElm.id] = { res: "Audio" };
          }
        map.fmtUrlList.push({
          bitrate: dashElm.bandwidth,
          filesize: dashElm.len,
          itag: dashElm.id,
          quality: "audio",
          type: dashElm.mimeType + ";+codecs=\"" + dashElm.codecs + "\"",
          url: dashElm.url
          });
        }
      });
    if(condInsertHdr(me.getInsertPt()))
      me.createLinks(dom.gE(HDR_LINKS_HTML_ID), map);
    }
  // Entry point
  var me = this;
  if(!map.dashmpd) {
    setTimeout(callback, 0);
    return;
    }
  //logMsg(map.dashmpd);
  if(map.dashmpd.match(/\/s\/([a-zA-Z0-9.]+)\//)) {
    var sig = deobfuscateVideoSig(map.scriptName, RegExp.$1);
    map.dashmpd = map.dashmpd.replace(/\/s\/[a-zA-Z0-9.]+\//, "/sig/" + sig + "/");
    }
  dom.crossAjax({
    url: map.dashmpd,
    dataType: "xml",
    success: function(data, status, xhr) {
      parse(data);
      callback();
      },
    error: function(xhr, status) {
      callback();
      },
    complete: function(xhr) {
      }
    });
  };
Links.prototype.checkFmts = function(forceFlag) {
  var me = this;
  if(!userConfig.showVideoFormats)
    return;
  if(!forceFlag && userConfig.showVideoFormats == "btn") {
    condRemoveHdr();
    if(dom.gE(VID_FMT_BTN_ID))
      return;
    // 'container' is for Material Design
    var mastH = dom.gE("yt-masthead-signin") || dom.gE("yt-masthead-user") || dom.gE("end") || dom.gE("container");
    if(!mastH)
      return;
    var btn = dom.cE("button");
    dom.attr(btn, "id", VID_FMT_BTN_ID);
    dom.attr(btn, "class", "yt-uix-button yt-uix-button-default");
    btn.innerHTML = "VidFmts";
    dom.prepend(mastH, btn);
    dom.addEvent(btn, "click", function(evt) {
      me.checkFmts(/*force*/ true);
      });
    return;
    }
  if(!loc.href.match(/watch\?(?:.+&)?v=([a-zA-Z0-9_-]+)/))
    return false;
  var videoId = RegExp.$1;
  var url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;
  var curVideoUrl = loc.toString();
  getVideoInfo(url, function(map) {
    me.parseDashManifest(map, function() {
      // Has become stale (eg switch forward/back pages quickly)
      if(curVideoUrl != loc.toString())
        return;
      me.showLinks(me.getInsertPt(), map);
      });
    });
  };
Links.prototype.genUrl = function(map, elm) {
  var url = elm.url + "&title=" + encodeSafeFname(map.title);
  if(elm.sig != null)
    url += "&sig=" + elm.sig;
  return url;
  };
Links.prototype.emitLinks = function(map) {
  function fmtSize(size, units, divisor) {
    if(!units) {
      units = fmtSizeSuffix;
      divisor = fmtSizeUnit;
      }
    for(var idx = 0; idx < units.length; ++idx) {
      size /= divisor;
      if(size < 10)
        return Math.round(size * 100) / 100 + units[idx];
      if(size < 100)
        return Math.round(size * 10) / 10 + units[idx];
      if(size < 1000 || idx == units.length - 1)
        return Math.round(size) + units[idx];
      }
    }
  function fmtBitrate(size) {
    return fmtSize(size, [ " kbps", " Mbps", " Gbps" ], 1000);
    }
  function getFileExt(videoName, elm) {
    if(videoName == "VP9")
      return "video.webm";
    if(videoName == "VOR")
      return "audio.webm";
    return videoName.toLowerCase();
    }
  // Entry point
  var me = this;
  var s = [];
  var resMap = {};
  map.fmtUrlList.sort(cmpUrlList);
  forEach(map.fmtUrlList, function(idx, elm) {
    var fmtMap = map.fmtMap[elm.itag];
    if(!resMap[fmtMap.res]) {
      resMap[fmtMap.res] = [];
      resMap[fmtMap.res].quality = elm.quality;
      }
    resMap[fmtMap.res].push(elm);
    });
  for(var res in resMap) {
    var qFields = [];
    qFields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "quality " + CSS_PREFIX + resMap[res].quality }, res));
    forEach(resMap[res], function(idx, elm) {
      var fields = [];
      var fmtMap = map.fmtMap[elm.itag];
      var videoName = getVideoName(elm.effType || elm.type);
      var addMsg = [ elm.itag, elm.type, elm.size || elm.quality ];
      if(elm.fps != null)
        addMsg.push(elm.fps + " fps");
      var varMsg = "";
      if(elm.bitrate != null)
        varMsg = fmtBitrate(elm.bitrate);
      else if(fmtMap.vars != null)
        varMsg = fmtMap.vars.join();
      addMsg.push(varMsg);
      if(elm.s != null)
        addMsg.push("sig-" + elm.s.length);
      if(elm.filesize != null && elm.filesize >= 0)
        addMsg.push(fmtSize(elm.filesize));
      var vidSuffix = "";
      if(inArray(elm.itag, [ 82, 83, 84, 100, 101, 102 ]) >= 0)
        vidSuffix = " (3D)";
      else if(elm.fps != null && elm.fps >= HFR_CUTOFF)
        vidSuffix = " (HFR)";
      else if(elm.drc)
        vidSuffix = " (DRC)";
      fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "video " + CSS_PREFIX + videoName.toLowerCase() }, videoName + vidSuffix));
      if(elm.filesize != null) {
        var filesize = elm.filesize;
        if((map.isLive || (elm.numSegments || 1) > 1) && filesize == 0)
          filesize = -1;
        if(filesize >= 0) {
          fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize" }, fmtSize(filesize)));
          }
        else {
          var msg;
          if(elm.isDrm)
            msg = "DRM";
          else if(elm.s != null)
            msg = "sig-" + elm.s.length;
          else if(elm.numSegments > 1)
            msg = "Frag";
          else if(map.isLive)
            msg = "Live";
          else
            msg = "Err";
          fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize-err" }, msg));
          }
        }
      var url;
      if(elm.isDrm)
        url = elm.conn + "?" + elm.stream;
      else
        url = me.genUrl(map, elm);
      var fname = cnvSafeFname(map.title);
      var ext = getFileExt(videoName, elm);
      if(ext)
        fname += "." + ext;
      var ahref = dom.emitHtml("a", {
        download: fname,
        ext: ext,
        href: url,
        res: res,
        title: addMsg.join(" | ")
        }, fields.join(""));
      qFields.push(ahref);
      });
    s.push(dom.emitHtml("div", { "class": CSS_PREFIX + "group" }, qFields.join("")));
    }
  return s.join("");
  };
Links.prototype.createLinks = function(insertNode, map) {
  function copyToClipboard(text) {
    var node = dom.cE("textarea");
    // Needed to prevent scrolling to top of page
    node.style.position = "fixed";
    node.value = text;
    dom.append(document.body, node);
    node.focus();
    node.select();
    var ret = false;
    try {
      if(document.execCommand("copy"))
        ret = true;
      } catch(e) {
        }
    document.body.removeChild(node);
    return ret;
    }
  function addCopyHandler(node) {
    forEach(dom.gT(node, "a"), function(idx, elm) {
      dom.addEvent(elm, "click", function(evt) {
        var me = this;
        var ext = dom.attr(me, "ext");
        var res = dom.attr(me, "res") || "";
        // This is the only video that can be downloaded directly
        if(ext == "mp4" && res.match(/^[a-z]?720[a-z]$/))
          return;
        evt.preventDefault();
        var fname = dom.attr(me, "download");
        //logMsg(fname);
        copyToClipboard(fname);
        var orgCls = dom.attr(me, "class") || "";
        dom.attr(me, "class", orgCls + " flash");
        setTimeout(function() { dom.attr(me, "class", orgCls + " flash-out"); }, 250);
        setTimeout(function() { dom.attr(me, "class", orgCls); }, 500);
        });
      });
    }
  // Entry point
  var me = this;
  if(insertNode == null)
    return;
  /* Emit to tmp node first because in GM 4, <a> event does not fire on nodes
   already in the DOM. */
  var stgNode = dom.cE("div");
  dom.html(stgNode, me.emitLinks(map));
  if(userConfig.copyToClipboard)
    addCopyHandler(stgNode);
  dom.html(insertNode, "");
  while(stgNode.childNodes.length > 0)
    insertNode.appendChild(stgNode.firstChild);
  };
var INI_SHOW_FILESIZE_DELAY_MS = 500;
var SUB_SHOW_FILESIZE_DELAY_MS = 150;
var PERIODIC_TAG_LINK_DELAY_MS = 3000;
Links.prototype.showLinks = function(divId, map) {
  function updateLinks() {
    // Has become stale (eg switch forward/back pages quickly)
    if(curVideoUrl != loc.toString())
      return;
    //!! Hack to update file size
    var node = dom.gE(HDR_LINKS_HTML_ID);
    if(node)
      me.createLinks(node, map);
    }
  // Entry point
  var me = this;
  // video is not avail
  if(!map.fmtUrlList)
    return;
  //logMsg(JSON.stringify(map));
  if(!condInsertHdr(divId))
    return;
  me.createLinks(dom.gE(HDR_LINKS_HTML_ID), map);
  if(!userConfig.showVideoSize)
    return;
  var curVideoUrl = loc.toString();
  forEach(map.fmtUrlList, function(idx, elm) {
    //logMsg(elm.itag + " " + elm.url);
    // We just fail outright for protected/obfuscated videos
    if(elm.isDrm || elm.s != null) {
      elm.filesize = -1;
      updateLinks();
      return;
      }
    if(elm.clen != null) {
      elm.filesize = elm.clen;
      updateLinks();
      return;
      }
    setTimeout(function() {
      // Has become stale (eg switch forward/back pages quickly)
      if(curVideoUrl != loc.toString())
        return;
      dom.crossAjax({
        type: "HEAD",
        url: me.genUrl(map, elm),
        success: function(data, status, xhr) {
          var filesize = xhr.getResponseHeader("Content-Length");
          if(filesize == null)
            return;
          //logMsg(map.title + " " + elm.itag + ": " + filesize);
          elm.filesize = +filesize;
          updateLinks();
          },
        error: function(xhr, status) {
          //logMsg(map.fmtMap[elm.itag].res + " " + getVideoName(elm.type) + ": " + xhr.status);
          if(xhr.status != 403 && xhr.status != 404)
            return;
          elm.filesize = -1;
          updateLinks();
          },
        complete: function(xhr) {
          //logMsg(map.title + ": " + xhr.getAllResponseHeaders());
          }
        });
       }, INI_SHOW_FILESIZE_DELAY_MS + idx * SUB_SHOW_FILESIZE_DELAY_MS);
    });
  };
Links.prototype.tagLinks = function() {
  var SCANNED = 1;
  var REQ_INFO = 2;
  var ADDED_INFO = 3;
  function prepareTagHtml(node, map) {
    var elm = me.getPreferredFmt(map);
    var fmtMap = map.fmtMap[elm.itag];
    dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "quality " + CSS_PREFIX + elm.quality);
    var label = fmtMap.res;
    if(elm.fps >= HFR_CUTOFF)
      label += elm.fps;
    var tagEvent;
    if(userConfig.tagLinks == "label")
      tagEvent = "click";
    else
      tagEvent = "mouseenter";
    dom.addEvent(node, tagEvent, function(evt) {
      //logMsg("mouse enter " + map.videoId);
      var pos = dom.offset(node);
      //logMsg("mouse enter: x " + pos.left + ", y " + pos.top);
      var toolTipNode = dom.gE(LINKS_TP_HTML_ID);
      dom.attr(toolTipNode, "style", "position: absolute; left: " + pos.left + "px; top: " + pos.top + "px");
      me.createLinks(toolTipNode, map);
      startChkMouseInPopup();
      });
    return label;
    }
  function addTag(hNode, map) {
    //logMsg(dom.html(hNode));
    //logMsg("hNode " + dom.attr(hNode, "class"));
    //var img = dom.gT(hNode, "img") [0];
    //logMsg(dom.attr(img, "src"));
    //logMsg(dom.attr(img, "class"));
    dom.attr(hNode, CSS_PREFIX + "processed", ADDED_INFO);
    var node = dom.cE("div");
    if(map.fmtUrlList && map.fmtUrlList.length > 0) {
      tagHtml = prepareTagHtml(node, map);
      }
    else {
      dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "not-avail");
      tagHtml = "NA";
      }
    var parentNode;
    var insNode;
    var cls = dom.attr(hNode, "class") || "";
    var isVideoWallStill = cls.match(/videowall-still/);
    if(isVideoWallStill) {
      parentNode = hNode;
      insNode = hNode.firstChild;
      }
    else {
      parentNode = hNode.parentNode;
      insNode = hNode;
      }
    // Remove existing tags
    var divNodes = parentNode.getElementsByTagName("div");
    for(var i = 0; i < divNodes.length; ++i) {
      var hNode = divNodes[i];
      if(me.isTagDiv(hNode))
        hNode.parentNode.removeChild(hNode);
      else
        ++i;
      }
    var parentCssPositionStyle = window.getComputedStyle(parentNode, null).getPropertyValue("position");
    if(parentCssPositionStyle != "absolute" && parentCssPositionStyle != "relative")
      dom.attr(parentNode, "class", dom.attr(parentNode, "class") + " " + CSS_PREFIX + "pos-rel");
    parentNode.insertBefore(node, insNode);
    dom.html(node, tagHtml);
    }
  function getFmt(videoId, hNode) {
    if(videoInfoCache[videoId]) {
      addTag(hNode, videoInfoCache[videoId]);
      return;
      }
    var url;
    if(videoId.match(/.+==$/))
      url = loc.protocol + "//" + loc.host + "/cthru?key=" + videoId;
    else
      url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;
    getVideoInfo(url, function(map) {
      videoInfoCache[videoId] = map;
      addTag(hNode, map);
      });
    }
  // Entry point
  var me = this;
  var list = [];
  forEach(dom.gT("a"), function(idx, hNode) {
    var href = dom.attr(hNode, "href") || "";
    if(!href.match(/watch\?v=([a-zA-Z0-9_-]+)/) &&
     !href.match(/watch_videos.+?&video_ids=([a-zA-Z0-9_-]+)/))
      return;
    var videoId = RegExp.$1;
    var oldHref = dom.attr(hNode, CSS_PREFIX + "href");
    if(href == oldHref && dom.attr(hNode, CSS_PREFIX + "processed"))
      return;
    if(!dom.inViewport(hNode))
      return;
    dom.attr(hNode, CSS_PREFIX + "processed", SCANNED);
    dom.attr(hNode, CSS_PREFIX + "href", href);
    var cls = dom.attr(hNode, "class") || "";
    if(!cls.match(/videowall-still/)) {
      if(cls == "yt-button" || cls.match(/yt-uix-button/))
        return;
      // Material Design
      if(cls.match(/ytd-playlist-(panel-)?video-renderer/))
        return;
      if(dom.attr(hNode.parentNode, "class") == "video-time")
        return;
      if(dom.html(hNode).match(/video-logo/i))
        return;
      var img = dom.gT(hNode, "img");
      if(img == null || img.length == 0)
        return;
      img = img[0];
      // /yts/img/pixel-*.gif is the placeholder image
      // can be null as well
      var imgSrc = dom.attr(img, "src") || "";
      if(imgSrc.indexOf("ytimg.com") < 0 && !imgSrc.match(/^\/yts\/img\/.*\.gif$/) && imgSrc != "")
        return;
      var tnSrc = dom.attr(img, "thumb") || "";
      if(imgSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/(hq)?default\.jpg$/))
        videoId = RegExp.$1;
      else if(tnSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/(hq)?default\.jpg$/))
        videoId = RegExp.$1;
      }
    //logMsg(idx + " " + href);
    //logMsg("videoId: " + videoId);
    list.push({ videoId: videoId, hNode: hNode });
    dom.attr(hNode, CSS_PREFIX + "processed", REQ_INFO);
    });
  forLoop({ num: list.length, inc: TAG_LINK_NUM_PER_BATCH, batchIdx: 0 }, function(idx) {
    var batchIdx = this.batchIdx++;
    var batchList = list.slice(idx, idx + TAG_LINK_NUM_PER_BATCH);
    setTimeout(function() {
      forEach(batchList, function(idx, elm) {
        //logMsg(batchIdx + " " + idx + " " + elm.hNode.href);
        getFmt(elm.videoId, elm.hNode);
        });
      }, INI_TAG_LINK_DELAY_MS + batchIdx * SUB_TAG_LINK_DELAY_MS);
    });
  };
Links.prototype.isTagDiv = function(node) {
  var cls = dom.attr(node, "class") || "";
  return cls.match(new RegExp("(^|\\s+)" + RegExp.escape(LINKS_HTML_ID) + "\\s+" + RegExp.escape(CSS_PREFIX + "quality") + "(\\s+|$)"));
  };
Links.prototype.invalidateTagLinks = function() {
  var me = this;
  if(!userConfig.tagLinks)
    return;
  forEach(dom.gT("a"), function(idx, hNode) {
    hNode.removeAttribute(CSS_PREFIX + "processed");
    });
  var nodes = dom.gT("div");
  for(var i = 0; i < nodes.length; ) {
    var hNode = nodes[i];
    if(me.isTagDiv(hNode))
      hNode.parentNode.removeChild(hNode);
    else
      ++i;
    }
  };
Links.prototype.periodicTagLinks = function(delayMs) {
  function poll() {
    me.tagLinks();
    me.tagLinksTimerId = setTimeout(poll, PERIODIC_TAG_LINK_DELAY_MS);
    }
  // Entry point
  if(!userConfig.tagLinks)
    return;
  var me = this;
  delayMs = delayMs || 0;
  if(me.tagLinksTimerId != null) {
    clearTimeout(me.tagLinksTimerId);
    delete me.tagLinksTimerId;
    }
  setTimeout(poll, delayMs);
  };
Links.prototype.getInsertPt = function() {
  if(dom.gE("page"))
    return "page";
  else if(dom.gE("columns")) // 2017 Material Design
    return "columns";
  else
    return "top";
  };
// -----------------------------------------------------------------------------
Links.prototype.loadSettings = function() {
  var obj = localStorage[STORE_ID];
  if(obj == null)
    return;
  obj = JSON.parse(obj);
  this.lastChkReqTs = +obj.lastChkReqTs;
  this.lastChkTs = +obj.lastChkTs;
  this.lastChkVer = +obj.lastChkVer;
  };
Links.prototype.storeSettings = function() {
  localStorage[STORE_ID] = JSON.stringify({
    lastChkReqTs: this.lastChkReqTs,
    lastChkTs: this.lastChkTs,
    lastChkVer: this.lastChkVer
    });
  };
// -----------------------------------------------------------------------------
var UPDATE_CHK_INTERVAL = 5 * 86400;
var FAIL_TO_CHK_UPDATE_INTERVAL = 14 * 86400;
Links.prototype.chkVer = function(forceFlag) {
  if(this.lastChkVer > relInfo.ver) {
    this.showNewVer({ ver: this.lastChkVer });
    return;
    }
  var now = timeNowInSec();
  //logMsg("lastChkReqTs " + this.lastChkReqTs + ", diff " + (now - this.lastChkReqTs));
  //logMsg("lastChkTs " + this.lastChkTs);
  //logMsg("lastChkVer " + this.lastChkVer);
  if(this.lastChkReqTs == null || now < this.lastChkReqTs) {
    this.lastChkReqTs = now;
    this.storeSettings();
    return;
    }
  if(now - this.lastChkReqTs < UPDATE_CHK_INTERVAL)
    return;
  if(this.lastChkReqTs - this.lastChkTs > FAIL_TO_CHK_UPDATE_INTERVAL)
    logMsg("Failed to check ver for " + ((this.lastChkReqTs - this.lastChkTs) / 86400) + " days");
  this.lastChkReqTs = now;
  this.storeSettings();
  unsafeWin[JSONP_ID] = this;
  var script = dom.cE("script");
  script.type = "text/javascript";
  script.src  = SCRIPT_UPDATE_LINK;
  dom.append(doc.body, script);
  };
Links.prototype.chkVerCallback = function(data) {
  delete unsafeWin[JSONP_ID];
  this.lastChkTs = timeNowInSec();
  this.storeSettings();
  //logMsg(JSON.stringify(data));
  var latestElm = data[0];
  if(latestElm.ver <= relInfo.ver)
    return;
  this.showNewVer(latestElm);
  };
Links.prototype.showNewVer = function(latestElm) {
  function getVerStr(ver) {
    var verStr = "" + ver;
    var majorV = verStr.substr(0, verStr.length - 4) || "0";
    var minorV = verStr.substr(verStr.length - 4, 2);
    return majorV + "." + minorV;
    }
  // Entry point
  this.lastChkVer = latestElm.ver;
  this.storeSettings();
  condInsertUpdateIcon();
  var aNode = dom.gE(UPDATE_HTML_ID);
  aNode.href = SCRIPT_LINK;
  if(latestElm.desc != null)
    dom.attr(aNode, "title", latestElm.desc);
  dom.html(aNode, dom.emitHtml("b", SCRIPT_NAME + " " + getVerStr(relInfo.ver)) +
   "<br>Click to update to " + getVerStr(latestElm.ver));
  };
// -----------------------------------------------------------------------------
var WAIT_FOR_READY_POLL_MS = 300;
var SCROLL_TAG_LINK_DELAY_MS = 200;
var inst;
function waitForReady() {
  function start() {
    inst = new Links();
    inst.init();
    inst.loadSettings();
    decryptSig.load();
    if(!userConfig.useDecUnits) {
      fmtSizeSuffix = [ " KiB", " MiB", " GiB" ];
      fmtSizeUnit = 1024;
      }
    dom.insertCss(CSS_STYLES);
    condInsertTooltip();
    if(loc.pathname.match(/\/watch/))
      inst.checkFmts();
    inst.periodicTagLinks();
    inst.chkVer();
    }
  // Entry point
  // 'columns' is for Material Design
  if(dom.gE("page") || dom.gE("columns") || dom.gE("top")) {
    start();
    return;
    }
  if(!dom.gE("top"))
    setTimeout(waitForReady, WAIT_FOR_READY_POLL_MS);
  }
var scrollTop = win.pageYOffset || doc.documentElement.scrollTop;
dom.addEvent(win, "scroll", function(e) {
  var newScrollTop = win.pageYOffset || doc.documentElement.scrollTop;
  if(Math.abs(newScrollTop - scrollTop) < 100)
    return;
  //logMsg("scroll by " + (newScrollTop - scrollTop));
  scrollTop = newScrollTop;
  if(inst)
    inst.periodicTagLinks(SCROLL_TAG_LINK_DELAY_MS);
  });
// -----------------------------------------------------------------------------
var CHK_MOUSE_IN_POPUP_POLL_MS = 1000;
var curMousePos = {};
var chkMouseInPopupTimer;
function trackMousePos(e) {
  curMousePos.x = e.pageX;
  curMousePos.y = e.pageY;
  }
dom.addEvent(window, "mousemove", trackMousePos);
function chkMouseInPopup() {
  chkMouseInPopupTimer = null;
  var toolTipNode = dom.gE(LINKS_TP_HTML_ID);
  if(!toolTipNode)
    return;
  var pos = dom.offset(toolTipNode);
  var rect = toolTipNode.getBoundingClientRect();
  //logMsg("mouse x " + curMousePos.x + ", y " + curMousePos.y);
  //logMsg("x " + Math.round(pos.left) + ", y " + Math.round(pos.top) + ", wd " + Math.round(rect.width) + ", ht " + Math.round(rect.height));
  if(curMousePos.x < pos.left || curMousePos.x >= pos.left + rect.width ||
   curMousePos.y < pos.top || curMousePos.y >= pos.top + rect.height) {
    dom.attr(toolTipNode, "style", "display: none;");
    return;
    }
  chkMouseInPopupTimer = setTimeout(chkMouseInPopup, CHK_MOUSE_IN_POPUP_POLL_MS);
  }
function startChkMouseInPopup() {
  stopChkMouseInPopup();
  chkMouseInPopupTimer = setTimeout(chkMouseInPopup, CHK_MOUSE_IN_POPUP_POLL_MS);
  }
function stopChkMouseInPopup() {
  if(!chkMouseInPopupTimer)
    return;
  clearTimeout(chkMouseInPopupTimer);
  chkMouseInPopupTimer = null;
  }
// -----------------------------------------------------------------------------
/* YouTube reuses the current page when the user clicks on a new video. We need
 to detect it and reload the formats. */
(function() {
var PERIODIC_CHK_VIDEO_URL_MS = 1000;
var NEW_URL_TAG_LINKS_DELAY_MS = 500;
var curVideoUrl = loc.toString();
function periodicChkVideoUrl() {
  var newVideoUrl = loc.toString();
  if(curVideoUrl != newVideoUrl && inst) {
    //logMsg(curVideoUrl + " -> " + newVideoUrl);
    curVideoUrl = newVideoUrl;
    inst.invalidateTagLinks();
    inst.periodicTagLinks(NEW_URL_TAG_LINKS_DELAY_MS);
    if(loc.pathname.match(/\/watch/))
      inst.checkFmts();
    else
      condRemoveHdr();
    }
  setTimeout(periodicChkVideoUrl, PERIODIC_CHK_VIDEO_URL_MS);
  }
periodicChkVideoUrl();
}) ();
// -----------------------------------------------------------------------------
waitForReady();
}) ();