// ==UserScript== // @name YouTube: Audio Only // @description No Video Streaming // @namespace UserScript // @version 0.4.0 // @author CY Fung // @match https://www.youtube.com/* // @match https://www.youtube.com/embed/* // @match https://www.youtube-nocookie.com/embed/* // @match https://m.youtube.com/* // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/YouTube-Audio-Only.png // @grant GM_registerMenuCommand // @grant GM.setValue // @grant GM.getValue // @run-at document-start // @license MIT // @compatible chrome // @compatible firefox // @compatible opera // @compatible edge // @compatible safari // @allFrames true // // @downloadURL none // ==/UserScript== (async function () { 'use strict'; let setTimeout_ = setTimeout; /** @type {globalThis.PromiseConstructor} */ const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. async function confirm(message) { // Create the HTML for the dialog if (!document.body) return; let dialog = document.getElementById('confirmDialog794'); if (!dialog) { const dialogHTML = `

${message}

`; // Append the dialog to the document body document.body.insertAdjacentHTML('beforeend', dialogHTML); dialog = document.getElementById('confirmDialog794'); } // Return a promise that resolves or rejects based on the user's choice return new Promise((resolve) => { document.getElementById('confirmBtn').onclick = () => { resolve(true); cleanup(); }; document.getElementById('cancelBtn').onclick = () => { resolve(false); cleanup(); }; function cleanup() { dialog && dialog.remove(); dialog = null; } }); } if (location.pathname === '/live_chat' || location.pathname === 'live_chat_replay') return; const pageInjectionCode = function () { /** @type {globalThis.PromiseConstructor} */ const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. const PromiseExternal = ((resolve_, reject_) => { const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject }; return class PromiseExternal extends Promise { constructor(cb = h) { super(cb); if (cb === h) { /** @type {(value: any) => void} */ this.resolve = resolve_; /** @type {(reason?: any) => void} */ this.reject = reject_; } } }; })(); const observablePromise = (proc, timeoutPromise) => { let promise = null; return { obtain() { if (!promise) { promise = new Promise(resolve => { let mo = null; const f = () => { let t = proc(); if (t) { mo.disconnect(); mo.takeRecords(); mo = null; resolve(t); } } mo = new MutationObserver(f); mo.observe(document, { subtree: true, childList: true }) f(); timeoutPromise && timeoutPromise.then(() => { resolve(null) }); }); } return promise } } } let vcc = 0; let vdd = -1; let u33 = null; document.addEventListener('durationchange', (evt) => { const target = (evt || 0).target; if (!(target instanceof HTMLMediaElement)) return; if (target.classList.contains('video-stream') && target.classList.contains('html5-main-video')) { if (target.readyState === 1) { vcc++; } if (target.readyState === 1 && target.networkState === 2) { target.__spfgs__ = true; if (u33) { u33.resolve(); u33 = null; } } else { target.__spfgs__ = false; } } }, true); // XMLHttpRequest.prototype.open299 = XMLHttpRequest.prototype.open; /* XMLHttpRequest.prototype.open2 = function(method, url, ...args){ if (typeof url === 'string' && url.length > 24 && url.includes('/videoplayback?') && url.replace('?', '&').includes('&source=')) { if (vcc !== vdd) { vdd = vcc; window.postMessage({ ZECxh: url.includes('source=yt_live_broadcast') }, "*"); } } return this.open299(method, url, ...args) }*/ // desktop only // document.addEventListener('yt-page-data-fetched', async (evt) => { // const pageFetchedDataLocal = evt.detail; // let isLiveNow; // try { // isLiveNow = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.isLiveNow; // } catch (e) { } // window.postMessage({ ZECxh: isLiveNow === true }, "*"); // }, false); // return; // let clickLockFn = null; if (location.origin === 'https://m.youtube.com') { EventTarget.prototype.addEventListener322 = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function (evt, fn, opts) { if (evt === 'visibilitychange') { evt += 'y' } let hn = fn; // if (evt === 'click' && this.id === 'movie_player') { // // clickLockFn = fn; // hn = function (e) { // // console.log(22 , e) // // console.log(433, e.type, e.detail, fn); // // window.em33 = true; // // if(e && e.type !=='updateui' && e.type!=='success' && e.type!==''){ // // console.log(433, e.type, e.detail); // // } // return fn.apply(this, arguments) // } // } /* if(evt ==='player-state-change' || evt == "player-autonav-pause" || evt === "video-data-change" || evt === "state-navigatestart"){ hn = function(){ let e = arguments[0]; if(e){ console.log(213, e.type, e.detail); } return fn.apply(this, arguments) } } */ return this.addEventListener322(evt, hn, opts) } /* const XMLHttpRequest_ = XMLHttpRequest; (() => { XMLHttpRequest = class XMLHttpRequest extends XMLHttpRequest_ { constructor(...args) { super(...args); } open(method, url, ...args) { if (typeof url === 'string' && url.length > 24 && url.includes('/videoplayback?') && url.replace('?', '&').includes('&source=')) { if (vcc !== vdd) { vdd = vcc; window.postMessage({ ZECxh: url.includes('source=yt_live_broadcast') }, "*"); } } return super.open(method, url, ...args) } } })(); */ } let setTimeout_ = setTimeout; if (location.origin === 'https://www.youtube.com') { document.addEventListener('yt-navigate-finish', async () => { const fn = () => { const elm = document.querySelector('ytd-player#ytd-player'); if (!elm) return; const cnt = elm.polymerController || elm.inst || elm; if (!cnt) return; if (!cnt.player_) return; if (!cnt.player_.playVideo) return; return { elm, cnt }; } let o = fn(); if (!o) { o = await observablePromise(fn).obtain() } const { cnt, elm } = o; if (!cnt || !cnt.player_ || !cnt.player_.playVideo) return; if (cnt.player_.getPlayerState() === 3) { const audio = HTMLElement.prototype.querySelector.call(elm, '.video-stream.html5-main-video'); if (audio.__spfgs__ !== true) { // undefined or false u33 = new PromiseExternal(); await u33.then(); } if (cnt.player_.getPlayerState() !== 3 || !audio.isConnected) return; if (audio && audio.__spfgs__ === true) { await cnt.player_.cancelPlayback(); await new Promise(resolve => window.setTimeout(resolve, 1)); await cnt.player_.playVideo(); } } }); } else if (location.origin === 'https://m.youtube.com') { let px = 0; let fa = 0; document.addEventListener('durationchange', (evt) => { if (evt.target.readyState !== 1) { fa = 1; if (px) clearTimeout(px); px = setTimeout_(() => { let qq = 0; let cid = setInterval(() => { let q = document.querySelector('#movie_player'); if (!q) return; let a = document.querySelector('.video-stream.html5-main-video'); if (a.muted) return; if (qq) return; qq = 1; clearInterval(cid); if (px) clearTimeout(px); px = setTimeout_(() => { if (document.querySelector('.player-controls-content')) return; if (fa !== 1) return; document.querySelector('#movie_player').click(); }, 400) }, 400) }, 400); return; } else { fa = 2; } console.log(123123, evt.target, evt.target.duration) }, true) } let prepared = false; function prepare() { if (prepared) return; prepared = true; if (typeof _yt_player !== 'undefined' && _yt_player && typeof _yt_player === 'object') { for (const [k, v] of Object.entries(_yt_player)) { if (typeof v === 'function' && typeof v.prototype.clone === 'function' && typeof v.prototype.get === 'function' && typeof v.prototype.set === 'function' && typeof v.prototype.isEmpty === 'undefined' && typeof v.prototype.forEach === 'undefined' && typeof v.prototype.clear === 'undefined' ) { key = k; } } } if (key) { const ClassX = _yt_player[key]; _yt_player[key] = class extends ClassX { constructor(...args) { if (typeof args[0] === 'string' && args[0].startsWith('http://')) args[0] = ''; super(...args); } } _yt_player[key].luX1Y = 1; } } let s3 = Symbol(); Object.defineProperty(Object.prototype, 'deviceIsAudioOnly', { get() { return this[s3]; }, set(nv) { if ('ATTRIBUTE_NODE' in this) { } else { if (typeof nv === 'boolean') this[s3] = true; else this[s3] = undefined; prepare(); } return true; }, enumerable: false, configurable: true }); let s1 = Symbol(); let s2 = Symbol(); Object.defineProperty(Object.prototype, 'defraggedFromSubfragments', { get() { return undefined; }, set(nv) { return true; }, enumerable: false, configurable: true }); Object.defineProperty(Object.prototype, 'hasSubfragmentedFmp4', { get() { return this[s1]; }, set(nv) { if (typeof nv === 'boolean') this[s1] = false; else this[s1] = undefined; return true; }, enumerable: false, configurable: true }); Object.defineProperty(Object.prototype, 'hasSubfragmentedWebm', { get() { return this[s2]; }, set(nv) { if (typeof nv === 'boolean') this[s2] = false; else this[s2] = undefined; return true; }, enumerable: false, configurable: true }); const supportedFormatsConfig = () => { function typeTest(type) { if (typeof type === 'string' && type.startsWith('video/')) { return false; } } // return a custom MIME type checker that can defer to the original function function makeModifiedTypeChecker(origChecker) { // Check if a video type is allowed return function (type) { let res = undefined; if (type === undefined) res = false; else { res = typeTest.call(this, type); } if (res === undefined) res = origChecker.apply(this, arguments); return res; }; } // Override video element canPlayType() function const proto = (HTMLVideoElement || 0).prototype; if (proto && typeof proto.canPlayType == 'function') { proto.canPlayType = makeModifiedTypeChecker(proto.canPlayType); } // Override media source extension isTypeSupported() function const mse = window.MediaSource; // Check for MSE support before use if (mse && typeof mse.isTypeSupported == 'function') { mse.isTypeSupported = makeModifiedTypeChecker(mse.isTypeSupported); } }; supportedFormatsConfig(); } const isEnable = (typeof GM !== 'undefined' && typeof GM.getValue === 'function') ? (await GM.getValue("isEnable_aWsjF", true)) : null; if (typeof isEnable !== 'boolean') throw new DOMException("Please Update your browser", "NotSupportedError"); if (isEnable) { const element = document.createElement('button'); element.setAttribute('onclick', `(${pageInjectionCode})()`); element.click(); } GM_registerMenuCommand(`Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`, async function () { await GM.setValue("isEnable_aWsjF", !isEnable); location.reload(); }); let messageCount = 0; let busy = false; window.addEventListener('message', (evt) => { const v = ((evt || 0).data || 0).ZECxh; if (typeof v === 'boolean') { if (messageCount > 1e9) messageCount = 9; const t = ++messageCount; if (v && isEnable) { requestAnimationFrame(async () => { if (t !== messageCount) return; if (busy) return; busy = true; if (await confirm("Livestream is detected. Press OK to disable YouTube Audio Mode.")) { await GM.setValue("isEnable_aWsjF", !isEnable); location.reload(); } busy = false; }); } } }); const pLoad = new Promise(resolve => { if (document.readyState !== 'loading') { resolve(); } else { window.addEventListener("DOMContentLoaded", resolve, false); } }); function contextmenuInfoItemAppearedFn(target) { const btn = target.closest('.ytp-menuitem[role="menuitem"]'); if (!btn) return; if (btn.parentNode.querySelector('.ytp-menuitem[role="menuitem"].audio-only-toggle-btn')) return; document.documentElement.classList.add('with-audio-only-toggle-btn'); const newBtn = btn.cloneNode(true) newBtn.querySelector('.ytp-menuitem-label').textContent = `Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`; newBtn.classList.add('audio-only-toggle-btn'); btn.parentNode.insertBefore(newBtn, btn.nextSibling); newBtn.addEventListener('click', async () => { await GM.setValue("isEnable_aWsjF", !isEnable); location.reload(); }); } function mobileMenuItemAppearedFn(target) { const btn = target.closest('ytm-menu-item'); if (!btn) return; if (btn.parentNode.querySelector('ytm-menu-item.audio-only-toggle-btn')) return; document.documentElement.classList.add('with-audio-only-toggle-btn'); const newBtn = btn.cloneNode(true); newBtn.querySelector('.menu-item-button').textContent = `Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`; newBtn.classList.add('audio-only-toggle-btn'); btn.parentNode.insertBefore(newBtn, btn.nextSibling); newBtn.addEventListener('click', async () => { await GM.setValue("isEnable_aWsjF", !isEnable); location.reload(); }); } pLoad.then(() => { document.addEventListener('animationstart', (evt) => { const animationName = evt.animationName; if (!animationName) return; if (animationName === 'contextmenuInfoItemAppeared') contextmenuInfoItemAppearedFn(evt.target); if (animationName === 'mobileMenuItemAppeared') mobileMenuItemAppearedFn(evt.target); }, true); const style = document.createElement('style'); style.textContent = ` @keyframes mobileMenuItemAppeared { 0% { background-position-x: 3px; } 100% { background-position-x: 4px; } } ytm-select.player-speed-settings ~ ytm-menu-item:last-of-type { animation: mobileMenuItemAppeared 1ms linear 0s 1 normal forwards; } @keyframes contextmenuInfoItemAppeared { 0% { background-position-x: 3px; } 100% { background-position-x: 4px; } } .ytp-contextmenu .ytp-menuitem[role="menuitem"] path[d^="M22 34h4V22h-4v12zm2-30C12.95"]{ animation: contextmenuInfoItemAppeared 1ms linear 0s 1 normal forwards; } .with-audio-only-toggle-btn .ytp-contextmenu, .ytp-panel-menu, .ytp-panel { height: 40vh !important; } #confirmDialog794 { z-index:999999 !important; display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 1; /* Sit on top */ left: 0; top: 0; width: 100%; /* Full width */ height: 100%; /* Full height */ overflow: auto; /* Enable scroll if needed */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ } #confirmDialog794 .confirm-box { position:relative; color: black; z-index:999999 !important; background-color: #fefefe; margin: 15% auto; /* 15% from the top and centered */ padding: 20px; border: 1px solid #888; width: 30%; /* Could be more or less, depending on screen size */ box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); } #confirmDialog794 .confirm-buttons { text-align: right; } #confirmDialog794 button { margin-left: 10px; } ` document.head.appendChild(style); }) })();