YouTube: Audio Only

No Video Streaming

目前为 2024-01-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube: Audio Only
  3. // @description No Video Streaming
  4. // @namespace UserScript
  5. // @version 0.4.0
  6. // @author CY Fung
  7. // @match https://www.youtube.com/*
  8. // @match https://www.youtube.com/embed/*
  9. // @match https://www.youtube-nocookie.com/embed/*
  10. // @match https://m.youtube.com/*
  11. // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
  12. // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/YouTube-Audio-Only.png
  13. // @grant GM_registerMenuCommand
  14. // @grant GM.setValue
  15. // @grant GM.getValue
  16. // @run-at document-start
  17. // @license MIT
  18. // @compatible chrome
  19. // @compatible firefox
  20. // @compatible opera
  21. // @compatible edge
  22. // @compatible safari
  23. // @allFrames true
  24. //
  25. // ==/UserScript==
  26.  
  27. (async function () {
  28. 'use strict';
  29.  
  30. let setTimeout_ = setTimeout;
  31.  
  32. /** @type {globalThis.PromiseConstructor} */
  33. const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  34.  
  35. async function confirm(message) {
  36. // Create the HTML for the dialog
  37.  
  38. if (!document.body) return;
  39.  
  40. let dialog = document.getElementById('confirmDialog794');
  41. if (!dialog) {
  42.  
  43. const dialogHTML = `
  44. <div id="confirmDialog794" class="dialog-style" style="display: block;">
  45. <div class="confirm-box">
  46. <p>${message}</p>
  47. <div class="confirm-buttons">
  48. <button id="confirmBtn">Confirm</button>
  49. <button id="cancelBtn">Cancel</button>
  50. </div>
  51. </div>
  52. </div>
  53. `;
  54.  
  55. // Append the dialog to the document body
  56. document.body.insertAdjacentHTML('beforeend', dialogHTML);
  57. dialog = document.getElementById('confirmDialog794');
  58.  
  59. }
  60.  
  61. // Return a promise that resolves or rejects based on the user's choice
  62. return new Promise((resolve) => {
  63. document.getElementById('confirmBtn').onclick = () => {
  64. resolve(true);
  65. cleanup();
  66. };
  67.  
  68. document.getElementById('cancelBtn').onclick = () => {
  69. resolve(false);
  70. cleanup();
  71. };
  72.  
  73. function cleanup() {
  74. dialog && dialog.remove();
  75. dialog = null;
  76. }
  77. });
  78. }
  79.  
  80.  
  81.  
  82. if (location.pathname === '/live_chat' || location.pathname === 'live_chat_replay') return;
  83.  
  84.  
  85. const pageInjectionCode = function () {
  86.  
  87.  
  88. /** @type {globalThis.PromiseConstructor} */
  89. const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  90.  
  91. const PromiseExternal = ((resolve_, reject_) => {
  92. const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
  93. return class PromiseExternal extends Promise {
  94. constructor(cb = h) {
  95. super(cb);
  96. if (cb === h) {
  97. /** @type {(value: any) => void} */
  98. this.resolve = resolve_;
  99. /** @type {(reason?: any) => void} */
  100. this.reject = reject_;
  101. }
  102. }
  103. };
  104. })();
  105.  
  106.  
  107.  
  108. const observablePromise = (proc, timeoutPromise) => {
  109. let promise = null;
  110. return {
  111. obtain() {
  112. if (!promise) {
  113. promise = new Promise(resolve => {
  114. let mo = null;
  115. const f = () => {
  116. let t = proc();
  117. if (t) {
  118. mo.disconnect();
  119. mo.takeRecords();
  120. mo = null;
  121. resolve(t);
  122. }
  123. }
  124. mo = new MutationObserver(f);
  125. mo.observe(document, { subtree: true, childList: true })
  126. f();
  127. timeoutPromise && timeoutPromise.then(() => {
  128. resolve(null)
  129. });
  130. });
  131. }
  132. return promise
  133. }
  134. }
  135. }
  136.  
  137.  
  138. let vcc = 0;
  139. let vdd = -1;
  140.  
  141. let u33 = null;
  142.  
  143. document.addEventListener('durationchange', (evt) => {
  144. const target = (evt || 0).target;
  145. if (!(target instanceof HTMLMediaElement)) return;
  146.  
  147. if (target.classList.contains('video-stream') && target.classList.contains('html5-main-video')) {
  148.  
  149. if (target.readyState === 1) {
  150.  
  151. vcc++;
  152.  
  153. }
  154. if (target.readyState === 1 && target.networkState === 2) {
  155. target.__spfgs__ = true;
  156. if (u33) {
  157. u33.resolve();
  158. u33 = null;
  159. }
  160. } else {
  161. target.__spfgs__ = false;
  162.  
  163. }
  164.  
  165. }
  166. }, true);
  167.  
  168.  
  169.  
  170. // XMLHttpRequest.prototype.open299 = XMLHttpRequest.prototype.open;
  171. /*
  172.  
  173. XMLHttpRequest.prototype.open2 = function(method, url, ...args){
  174.  
  175. if (typeof url === 'string' && url.length > 24 && url.includes('/videoplayback?') && url.replace('?', '&').includes('&source=')) {
  176. if (vcc !== vdd) {
  177. vdd = vcc;
  178. window.postMessage({ ZECxh: url.includes('source=yt_live_broadcast') }, "*");
  179. }
  180. }
  181.  
  182. return this.open299(method, url, ...args)
  183. }*/
  184.  
  185.  
  186.  
  187. // desktop only
  188. // document.addEventListener('yt-page-data-fetched', async (evt) => {
  189.  
  190. // const pageFetchedDataLocal = evt.detail;
  191. // let isLiveNow;
  192. // try {
  193. // isLiveNow = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.isLiveNow;
  194. // } catch (e) { }
  195. // window.postMessage({ ZECxh: isLiveNow === true }, "*");
  196.  
  197. // }, false);
  198.  
  199. // return;
  200.  
  201. // let clickLockFn = null;
  202. if (location.origin === 'https://m.youtube.com') {
  203.  
  204.  
  205.  
  206. EventTarget.prototype.addEventListener322 = EventTarget.prototype.addEventListener;
  207.  
  208. EventTarget.prototype.addEventListener = function (evt, fn, opts) {
  209.  
  210. if (evt === 'visibilitychange') {
  211. evt += 'y'
  212. }
  213. let hn = fn;
  214.  
  215. // if (evt === 'click' && this.id === 'movie_player') {
  216.  
  217.  
  218. // // clickLockFn = fn;
  219. // hn = function (e) {
  220.  
  221. // // console.log(22 , e)
  222. // // console.log(433, e.type, e.detail, fn);
  223. // // window.em33 = true;
  224. // // if(e && e.type !=='updateui' && e.type!=='success' && e.type!==''){
  225. // // console.log(433, e.type, e.detail);
  226.  
  227. // // }
  228. // return fn.apply(this, arguments)
  229. // }
  230.  
  231. // }
  232.  
  233. /*
  234.  
  235. if(evt ==='player-state-change' || evt == "player-autonav-pause" || evt === "video-data-change" || evt === "state-navigatestart"){
  236.  
  237. hn = function(){
  238.  
  239. let e = arguments[0];
  240. if(e){
  241. console.log(213, e.type, e.detail);
  242.  
  243. }
  244. return fn.apply(this, arguments)
  245. }
  246. }
  247. */
  248.  
  249. return this.addEventListener322(evt, hn, opts)
  250.  
  251. }
  252.  
  253. /*
  254. const XMLHttpRequest_ = XMLHttpRequest;
  255.  
  256. (() => {
  257. XMLHttpRequest = class XMLHttpRequest extends XMLHttpRequest_ {
  258. constructor(...args) {
  259. super(...args);
  260. }
  261. open(method, url, ...args) {
  262.  
  263. if (typeof url === 'string' && url.length > 24 && url.includes('/videoplayback?') && url.replace('?', '&').includes('&source=')) {
  264. if (vcc !== vdd) {
  265. vdd = vcc;
  266. window.postMessage({ ZECxh: url.includes('source=yt_live_broadcast') }, "*");
  267. }
  268. }
  269. return super.open(method, url, ...args)
  270. }
  271. }
  272. })();
  273. */
  274. }
  275.  
  276.  
  277. let setTimeout_ = setTimeout;
  278.  
  279.  
  280. if (location.origin === 'https://www.youtube.com') {
  281.  
  282.  
  283. document.addEventListener('yt-navigate-finish', async () => {
  284.  
  285. const fn = () => {
  286.  
  287. const elm = document.querySelector('ytd-player#ytd-player');
  288. if (!elm) return;
  289. const cnt = elm.polymerController || elm.inst || elm;
  290. if (!cnt) return;
  291.  
  292. if (!cnt.player_) return;
  293. if (!cnt.player_.playVideo) return;
  294.  
  295. return { elm, cnt };
  296. }
  297. let o = fn();
  298. if (!o) {
  299. o = await observablePromise(fn).obtain()
  300. }
  301. const { cnt, elm } = o;
  302. if (!cnt || !cnt.player_ || !cnt.player_.playVideo) return;
  303. if (cnt.player_.getPlayerState() === 3) {
  304. const audio = HTMLElement.prototype.querySelector.call(elm, '.video-stream.html5-main-video');
  305. if (audio.__spfgs__ !== true) { // undefined or false
  306. u33 = new PromiseExternal();
  307. await u33.then();
  308. }
  309.  
  310. if (cnt.player_.getPlayerState() !== 3 || !audio.isConnected) return;
  311. if (audio && audio.__spfgs__ === true) {
  312. await cnt.player_.cancelPlayback();
  313.  
  314. await new Promise(resolve => window.setTimeout(resolve, 1));
  315. await cnt.player_.playVideo();
  316.  
  317. }
  318. }
  319.  
  320. });
  321.  
  322. } else if (location.origin === 'https://m.youtube.com') {
  323.  
  324.  
  325. let px = 0;
  326. let fa = 0;
  327. document.addEventListener('durationchange', (evt) => {
  328.  
  329. if (evt.target.readyState !== 1) {
  330. fa = 1;
  331. if (px) clearTimeout(px);
  332. px = setTimeout_(() => {
  333.  
  334. let qq = 0;
  335. let cid = setInterval(() => {
  336. let q = document.querySelector('#movie_player');
  337. if (!q) return;
  338. let a = document.querySelector('.video-stream.html5-main-video');
  339. if (a.muted) return;
  340.  
  341. if (qq) return;
  342. qq = 1;
  343. clearInterval(cid);
  344.  
  345. if (px) clearTimeout(px);
  346. px = setTimeout_(() => {
  347.  
  348.  
  349. if (document.querySelector('.player-controls-content')) return;
  350.  
  351. if (fa !== 1) return;
  352. document.querySelector('#movie_player').click();
  353.  
  354. }, 400)
  355.  
  356. }, 400)
  357.  
  358.  
  359. }, 400);
  360. return;
  361. } else {
  362. fa = 2;
  363. }
  364. console.log(123123, evt.target, evt.target.duration)
  365.  
  366.  
  367. }, true)
  368.  
  369.  
  370.  
  371. }
  372.  
  373.  
  374.  
  375. let prepared = false;
  376. function prepare() {
  377. if (prepared) return;
  378. prepared = true;
  379.  
  380. if (typeof _yt_player !== 'undefined' && _yt_player && typeof _yt_player === 'object') {
  381.  
  382. for (const [k, v] of Object.entries(_yt_player)) {
  383.  
  384. if (typeof v === 'function' && typeof v.prototype.clone === 'function'
  385. && typeof v.prototype.get === 'function' && typeof v.prototype.set === 'function'
  386.  
  387. && typeof v.prototype.isEmpty === 'undefined' && typeof v.prototype.forEach === 'undefined'
  388. && typeof v.prototype.clear === 'undefined'
  389.  
  390. ) {
  391.  
  392. key = k;
  393.  
  394. }
  395.  
  396. }
  397.  
  398. }
  399.  
  400. if (key) {
  401.  
  402. const ClassX = _yt_player[key];
  403. _yt_player[key] = class extends ClassX {
  404. constructor(...args) {
  405.  
  406. if (typeof args[0] === 'string' && args[0].startsWith('http://')) args[0] = '';
  407. super(...args);
  408.  
  409. }
  410. }
  411. _yt_player[key].luX1Y = 1;
  412. }
  413.  
  414. }
  415. let s3 = Symbol();
  416. Object.defineProperty(Object.prototype, 'deviceIsAudioOnly', {
  417. get() {
  418. return this[s3];
  419. },
  420. set(nv) {
  421. if ('ATTRIBUTE_NODE' in this) {
  422.  
  423. } else {
  424. if (typeof nv === 'boolean') this[s3] = true;
  425. else this[s3] = undefined;
  426. prepare();
  427. }
  428. return true;
  429. },
  430. enumerable: false,
  431. configurable: true
  432. });
  433.  
  434.  
  435. let s1 = Symbol();
  436. let s2 = Symbol();
  437. Object.defineProperty(Object.prototype, 'defraggedFromSubfragments', {
  438. get() {
  439. return undefined;
  440. },
  441. set(nv) {
  442. return true;
  443. },
  444. enumerable: false,
  445. configurable: true
  446. });
  447.  
  448. Object.defineProperty(Object.prototype, 'hasSubfragmentedFmp4', {
  449. get() {
  450. return this[s1];
  451. },
  452. set(nv) {
  453. if (typeof nv === 'boolean') this[s1] = false;
  454. else this[s1] = undefined;
  455. return true;
  456. },
  457. enumerable: false,
  458. configurable: true
  459. });
  460.  
  461. Object.defineProperty(Object.prototype, 'hasSubfragmentedWebm', {
  462. get() {
  463. return this[s2];
  464. },
  465. set(nv) {
  466. if (typeof nv === 'boolean') this[s2] = false;
  467. else this[s2] = undefined;
  468. return true;
  469. },
  470. enumerable: false,
  471. configurable: true
  472. });
  473.  
  474.  
  475. const supportedFormatsConfig = () => {
  476.  
  477. function typeTest(type) {
  478. if (typeof type === 'string' && type.startsWith('video/')) {
  479. return false;
  480. }
  481. }
  482.  
  483. // return a custom MIME type checker that can defer to the original function
  484. function makeModifiedTypeChecker(origChecker) {
  485. // Check if a video type is allowed
  486. return function (type) {
  487. let res = undefined;
  488. if (type === undefined) res = false;
  489. else {
  490. res = typeTest.call(this, type);
  491. }
  492. if (res === undefined) res = origChecker.apply(this, arguments);
  493. return res;
  494. };
  495. }
  496.  
  497. // Override video element canPlayType() function
  498. const proto = (HTMLVideoElement || 0).prototype;
  499. if (proto && typeof proto.canPlayType == 'function') {
  500. proto.canPlayType = makeModifiedTypeChecker(proto.canPlayType);
  501. }
  502.  
  503. // Override media source extension isTypeSupported() function
  504. const mse = window.MediaSource;
  505. // Check for MSE support before use
  506. if (mse && typeof mse.isTypeSupported == 'function') {
  507. mse.isTypeSupported = makeModifiedTypeChecker(mse.isTypeSupported);
  508. }
  509.  
  510. };
  511.  
  512. supportedFormatsConfig();
  513. }
  514.  
  515. const isEnable = (typeof GM !== 'undefined' && typeof GM.getValue === 'function') ? (await GM.getValue("isEnable_aWsjF", true)) : null;
  516. if (typeof isEnable !== 'boolean') throw new DOMException("Please Update your browser", "NotSupportedError");
  517. if (isEnable) {
  518. const element = document.createElement('button');
  519. element.setAttribute('onclick', `(${pageInjectionCode})()`);
  520. element.click();
  521. }
  522.  
  523. GM_registerMenuCommand(`Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`, async function () {
  524. await GM.setValue("isEnable_aWsjF", !isEnable);
  525. location.reload();
  526. });
  527.  
  528. let messageCount = 0;
  529. let busy = false;
  530. window.addEventListener('message', (evt) => {
  531.  
  532. const v = ((evt || 0).data || 0).ZECxh;
  533. if (typeof v === 'boolean') {
  534. if (messageCount > 1e9) messageCount = 9;
  535. const t = ++messageCount;
  536. if (v && isEnable) {
  537. requestAnimationFrame(async () => {
  538. if (t !== messageCount) return;
  539. if (busy) return;
  540. busy = true;
  541. if (await confirm("Livestream is detected. Press OK to disable YouTube Audio Mode.")) {
  542. await GM.setValue("isEnable_aWsjF", !isEnable);
  543. location.reload();
  544. }
  545. busy = false;
  546. });
  547. }
  548. }
  549.  
  550. });
  551.  
  552.  
  553. const pLoad = new Promise(resolve => {
  554. if (document.readyState !== 'loading') {
  555. resolve();
  556. } else {
  557. window.addEventListener("DOMContentLoaded", resolve, false);
  558. }
  559. });
  560.  
  561.  
  562. function contextmenuInfoItemAppearedFn(target) {
  563.  
  564. const btn = target.closest('.ytp-menuitem[role="menuitem"]');
  565. if (!btn) return;
  566. if (btn.parentNode.querySelector('.ytp-menuitem[role="menuitem"].audio-only-toggle-btn')) return;
  567. document.documentElement.classList.add('with-audio-only-toggle-btn');
  568. const newBtn = btn.cloneNode(true)
  569. newBtn.querySelector('.ytp-menuitem-label').textContent = `Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`;
  570. newBtn.classList.add('audio-only-toggle-btn');
  571. btn.parentNode.insertBefore(newBtn, btn.nextSibling);
  572. newBtn.addEventListener('click', async () => {
  573. await GM.setValue("isEnable_aWsjF", !isEnable);
  574. location.reload();
  575. });
  576. }
  577.  
  578.  
  579. function mobileMenuItemAppearedFn(target) {
  580.  
  581. const btn = target.closest('ytm-menu-item');
  582. if (!btn) return;
  583. if (btn.parentNode.querySelector('ytm-menu-item.audio-only-toggle-btn')) return;
  584. document.documentElement.classList.add('with-audio-only-toggle-btn');
  585. const newBtn = btn.cloneNode(true);
  586. newBtn.querySelector('.menu-item-button').textContent = `Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`;
  587. newBtn.classList.add('audio-only-toggle-btn');
  588. btn.parentNode.insertBefore(newBtn, btn.nextSibling);
  589. newBtn.addEventListener('click', async () => {
  590. await GM.setValue("isEnable_aWsjF", !isEnable);
  591. location.reload();
  592. });
  593. }
  594.  
  595.  
  596.  
  597.  
  598. pLoad.then(() => {
  599.  
  600. document.addEventListener('animationstart', (evt) => {
  601. const animationName = evt.animationName;
  602. if (!animationName) return;
  603.  
  604. if (animationName === 'contextmenuInfoItemAppeared') contextmenuInfoItemAppearedFn(evt.target);
  605. if (animationName === 'mobileMenuItemAppeared') mobileMenuItemAppearedFn(evt.target);
  606.  
  607. }, true);
  608.  
  609.  
  610. const style = document.createElement('style');
  611. style.textContent = `
  612. @keyframes mobileMenuItemAppeared {
  613. 0% {
  614. background-position-x: 3px;
  615. }
  616. 100% {
  617. background-position-x: 4px;
  618. }
  619. }
  620. ytm-select.player-speed-settings ~ ytm-menu-item:last-of-type {
  621. animation: mobileMenuItemAppeared 1ms linear 0s 1 normal forwards;
  622. }
  623. @keyframes contextmenuInfoItemAppeared {
  624. 0% {
  625. background-position-x: 3px;
  626. }
  627. 100% {
  628. background-position-x: 4px;
  629. }
  630. }
  631. .ytp-contextmenu .ytp-menuitem[role="menuitem"] path[d^="M22 34h4V22h-4v12zm2-30C12.95"]{
  632. animation: contextmenuInfoItemAppeared 1ms linear 0s 1 normal forwards;
  633. }
  634. .with-audio-only-toggle-btn .ytp-contextmenu, .ytp-panel-menu, .ytp-panel {
  635. height: 40vh !important;
  636. }
  637. #confirmDialog794 {
  638. z-index:999999 !important;
  639. display: none;
  640. /* Hidden by default */
  641. position: fixed;
  642. /* Stay in place */
  643. z-index: 1;
  644. /* Sit on top */
  645. left: 0;
  646. top: 0;
  647. width: 100%;
  648. /* Full width */
  649. height: 100%;
  650. /* Full height */
  651. overflow: auto;
  652. /* Enable scroll if needed */
  653. background-color: rgba(0,0,0,0.4);
  654. /* Black w/ opacity */
  655. }
  656. #confirmDialog794 .confirm-box {
  657. position:relative;
  658. color: black;
  659.  
  660. z-index:999999 !important;
  661. background-color: #fefefe;
  662. margin: 15% auto;
  663. /* 15% from the top and centered */
  664. padding: 20px;
  665. border: 1px solid #888;
  666. width: 30%;
  667. /* Could be more or less, depending on screen size */
  668. box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  669. }
  670. #confirmDialog794 .confirm-buttons {
  671. text-align: right;
  672. }
  673. #confirmDialog794 button {
  674. margin-left: 10px;
  675. }
  676.  
  677.  
  678.  
  679. `
  680. document.head.appendChild(style);
  681. })
  682.  
  683.  
  684. })();
  685.