- // ==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
- //
- // ==/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;
-
- 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);
- })
-
-
- })();
-