YTSpeed+

Provides an improved speed changer for YouTube

  1. // ==UserScript==
  2. // @name YTSpeed+
  3. // @version 1.0.0
  4. // @description Provides an improved speed changer for YouTube
  5. // @author @charlesbobomb (Discord)
  6. // @match http*://www.youtube.com/watch*
  7. // @icon64 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAACiElEQVR4nO3ZvU5UURSG4RdFE+QSUEu9C1sLCy5A6Uy8EtRSwViReAcEC0y0k0ugIwIJBbWNFiQmY8FMmDmcn7X3Xt/aU5yVnIZM8s37ZAgJA+ONN954i7cKHALPg/ZeA5+B+0F7g7cBTIArYDNgb3+695UlQXjM9RuaAP+Al+K9g7m978CaeG/w5gEiEOYBlgKhCaBGaAJUR2gDUCK0AVRF6AJQIXQBVEPoA1Ag9AFUQRgCmCG8ctobAghHsAB4IlgAJsAPghCsAF4IVoAwhBQAD4QUgBCEVIBShFQAOUIOQAlCDoAUIRcgFyEXQIZQApCDUAIgQSgFSEUoBXBH8ABIQfAAcEXwArAieAG4IXgCWBA8AVwQvAGGELwBihEUADOErSCACfATWF8mgC4EFUA2ghKgDUEJkIWgBmgiqAF6EVZTZBzvLvAlcO8Z8A14AfwZenHEJ2D2XAEngXufLFqRANHPZTP2jkVEfL8Dt46bP6gNsAMcBW1dAG8sL4z6Fdie7kX8FfgFPLQ5xQBsc3NqgKT4CID5eDVAcrwa4G3LngrgNCdeCdAWrwI4BR7lxKsAuuIVAEXxCoC+eG+A4njwBXhn2PMCcIkHPwBLPPgAuMWDD4A1HsoBXOOhHCAlHsoA3OOhDCA1HvIBJPGQD/A+cy8H4AxRPOQB5MZDOsDZ9D3KLhXgA7BSsJcCII+HNIDSeLADhMSDHeAj5fFgAwiLBxuAVzwMA4TGwzCAZzz0A5wTHA/9AN7x0A1QJR66ARTx0A5QLR7aAXbQxMNtgKrxcBtAGQ+LANXjYRFgD/13BzOAS+CJeMt068BfYJeYL052uf7v7dOALfM9CNy6N33GG2+88cLvP3CmutKKbbx2AAAAAElFTkSuQmCC
  8. // @grant none
  9. // @license MPL-2.0
  10. // @namespace charlesbobomb
  11. // ==/UserScript==
  12. function waitForElement(guaranteedParent, selector) {
  13. /**
  14. Waits for a given element to exist.
  15. @param {Element} guaranteedParent - A parent that will always exist (to be watched)
  16. @param {string} selector - A CSS selector for the element to wait for
  17. **/
  18. return new Promise(resolve => {
  19. const o = new MutationObserver(() => { // watch for descendants being added
  20. let el = document.querySelector(selector);
  21. if (el) {
  22. resolve(el || null);
  23. o.disconnect(); // stop watching
  24. }
  25. });
  26. o.observe(guaranteedParent || document.body, {
  27. childList: true,
  28. subtree: true
  29. });
  30. });
  31. }
  32.  
  33. (function() {
  34. 'use strict';
  35. const container = document.createElement("div");
  36. container.id = "ytspeed-container";
  37. container.innerHTML = '<p class="bold" style="font-size:2rem">YTSpeed+</p><input type="range" min="0.1" max="8" value="1" step="0.1" id="ytspeed-slider" list="ytspeed-list">'+
  38. '<datalist id="ytspeed-list" display="none"><option value="0.5"></option><option value="1"></option><option value="2"></option><option value="4"></option><option value="8"></option></option></datalist>'+
  39. '<p id="ytspeed-label">1x</p><label for="ytspeed-pitch">Preserve pitch</label><input type="checkbox" id="ytspeed-pitch" checked><a id="ytspeed-url" target="_blank" href="https://greasyfork.org/en/scripts/470633">about</a>';
  40. const style = document.createElement("style");
  41. style.innerText = `
  42. #ytspeed-container {font-size:1.2rem;}
  43. #ytspeed-container{background:var(--yt-spec-badge-chip-background);width:75%;padding:3vh;margin:3vh auto;border-radius:12px;text-align:center;}
  44. #ytspeed-label{margin-bottom:1vh;}
  45. #ytspeed-slider{width:100%;margin-top:1vh;background:transparent;height:2vh;}
  46.  
  47. #ytspeed-slider::-moz-range-track{background:var(--yt-spec-10-percent-layer);}
  48. #ytspeed-slider::-webkit-slider-runnable-track{background:var(--yt-spec-10-percent-layer);}
  49.  
  50. #ytspeed-slider::-moz-range-thumb{background:var(--yt-spec-themed-blue);height:12px;width:12px;transition:.1s;border:transparent;border-radius:50%;}
  51. #ytspeed-slider::-webkit-slider-thumb{background:var(--yt-spec-themed-blue);height:12px;width:12px;transition:.1s;border:transparent;border-radius:50%;}
  52. #ytspeed-slider:hover::-moz-range-thumb{height:18px;width:18px;}
  53. #ytspeed-slider:hover::-webkit-slider-thumb{height:18px;width:18px;}
  54. #ytspeed-slider::-moz-range-progress{background:var(--yt-spec-themed-blue);}
  55. #ytspeed-url {margin-top:1vh;color:var(--yt-spec-themed-blue);display:block;}
  56.  
  57. `; // various styles
  58.  
  59. document.head.appendChild(style);
  60. let slider = container.querySelector("#ytspeed-slider"),
  61. label = container.querySelector("#ytspeed-label"),
  62. pitch = container.querySelector("#ytspeed-pitch");
  63.  
  64. waitForElement(null, "#bottom-row").then(e => { // make sure the element immediately below the container exists
  65. e.insertAdjacentElement("beforebegin", container);
  66. const video = document.querySelector("video");
  67. const observer = new MutationObserver((changes)=> { // resets the speed & stuff when the video changes (doesn't reload bottom part of video)
  68. changes.forEach(function(mutation) {
  69. if (mutation.type === "attributes" && mutation.attributeName === "src") {
  70. pitch.checked = true;
  71. video.preservesPitch = true;
  72. slider.value = 1;
  73. video.playbackRate = 1;
  74. label.innerText = "1x";
  75. }
  76. });
  77. });
  78. observer.observe(video, {
  79. attributes: true
  80. });
  81. pitch.onchange = () => { video.preservesPitch = pitch.checked; }; // "Preserves pitch" checkbox
  82. slider.oninput = () => { video.playbackRate = parseFloat(slider.value);label.innerText = slider.value + "x"; }; // Slider
  83. });
  84. })();