Greasy Fork is available in English.

YouTube: Plain Video Player

To force Low Resource

// ==UserScript==
// @name        YouTube: Plain Video Player
// @namespace   UserScripts
// @match       https://www.youtube.com/watch?*
// @exclude     /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
// @grant       none
// @version     0.2.0
// @author      CY Fung
// @license     MIT
// @description To force  Low Resource
// @run-at      document-start
// @inject-into page
// @unwrap
// @license             MIT
// @compatible          chrome
// @compatible          firefox
// @compatible          opera
// @compatible          edge
// @compatible          safari
// @allFrames           true
// ==/UserScript==


(() => {


    let enable = true; // for debug

    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
            }
        }
    }

    const pp = {
        create() {

        },
        setProperties() {

        }
    }
    let mm = new Proxy({}, {
        get(target, prop) {
            // throw SyntaxError();
            // return pp;
            return true
        },
        set(target, prop, val) {
            return true;
        },
        configurable: true,
        enumerable: false
    });

    const cssText = () => `

        .container, #container, #masthead {
        visibility: collapse;
        display: none !important;
        }
        .container *, #container *, #masthead *{
        margin:0 !important;
        padding:0 !important;
        border:0 !important;
        }

        :fullscreen #movie_player{
        position:fixed;
        top:0;
        left:0;
        right:0;
        bottom:0;
        }
        #player.skeleton.flexy #player-wrap[id] {
        width:initial;
        }


        .ytp-next-button, .ytp-miniplayer-button /* , .ytp-size-button */  {
        display: none !important;
        }
        .ytp-pip-button{

            display: inline-block !important;

        }
        
        
    
    `;

    addCSS = 0;
    enable && Object.defineProperty(Object.prototype, '__mixinSet', {
        get() {
            if (!addCSS) {
                addCSS = 1;
                // document.body.appendChild(document.createElement('ytd-watch-flexy'))
                document.head.appendChild(document.createElement('style')).textContent = cssText();
            }
            let rr = window.onerror;
            window.onerror = function () { return true; }
            Promise.resolve().then(() => {
                window.onerror = rr
            })
            throw new Error('');

            return mm;
        },
        set(nv) {
            return true;
        },
        configurable: true,
        enumerable: false
    })



    const addProtoToArr = (parent, key, arr) => {


        let isChildProto = false;
        for (const sr of arr) {
            if (parent[key].prototype instanceof parent[sr]) {
                isChildProto = true;
                break;
            }
        }

        if (isChildProto) return;

        arr = arr.filter(sr => {
            if (parent[sr].prototype instanceof parent[key]) {
                return false;
            }
            return true;
        });

        arr.push(key);

        return arr;


    }


    const getKeys = (_yt_player) => {

        const w = 'Keys';

        let arr = [];

        for (const [k, v] of Object.entries(_yt_player)) {


            const p = typeof v === 'function' ? v.prototype : 0;

            if (p
                && typeof p.isFullscreen === 'function' && p.isFullscreen.length === 0
                && typeof p.getVisibilityState === 'function'
                // && typeof p.getAppState === 'function'
            ) {

                arr = addProtoToArr(_yt_player, k, arr) || arr;

            }

        }





        if (arr.length === 0) {

            console.warn(`Key does not exist. [${w}]`);
        } else {

            return arr;
        }

    }


    const cleanContext = async (win) => {
        const waitFn = requestAnimationFrame; // shall have been binded to window
        try {
            let mx = 16; // MAX TRIAL
            const frameId = 'vanillajs-iframe-v1';
            /** @type {HTMLIFrameElement | null} */
            let frame = document.getElementById(frameId);
            let removeIframeFn = null;
            if (!frame) {
                frame = document.createElement('iframe');
                frame.id = frameId;
                const blobURL = typeof webkitCancelAnimationFrame === 'function' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash
                frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
                let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
                n.appendChild(frame);
                while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
                const root = document.documentElement;
                root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
                if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL));

                removeIframeFn = (setTimeout) => {
                    const removeIframeOnDocumentReady = (e) => {
                        e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
                        win = null;
                        const m = n;
                        n = null;
                        setTimeout(() => m.remove(), 200);
                    }
                    if (document.readyState !== 'loading') {
                        removeIframeOnDocumentReady();
                    } else {
                        win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
                    }
                }
            }
            while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
            const fc = frame.contentWindow;
            if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
            const { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback, getComputedStyle } = fc;
            const res = { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback, getComputedStyle };
            for (let k in res) res[k] = res[k].bind(win); // necessary
            if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
            res.animate = fc.HTMLElement.prototype.animate;
            return res;
        } catch (e) {
            console.warn(e);
            return null;
        }
    };

    cleanContext(window).then(__CONTEXT__ => {
        if (!__CONTEXT__) return null;

        const { setTimeout } = __CONTEXT__;



        const isUrlInEmbed = location.href.includes('.youtube.com/embed/');
        const isAbortSignalSupported = typeof AbortSignal !== "undefined";

        const promiseForTamerTimeout = new Promise(resolve => {
            !isUrlInEmbed && isAbortSignalSupported && document.addEventListener('yt-action', function () {
                setTimeout(resolve, 480);
            }, { capture: true, passive: true, once: true });
            !isUrlInEmbed && isAbortSignalSupported && typeof customElements === "object" && customElements.whenDefined('ytd-app').then(() => {
                setTimeout(resolve, 1200);
            });
            setTimeout(resolve, 3000);
        });

        (async () => {
            const _yt_player = await observablePromise(() => {
                return (((window || 0)._yt_player || 0) || 0);
            }, promiseForTamerTimeout).obtain();


            const keys = getKeys(_yt_player);

            const selectionForJ0 = keys.map(k => {
                const p = _yt_player[k].prototype;
                let v = 0;
                if (!p.getAppState) v -= 900;
                if (!p.getInternalApi) v -= 400;
                if (!p.getApiInterface) v += 100;
                if (!p.getPlayerResponse) v += 100;
                return [k, v];
            });
            const selectionForQT = keys.map(k => {
                const p = _yt_player[k].prototype;
                let v = 0;
                if (!p.getAppState) v -= 900;
                if (!p.getInternalApi) v -= 400;
                if (p.getApiInterface) v += 100;
                if (p.getPlayerResponse) v += 100;
                return [k, v];
            });


            const keyJ0 = selectionForJ0.sort((a, b) => b[1] - a[1])[0][0];
            const keyQT = selectionForQT.sort((a, b) => b[1] - a[1])[0][0];

            if (keyJ0 && keyQT && keyJ0 !== keyQT) {

                let _visibility = null;

                document.addEventListener('fullscreenchange', () => {
                    if (enable) {

                        if (_visibility) _visibility.fullscreen = !!document.fullscreenElement ? 2 : 0;
                    }
                });

                document.addEventListener('enterpictureinpicture', () => {
                    if (enable) {

                        if (_visibility) _visibility.pictureInPicture = true;
                    }

                });
                document.addEventListener('leavepictureinpicture', () => {

                    if (enable) {

                        if (_visibility) _visibility.pictureInPicture = false;
                    }
                });

                // let pp = 0;
                const gkp = _yt_player[keyJ0].prototype;

                gkp.isFullscreen68 = gkp.isFullscreen;
                gkp.isFullscreen = function () {
                    _visibility = this.visibility;
                    // if (this.visibility && !pp) {
                    //     pp = 1;
                    //     console.log(3992, this.visibility)
                    // }
                    return this.isFullscreen68();
                }




            }


            const ytpSizeBtn = await observablePromise(() => {
                return document.querySelector('.ytp-size-button');
            }, promiseForTamerTimeout).obtain();


            ytpSizeBtn.addEventListener('click', (evt) => {
                const player = document.querySelector('#player')
                player.classList.toggle('theater')
                player.classList.toggle('flexy')
                window.dispatchEvent(new Event('resize'));
                // document.body.dispatchEvent(new Event('resize'));
                evt.preventDefault();
                evt.stopImmediatePropagation();
                evt.stopPropagation();
            }, true)




        })();

    });

})();