YouTube Volume Assistant

Enhances the volume control on YouTube by providing additional information and features.

目前为 2023-05-21 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Volume Assistant
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.0
  5. // @description Enhances the volume control on YouTube by providing additional information and features.
  6. // @author CY Fung
  7. // @license MIT License
  8. // @match https://www.youtube.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  10. // @grant none
  11. // @run-at document-start
  12. // @unwrap
  13. // @allFrames
  14. // @inject-into page
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. // AudioContext.prototype._createGain = AudioContext.prototype.createGain;
  21.  
  22.  
  23. let wm = new WeakMap();
  24. /*
  25. AudioContext.prototype.createGain = function(...args){
  26. return this.createdGain || (this.createdGain = this._createGain(...args));
  27. }
  28. */
  29.  
  30. function getMediaElementSource() {
  31. return wm.get(this) || null;
  32. }
  33. function getGainNode() {
  34. return wm.get(this) || null;
  35. }
  36.  
  37. AudioContext.prototype._createMediaElementSource = AudioContext.prototype.createMediaElementSource;
  38.  
  39. AudioContext.prototype.createMediaElementSource = function (video, ...args) {
  40. let createdMediaElementSource = wm.get(video);
  41. if (createdMediaElementSource) return createdMediaElementSource;
  42. wm.set(video, createdMediaElementSource = this._createMediaElementSource(video, ...args));
  43. video.getMediaElementSource = getMediaElementSource;
  44. return createdMediaElementSource;
  45. }
  46.  
  47.  
  48. MediaElementAudioSourceNode.prototype._connect = MediaElementAudioSourceNode.prototype.connect;
  49.  
  50. MediaElementAudioSourceNode.prototype.connect = function (gainNode, ...args) {
  51.  
  52. this._connect(gainNode, ...args);
  53. wm.set(this, gainNode);
  54.  
  55. this.getGainNode = getGainNode;
  56. }
  57.  
  58.  
  59.  
  60. function addDblTap(element, doubleClick) {
  61. // https://stackoverflow.com/questions/45804917/dblclick-doesnt-work-on-touch-devices
  62.  
  63. let expired
  64.  
  65.  
  66. let doubleTouch = function (e) {
  67. if (e.touches.length === 1) {
  68. if (!expired) {
  69. expired = e.timeStamp + 400
  70. } else if (e.timeStamp <= expired) {
  71. // remove the default of this event ( Zoom )
  72. e.preventDefault()
  73. doubleClick(e)
  74. // then reset the variable for other "double Touches" event
  75. expired = null
  76. } else {
  77. // if the second touch was expired, make it as it's the first
  78. expired = e.timeStamp + 400
  79. }
  80. }
  81. }
  82.  
  83. element.addEventListener('touchstart', doubleTouch)
  84. element.addEventListener('dblclick', doubleClick)
  85. }
  86.  
  87.  
  88. function createCSS() {
  89.  
  90. if (document.querySelector('#iTFoh')) return;
  91. let style = document.createElement('style');
  92. style.id = 'iTFoh';
  93. style.textContent = `
  94. .video-tip-offseted {
  95. margin-top:-1em;
  96. }
  97. .volume-tip-gain{
  98. opacity:0.52;
  99. }
  100. .volume-tip-normalized{
  101. opacity:0.4;
  102. }
  103. `;
  104.  
  105. document.head.appendChild(style)
  106.  
  107. }
  108.  
  109. let volumeSlider = null;
  110. let volumeTitle = '';
  111.  
  112.  
  113. let volumeSpan = null;
  114. let lastContent = null;
  115. let source = null;
  116. let gainNode = null;
  117.  
  118.  
  119.  
  120. function refreshDOM() {
  121.  
  122. volumeSlider = document.querySelector('.ytp-volume-panel[role="slider"][title]');
  123. if (volumeSlider) {
  124.  
  125. volumeTitle = volumeSlider.getAttribute('title');
  126. } else {
  127. volumeTitle = '';
  128. }
  129.  
  130. }
  131. function setDblTap() {
  132. if (!volumeSlider) return;
  133. if (volumeSlider.hasAttribute('pKRyA')) return;
  134. volumeSlider.setAttribute('pKRyA', '');
  135.  
  136. addDblTap(volumeSlider, (e) => {
  137.  
  138. let target = null;
  139. try {
  140. target = e.target.closest('.ytp-volume-area').querySelector('.ytp-mute-button');
  141. } catch (e) { }
  142. console.log(target)
  143. const e2 = new MouseEvent('contextmenu', {
  144. bubbles: true,
  145. cancelable: true,
  146. view: window
  147. });
  148.  
  149. if (target) target.dispatchEvent(e2);
  150.  
  151.  
  152. });
  153. }
  154.  
  155. let template = document.createElement('template');
  156.  
  157.  
  158. function changeVolumeText() {
  159.  
  160. let video = document.querySelector('#player video[src]');
  161. if (!video) return;
  162.  
  163. if (gainNode === null) {
  164.  
  165. source = video.getMediaElementSource ? video.getMediaElementSource() : null;
  166. if (source) {
  167. gainNode = source.getGainNode ? source.getGainNode() : null;
  168. }
  169. }
  170.  
  171. let gainValue = (((gainNode || 0).gain || 0).value || 0);
  172. let m = gainValue || 1.0;
  173.  
  174. let actualVolume = document.querySelector('ytd-player').player_.getVolume();
  175. let normalized = video.volume * 100;
  176.  
  177. let gainText = gainValue ? `<span class="volume-tip-gain">Gain = ${+(gainValue.toFixed(2))}</span><br>` : '';
  178.  
  179. template.innerHTML = `
  180. <span class="volume-tip-offset">
  181. ${gainText}
  182. <span class="volume-tip-volume">Volume: ${(m * actualVolume).toFixed(1)}% </span><br>
  183. <span class="volume-tip-normalized"> Normalized: ${(m * normalized).toFixed(1)}%</span>
  184. </span>`.trim();
  185. if (volumeSpan.textContent !== template.content.textContent && lastContent === volumeSpan.textContent) {
  186.  
  187. volumeSpan.innerHTML = template.innerHTML;
  188. lastContent = volumeSpan.textContent;
  189.  
  190. }
  191. }
  192.  
  193. function addVideoEvents() {
  194.  
  195.  
  196.  
  197. let video = document.querySelector('#player video[src]');
  198. if (!video) return;
  199. if (video.hasAttribute('zHbT0')) return;
  200. video.setAttribute('zHbT0', '');
  201. video.addEventListener('volumechange', changeVolumeText, false)
  202.  
  203.  
  204.  
  205. }
  206.  
  207.  
  208. let ktid = 0;
  209. let goChecking = false;
  210.  
  211. const asyncNavigateFinish = async () => {
  212.  
  213. goChecking = false;
  214. createCSS();
  215. let cid = 0;
  216.  
  217. const f = () => {
  218. if (!cid) return;
  219. refreshDOM();
  220. if (!volumeSlider) return;
  221. setDblTap();
  222. addVideoEvents();
  223. goChecking = true;
  224. clearTimeout(cid);
  225. cid = 0;
  226. };
  227.  
  228. cid = setTimeout(f, 300);
  229. f();
  230. }
  231.  
  232. const onNavigateFinish = () => {
  233. asyncNavigateFinish();
  234.  
  235. };
  236. document.addEventListener('yt-navigate-finish', onNavigateFinish, true);
  237.  
  238.  
  239. setInterval(() => {
  240.  
  241. if (!goChecking) return;
  242.  
  243. if (!volumeSpan) {
  244. let elms = [...document.querySelectorAll('#player .ytp-tooltip div.ytp-tooltip-text-wrapper span.ytp-tooltip-text')];
  245. elms = elms.filter(t => t.textContent === volumeTitle);
  246.  
  247. if (elms[0]) {
  248. HTMLElement.prototype.closest.call(elms[0], '#player .ytp-tooltip').classList.add('video-tip-offseted');
  249. volumeSpan = elms[0];
  250. lastContent = volumeSpan.textContent;
  251. }
  252. }
  253.  
  254. if (volumeSpan && (!volumeSpan.isConnected || volumeSpan.textContent !== lastContent)) {
  255. // volumeSpan.textContent = volumeTitle;
  256. let p = document.querySelector('.video-tip-offseted');
  257. if (p) p.classList.remove('video-tip-offseted');
  258. let m = document.querySelector('.volume-tip-offset');
  259. if (m) m.remove();
  260. volumeSpan = null;
  261. lastContent = null;
  262. }
  263.  
  264. if (!volumeSpan) return;
  265. let tid = Date.now();
  266. ktid = tid;
  267. requestAnimationFrame(() => {
  268. if (ktid !== tid) return;
  269. changeVolumeText();
  270.  
  271. });
  272.  
  273. }, 80)
  274.  
  275.  
  276. })();