YouTube Volume Mouse Controller

Control YouTube video volume by mouse wheel.

当前为 2022-05-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Volume Mouse Controller
  3. // @namespace wddd
  4. // @version 1.5.0
  5. // @author wddd
  6. // @license MIT
  7. // @description Control YouTube video volume by mouse wheel.
  8. // @homepage https://github.com/wdwind/YouTubeVolumeMouseController
  9. // @match *://www.youtube.com/*
  10. // ==/UserScript==
  11.  
  12. function getVideo() {
  13. return document.getElementsByTagName("video")[0];
  14. }
  15.  
  16. function getPlayer() {
  17. var ytd_player = document.getElementsByTagName("ytd-player");
  18. for (var player of ytd_player) {
  19. if (player.getPlayer()) {
  20. return player.getPlayer();
  21. }
  22. }
  23. }
  24.  
  25. function run() {
  26. var player = getPlayer();
  27.  
  28. if (!player) {
  29. // eslint-disable-next-line no-console
  30. console.log("Player not found (yet).");
  31. return;
  32. }
  33.  
  34. var timer = 0;
  35.  
  36. // detect available wheel event
  37. var support = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel"
  38. document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel"
  39. "DOMMouseScroll"; // let"s assume that remaining browsers are older Firefox
  40.  
  41. getVideo().addEventListener(support, function (event) {
  42. var volume = player.getVolume();
  43. var volumeDelta = 5;
  44. var deltaY = support == "mousewheel" ? event.wheelDelta : (event.deltaY || event.detail);
  45. // Optimize volume change for touchpad
  46. if (Math.abs(deltaY) < 5) {
  47. volumeDelta = Math.max(Math.floor(Math.abs(deltaY)), 1);
  48. }
  49.  
  50. volume += (deltaY > 0 ? -volumeDelta : volumeDelta);
  51.  
  52. // Limit the volume between 0 and 100
  53. volume = Math.max(0, Math.min(100, volume));
  54.  
  55. if (volume > 0) {
  56. player.unMute(true);
  57. }
  58. player.setVolume(volume, true);
  59.  
  60. timer = showSlider(timer, volume);
  61.  
  62. // Prevent the page to scroll
  63. event.preventDefault();
  64. event.stopImmediatePropagation();
  65. return false;
  66. });
  67. }
  68.  
  69. function showSlider(timer, volume) {
  70. if (timer) {
  71. clearTimeout(timer);
  72. }
  73.  
  74. var sliderBar = appendSlideBar();
  75.  
  76. sliderBar.style.display = "block";
  77. timer = setTimeout(function () {
  78. sliderBar.style.display = "none";
  79. }, 1000);
  80.  
  81. sliderBar.innerText = "Volume: " + volume;
  82.  
  83. return timer;
  84. }
  85.  
  86. function appendSlideBar() {
  87. var sliderBar = document.getElementById("sliderBar");
  88. if (!sliderBar) {
  89. var sliderBarElement = document.createElement("div");
  90. sliderBarElement.id = "sliderBar";
  91.  
  92. document.getElementsByClassName("html5-video-container")[0].appendChild(sliderBarElement);
  93.  
  94. sliderBar = document.getElementById("sliderBar");
  95. addCss(sliderBar, {
  96. "width": "100%",
  97. "height": "20px",
  98. "position": "relative",
  99. "z-index": "9999",
  100. "text-align": "center",
  101. "color": "#fff",
  102. "font-size": "initial",
  103. "opacity": "0.9",
  104. "background-color": "rgba(0,0,0,0.2)",
  105. });
  106. }
  107.  
  108. addCss(sliderBar, {"top": getSliderBarTopProp() + "px"});
  109.  
  110. return sliderBar;
  111. }
  112.  
  113. function addCss(element, css) {
  114. for (var cssAttr in css) {
  115. element.style[cssAttr] = css[cssAttr];
  116. }
  117. }
  118.  
  119. function getSliderBarTopProp() {
  120. var fullScreenTitleHeight = 0;
  121.  
  122. var fullScreenTitle = document.getElementsByClassName("ytp-title")[0];
  123. if (fullScreenTitle && fullScreenTitle.offsetParent) {
  124. fullScreenTitleHeight = fullScreenTitle.offsetHeight;
  125. }
  126.  
  127. var videoTop = getVideo().getBoundingClientRect().top;
  128. var headerBoundingBox =
  129. (document.getElementById("masthead-positioner") || document.getElementById("masthead-container")).getBoundingClientRect();
  130. var headerTop = headerBoundingBox.top;
  131. var headerHeight = headerBoundingBox.height;
  132.  
  133. var overlap = (headerHeight + headerTop > 0) ? Math.max(0, headerHeight - videoTop) : 0;
  134.  
  135. return Math.max(fullScreenTitleHeight, overlap);
  136. }
  137.  
  138. /**
  139. * YouTube use Javascript to navigate between pages. So the script will not work:
  140. * 1. If the script only includes/matches the sub pages (like the video page www.youtube.com/watch?v=...)
  141. * 2. And the user navigates to the sub page from a page which is not included/matched by the script
  142. *
  143. * In the above scenario, the script will not be executed.
  144. *
  145. * To run the script in all cases,
  146. * 1. Include/match the whole YouTube host
  147. * 2. Detect Javascript events, and run the script appropriately
  148. *
  149. * Details:
  150. * * https://stackoverflow.com/questions/32275387/recall-tampermonkey-script-when-page-location-changes/32277150#32277150
  151. * * https://stackoverflow.com/questions/34077641/how-to-detect-page-navigation-on-youtube-and-modify-html-before-page-is-rendered
  152. * * https://github.com/1c7/Youtube-Auto-Subtitle-Download/blob/master/Youtube-Subtitle-Downloader/Tampermonkey.js#L122-L152
  153. */
  154.  
  155. // trigger when navigating to new material design page
  156. window.addEventListener("yt-navigate-finish", function () {
  157. if (window.location.href.includes("/watch?v=")) {
  158. run();
  159. }
  160. });
  161.  
  162. // trigger when navigating to the old page
  163. window.addEventListener("spfdone", function () {
  164. if (window.location.href.includes("/watch?v=")) {
  165. run();
  166. }
  167. });
  168.  
  169. // trigger when directly loading the page
  170. window.addEventListener("DOMContentLoaded", function () {
  171. if (window.location.href.includes("/watch?v=")) {
  172. run();
  173. }
  174. });
  175.  
  176. /**
  177. * Use MutationObserver to cover all edge cases.
  178. * https://stackoverflow.com/a/39803618
  179. *
  180. * This is to handle the use case where navigation happens but <video> has not been loaded yet.
  181. * (In YouTube the contents are loaded asynchronously.)
  182. */
  183. var observer = new MutationObserver(function() {
  184. if (getVideo() && getPlayer()) {
  185. observer.disconnect();
  186. run();
  187. }
  188. });
  189.  
  190. observer.observe(document.body, /* config */ {childList: true, subtree: true});