// ==UserScript==
// @name YouTube: Audio Only
// @description No Video Streaming
// @namespace UserScript
// @version 0.4.1
// @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
//
// ==/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 = `
<div id="confirmDialog794" class="dialog-style" style="display: block;">
<div class="confirm-box">
<p>${message}</p>
<div class="confirm-buttons">
<button id="confirmBtn">Confirm</button>
<button id="cancelBtn">Cancel</button>
</div>
</div>
</div>
`;
// 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;
let cv = 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;
}
if (cv) {
cv.resolve();
cv = 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;
let clickLockFn = null;
let clickTarget = 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; clickTarget = this;
// 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 qm = new PromiseExternal();
// document.addEventListener('DOMContentLoaded', (evt) => {
// const mo = new MutationObserver((mutations)=>{
// console.log(5899, mutations)
// });
// mo.observe(document, {subtree: true, childList: true})
// })
// window.addEventListener('onReady', (evt) => {
// console.log(6811)
// }, true);
// window.addEventListener('localmediachange', (evt) => {
// console.log(6812)
// }, true);
// window.addEventListener('onVideoDataChange', (evt) => {
// console.log(6813)
// }, true);
window.addEventListener('state-navigateend', async (evt) => {
// console.log(5910)
if (clickLockFn && clickTarget) {
let a = HTMLElement.prototype.querySelector.call(clickTarget, '.video-stream.html5-main-video');
if (a) {
if (a.muted === true && a.paused === true) clickLockFn.call(clickTarget, { type: 'click', target: clickTarget })
// console.log(588, clickLockFn, a.muted)
if (a.__spfgs__ !== true && a.paused === true) {
clickLockFn.call(clickTarget, { type: 'click', target: clickTarget })
}
}
}
qm.resolve();
}, true);
// window.addEventListener('onStateChange', (evt) => {
// console.log(6815)
// }, true);
let px = 0;
let fa = 0;
let ez = 0;
async function delayRun() {
await qm.then();
if (ez) return;
ez = 1;
let qq = 0;
const { q, a } = await observablePromise(() => {
let q = document.querySelector('#movie_player');
if (!q) return;
let a = document.querySelector('.video-stream.html5-main-video');
if (!a) return;
return { q, a };
}).obtain();
if (a.muted) return;
if (a.muted === false && a.readyState === 0 && a.networkState === 2) {
} else {
return;
}
let cid = setInterval(() => {
if (a.muted === false && a.readyState === 0 && a.networkState === 2) {
} else {
clearInterval(cid);
}
if (a.paused !== true) return;
clearInterval(cid);
if (qq) return;
qq = 1;
if (document.querySelector('.player-controls-content')) return;
if (fa !== 1) return;
if (a.paused === true && a.muted === false && a.readyState === 0 && a.networkState === 2) document.querySelector('#movie_player').click();
// console.log(a.paused)
// console.log(7710)
}, 10)
}
document.addEventListener('durationchange', (evt) => {
// console.log(5911)
if (evt.target.readyState !== 1) {
fa = 1;
if (px) clearTimeout(px);
px = setTimeout_(delayRun, 100);
} 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);
})
})();