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