Greasy Fork is available in English.

YouTube Exit Fullscreen on Video End (Modified)

Exit YouTube fullscreen when a video finishes playing (disabled for playlists)

// ==UserScript==
// @name         YouTube Exit Fullscreen on Video End (Modified)
// @namespace    https://www.youtube.com/
// @version      1.3.2
// @description  Exit YouTube fullscreen when a video finishes playing (disabled for playlists)
// @author       CY Fung
// @match        *://www.youtube.com/*
// @license      MIT
// @icon         https://www.google.com/s2/favicons?domain=youtube.com
// @grant        none
// @run-at       document-start
// ==/UserScript==

(() => {

    /** @type {globalThis.PromiseConstructor} */
    const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.

    let lastPause = 0;
    let lastFullscreenEnded = 0;
    let mutObserver = null;
    let dt0 = Date.now() - 2000;
    const nowFn = () => Date.now() - dt0;

    const check = () => {
        let t = nowFn();
        return t - lastFullscreenEnded < 800 && t - lastPause < 800 && Math.abs(lastFullscreenEnded - lastPause) < 400;
    };

    const mCallback = (mutations) => {
        if (!document.fullscreenElement || !mutations || !mutations.length) return;
        let detected = false;
        let video = null;
        for (const mutation of mutations) {
            video = (mutation || 0).target;
            if (!video) continue;
            const newValue = video.className;
            const oldValue = mutation.oldValue;
            if (newValue.indexOf("ended-mode") >= 0 && oldValue.indexOf("ended-mode") < 0) {
                detected = true;
                break;
            }
        }
        if (detected) {
            if (video && video.classList.contains("ended-mode")) {
                lastFullscreenEnded = nowFn();
                check() && endFullscreen(video);
            }
        }
    };

    const endFullscreen = (elm) => {
        lastPause = 0;
        lastFullscreenEnded = 0;
        if(location.search.includes("list=")) return;
        const movie_player = HTMLElement.prototype.closest.call(elm, '#movie_player');
        const btn = movie_player ? HTMLElement.prototype.querySelector.call(movie_player, '.ytp-fullscreen-button') : null;
        Promise.resolve(btn).then(btn => {
            if (btn) {
                btn.click();
            } else {
                document.exitFullscreen();
            }
        })
    };

    const setup = () => {
        const movie_player = location.pathname.includes("/watch") ? document.getElementById('movie_player') : null;
        if (movie_player) {
            if (!mutObserver) {
                mutObserver = new MutationObserver(mCallback);
            } else {
                mutObserver.disconnect();
                mutObserver.takeRecords();
            }
            mutObserver.observe(movie_player, {
                attributes: true,
                attributeFilter: ['class'],
                attributeOldValue: true
            });
        } else if (mutObserver) {
            mutObserver.disconnect();
            mutObserver.takeRecords();
            mutObserver = null;
        }
    };

    const onPause = (evt) => {
        const target = ((evt || 0).target || 0);
        if (!(target instanceof HTMLVideoElement)) return;
        if (mutObserver === null) return;
        const movie_player = HTMLElement.prototype.closest.call(target, '#movie_player');
        if (!movie_player) return;
        lastPause = nowFn();
        check() && endFullscreen(target);
    };

    document.addEventListener('yt-navigate-finish', setup, false);
    document.addEventListener('spfdone', setup, true);
    document.addEventListener("pause", onPause, true);
    setup();
})();