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.1
  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. let volumeSpan = null;
  113. let lastContent = null;
  114. let gainNode = null;
  115.  
  116. function refreshDOM() {
  117. volumeSlider = document.querySelector('.ytp-volume-panel[role="slider"][title]');
  118. if (volumeSlider) {
  119. volumeTitle = volumeSlider.getAttribute('title');
  120. } else {
  121. volumeTitle = '';
  122. }
  123. }
  124.  
  125. function setDblTap() {
  126. if (!volumeSlider) return;
  127. if (volumeSlider.hasAttribute('pKRyA')) return;
  128. volumeSlider.setAttribute('pKRyA', '');
  129.  
  130. addDblTap(volumeSlider, (e) => {
  131. let target = null;
  132. try {
  133. target = e.target.closest('.ytp-volume-area').querySelector('.ytp-mute-button');
  134. } catch (e) { }
  135. if (target !== null) {
  136. const e2 = new MouseEvent('contextmenu', {
  137. bubbles: true,
  138. cancelable: true,
  139. view: window
  140. });
  141. target.dispatchEvent(e2);
  142. }
  143. });
  144. }
  145.  
  146. let template = document.createElement('template');
  147.  
  148. function changeVolumeText() {
  149.  
  150. if (!volumeSpan || !lastContent) return;
  151.  
  152. let video = document.querySelector('#player video[src]');
  153. if (!video) return;
  154.  
  155. if (gainNode === null) {
  156. let 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. function addVideoEvents() {
  185. let video = document.querySelector('#player video[src]');
  186. if (!video) return;
  187. if (video.hasAttribute('zHbT0')) return;
  188. video.setAttribute('zHbT0', '');
  189. video.addEventListener('volumechange', changeVolumeText, false)
  190. }
  191.  
  192.  
  193. let ktid = 0;
  194. let goChecking = false;
  195.  
  196. const asyncNavigateFinish = async () => {
  197. goChecking = false;
  198. createCSS();
  199. const f = () => {
  200. refreshDOM();
  201. if (!volumeSlider) return;
  202. setDblTap();
  203. addVideoEvents();
  204. goChecking = true;
  205. return true;
  206. };
  207. f() || setTimeout(f, 300);
  208. }
  209.  
  210. const onNavigateFinish = () => {
  211. asyncNavigateFinish();
  212. };
  213. document.addEventListener('yt-navigate-finish', onNavigateFinish, true);
  214.  
  215.  
  216. (async function () {
  217.  
  218. const filterFn = t => t.textContent === volumeTitle;
  219. const r80Fn = r => setTimeout(r, 80);
  220. const r0Fn = r => requestAnimationFrame(r);
  221.  
  222. while (true) {
  223.  
  224. let tid = Date.now();
  225. ktid = tid;
  226. await new Promise(r80Fn);
  227. if (!goChecking) continue;
  228.  
  229. if (!volumeSpan) {
  230. let elms = [...document.querySelectorAll('#player .ytp-tooltip div.ytp-tooltip-text-wrapper span.ytp-tooltip-text')];
  231. if (elms.length > 0) {
  232. elms = elms.filter(filterFn);
  233. }
  234.  
  235. if (elms[0]) {
  236. HTMLElement.prototype.closest.call(elms[0], '#player .ytp-tooltip').classList.add('video-tip-offseted');
  237. volumeSpan = elms[0];
  238. lastContent = volumeSpan.textContent;
  239. }
  240. }
  241.  
  242. if (volumeSpan && (!volumeSpan.isConnected || volumeSpan.textContent !== lastContent)) {
  243. // volumeSpan.textContent = volumeTitle;
  244. let p = document.querySelector('.video-tip-offseted');
  245. if (p) p.classList.remove('video-tip-offseted');
  246. let m = document.querySelector('.volume-tip-offset');
  247. if (m) m.remove();
  248. volumeSpan = null;
  249. lastContent = null;
  250. }
  251.  
  252. if (volumeSpan) {
  253.  
  254. await new Promise(r0Fn);
  255. if (ktid === tid) {
  256. changeVolumeText();
  257. }
  258.  
  259. }
  260.  
  261.  
  262. }
  263.  
  264. })();
  265.  
  266.  
  267. })();