YouTube Annotation Markers

Marks where annotations are on the progress bar of the HTML5 YouTube player.

  1. // ==UserScript==
  2. // @name YouTube Annotation Markers
  3. // @namespace https://github.com/HatScripts/YouTubeAnnotationMarkers
  4. // @version 1.2.5
  5. // @description Marks where annotations are on the progress bar of the HTML5 YouTube player.
  6. // @author HatScripts
  7. // @match http://*.youtube.com/*
  8. // @match https://*.youtube.com/*
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. function parseTime(s) {
  13. const multipliers = [1, 60, 3600, 86400];
  14. return s
  15. .split(":")
  16. .reverse()
  17. .map(p => parseFloat(p) || 0)
  18. .reduce((prev, curr, i) => prev + curr * multipliers[i]);
  19. }
  20.  
  21. function getVideoAnnotationXml(videoId, success) {
  22. let request = new XMLHttpRequest();
  23. request.open("GET", "annotations_invideo?video_id=" + videoId, true);
  24. request.onload = () => {
  25. if (request.status >= 200 && request.status < 400) {
  26. let xmlString = request.responseText;
  27. let domParser = new DOMParser();
  28. success(domParser.parseFromString(xmlString, "text/xml"));
  29. }
  30. };
  31. request.send();
  32. }
  33.  
  34. function annotationToMarker(annotation) {
  35. const rectRegions = Array.from(
  36. annotation.querySelectorAll("movingRegion *:not([t=never])"));
  37. if (rectRegions.length === 0) {
  38. return false;
  39. }
  40. const start = rectRegions[0].getAttribute("t");
  41. const end = rectRegions[rectRegions.length - 1].getAttribute("t");
  42. const startTime = parseTime(start);
  43. const duration = parseTime(end) - startTime;
  44. const appearance = annotation.querySelector("appearance[bgColor]");
  45. const c = appearance ? parseInt(appearance.getAttribute("bgColor"), 10) : 16777215;
  46. const rgb = [(c & 0xff0000) >> 16, (c & 0x00ff00) >> 8, c & 0x0000ff];
  47. const marker = document.createElement("div");
  48. marker.classList.add("ytp-play-progress");
  49. marker.style.position = "absolute";
  50. marker.style.left = ((startTime / videoDuration) * 100) + "%";
  51. marker.style.bottom = "100%";
  52. marker.style.transform = "scaleX(" + (duration / videoDuration) + ")";
  53. marker.style.background = "rgb(" + rgb + ")";
  54. marker.style.opacity = 0.5;
  55. marker.style.zIndex = Math.round(startTime * 10);
  56. return marker;
  57. }
  58.  
  59. function parseVideoAnnotationXml(xml) {
  60. Array.from(xml.querySelectorAll("annotation"))
  61. .map(annotation => annotationToMarker(annotation))
  62. .filter(annotation => annotation)
  63. .sort((a1, a2) => parseInt(a1.style.zIndex) - parseInt(a2.style.zIndex))
  64. .forEach(annotation => progressList.appendChild(annotation));
  65. }
  66.  
  67. function getVideoId() {
  68. const url = new URL(location.href);
  69. return url.searchParams.get("v") || url.pathname.split("/").pop()
  70. || document.querySelector("#page-manage ytd-watch").getAttribute("video-id");
  71. }
  72.  
  73. const videoId = getVideoId();
  74. const player = document.querySelector("#player:not(.skeleton)");
  75. const progressBar = player.querySelector(".ytp-progress-bar");
  76. const progressList = progressBar.querySelector(".ytp-progress-list");
  77. const videoDuration = parseTime(player.querySelector(".ytp-time-duration").innerText);
  78. getVideoAnnotationXml(videoId, parseVideoAnnotationXml);
  79. })();