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.1.2
  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.  
  61. let finished = false;
  62. const onNavigateFinish = () => {
  63. if (finished) return;
  64. finished = true;
  65. document.removeEventListener('yt-navigate-finish', onNavigateFinish, true);
  66.  
  67. document.head.appendChild(document.createElement('style')).textContent = `
  68. .video-tip-offseted {
  69. margin-top:-1em;
  70. }
  71. .volume-tip-gain{
  72. opacity:0.52;
  73. }
  74. .volume-tip-normalized{
  75. opacity:0.4;
  76. }
  77. `;
  78.  
  79.  
  80. setTimeout(() => {
  81.  
  82. let volumeSlider = document.querySelector('.ytp-volume-panel[role="slider"][title]');
  83.  
  84. let volumeTitle = volumeSlider.getAttribute('title');
  85.  
  86.  
  87.  
  88. function addDblTap(element, doubleClick) {
  89. // https://stackoverflow.com/questions/45804917/dblclick-doesnt-work-on-touch-devices
  90.  
  91. let expired
  92.  
  93.  
  94. let doubleTouch = function (e) {
  95. if (e.touches.length === 1) {
  96. if (!expired) {
  97. expired = e.timeStamp + 400
  98. } else if (e.timeStamp <= expired) {
  99. // remove the default of this event ( Zoom )
  100. e.preventDefault()
  101. doubleClick(e)
  102. // then reset the variable for other "double Touches" event
  103. expired = null
  104. } else {
  105. // if the second touch was expired, make it as it's the first
  106. expired = e.timeStamp + 400
  107. }
  108. }
  109. }
  110.  
  111. element.addEventListener('touchstart', doubleTouch)
  112. element.addEventListener('dblclick', doubleClick)
  113. }
  114.  
  115. addDblTap(volumeSlider, (e) => {
  116.  
  117. let target = null;
  118. try {
  119. target = e.target.closest('.ytp-volume-area').querySelector('.ytp-mute-button');
  120. } catch (e) { }
  121. console.log(target)
  122. const e2 = new MouseEvent('contextmenu', {
  123. bubbles: true,
  124. cancelable: true,
  125. view: window
  126. });
  127.  
  128. if (target) target.dispatchEvent(e2);
  129.  
  130.  
  131. })
  132.  
  133.  
  134.  
  135. let volumeSpan = null;
  136. let lastContent = null;
  137.  
  138.  
  139.  
  140. let video = document.querySelector('#player video[src]');
  141. let source = null;
  142. let gainNode = null;
  143. video.addEventListener('volumechange', changeVolumeText, false)
  144.  
  145.  
  146. let ktid = 0;
  147. let template = document.createElement('template');
  148.  
  149. function changeVolumeText() {
  150.  
  151. let video = document.querySelector('#player video[src]');
  152. if (!video) return;
  153.  
  154. if (gainNode === null) {
  155.  
  156. source = video.getMediaElementSource ? video.getMediaElementSource() : null;
  157. if (source) {
  158. gainNode = source.getGainNode ? source.getGainNode() : null;
  159. }
  160. }
  161.  
  162. let gainValue = (((gainNode || 0).gain || 0).value || 0);
  163. let m = gainValue || 1.0;
  164.  
  165. let actualVolume = document.querySelector('ytd-player').player_.getVolume();
  166. let normalized = video.volume * 100;
  167.  
  168. let gainText = gainValue ? `<span class="volume-tip-gain">Gain = ${+(gainValue.toFixed(2))}</span><br>` : '';
  169.  
  170. template.innerHTML = `
  171. <span class="volume-tip-offset">
  172. ${gainText}
  173. <span class="volume-tip-volume">Volume: ${(m * actualVolume).toFixed(1)}% </span><br>
  174. <span class="volume-tip-normalized"> Normalized: ${(m * normalized).toFixed(1)}%</span>
  175. </span>`.trim();
  176. if (volumeSpan.textContent !== template.content.textContent && lastContent === volumeSpan.textContent) {
  177.  
  178. volumeSpan.innerHTML = template.innerHTML;
  179. lastContent = volumeSpan.textContent;
  180.  
  181. }
  182. }
  183.  
  184. setInterval(() => {
  185.  
  186. if (!volumeSpan) {
  187. let elms = [...document.querySelectorAll('#player .ytp-tooltip div.ytp-tooltip-text-wrapper span.ytp-tooltip-text')];
  188. elms = elms.filter(t => t.textContent === volumeTitle);
  189.  
  190. if (elms[0]) {
  191. HTMLElement.prototype.closest.call(elms[0],'#player .ytp-tooltip').classList.add('video-tip-offseted');
  192. volumeSpan = elms[0];
  193. lastContent = volumeSpan.textContent;
  194. }
  195. }
  196.  
  197. if (volumeSpan && (!volumeSpan.isConnected || volumeSpan.textContent !== lastContent)) {
  198. // volumeSpan.textContent = volumeTitle;
  199. let p = document.querySelector('.video-tip-offseted');
  200. if(p) p.classList.remove('video-tip-offseted');
  201. let m = document.querySelector('.volume-tip-offset');
  202. if(m) m.remove();
  203. volumeSpan = null;
  204. lastContent = null;
  205. }
  206.  
  207. if (!volumeSpan) return;
  208. let tid = Date.now();
  209. ktid = tid;
  210. requestAnimationFrame(() => {
  211. if (ktid !== tid) return;
  212. changeVolumeText();
  213.  
  214. });
  215.  
  216. }, 80)
  217.  
  218. }, 300);
  219.  
  220.  
  221. };
  222. document.addEventListener('yt-navigate-finish', onNavigateFinish, true);
  223. setTimeout(onNavigateFinish, 800);
  224.  
  225.  
  226. })();