Greasy Fork is available in English.

YouTube Loop Controls and Background Play For Mobile and Desktop

Adds YouTube loop controls, prevents pausing and allows background play

// ==UserScript==
// @name        YouTube Loop Controls and Background Play For Mobile and Desktop
// @namespace   ytcontrolandbg
// @match       *://*.youtube.com/*
// @grant       none
// @version     1.39
// @run-at      document-end
// @author      ab498
// @description Adds YouTube loop controls, prevents pausing and allows background play
// ==/UserScript==



let callback = () => {
    insertControls();
    attachListeners();

    setInterval(() => {
        safe(() => {
            let video = document.querySelector(".html5-main-video");
            if (!video) return;
            video.loop = localStorage.getItem('extensionLoop') == 'true';
            loopBtnStateUpdate();
        });
    }, 100);

}

function attachListeners() {

    try {

        let pipBtn = document.querySelector(".pip-btn");
        pipBtn.addEventListener("click", () => safe(() => callPIP()));

        let loopBtn = document.querySelector(".looper-btn");
        loopBtn.addEventListener("click", () => safe(() => callLoop()));

        let speedUpBtn = document.querySelector(".speed-up-btn");
        speedUpBtn.addEventListener("click", () => safe(() => callSpeedUp()));

        let speedDownBtn = document.querySelector(".speed-down-btn");
        speedDownBtn.addEventListener("click", () => safe(() => callSpeedDown()));

        let speedResetBtn = document.querySelector(".speed-reset-btn");
        speedResetBtn.addEventListener("click", () => safe(() => callSpeedReset()));


        let isDragging = false;
        let dragger = document.querySelector(".dragger-btn");
        dragger.addEventListener("mousedown", () => isDragging = true);
        document.addEventListener("mouseup", () => isDragging = false);

        dragger.addEventListener("touchstart", () => isDragging = true);
        document.addEventListener("touchend", () => isDragging = false);

        document.addEventListener("touchmove", (e) => {
            if (isDragging) {
                e.preventDefault();
                safe(() => callDrag(e.targetTouches[0]));
            }
        }, { passive: false });

        document.addEventListener("mousemove", (e) => {
            if (isDragging) {
                e.preventDefault();
                safe(() => callDrag(e));
            }
        }, { passive: false });

        dragger.addEventListener("click", (e) => {
            let disp = "";
            if (pipBtn.style.display != "none")
                disp = "none";
            pipBtn.style.display = disp;
            loopBtn.style.display = disp;
            speedUpBtn.style.display = disp;
            speedDownBtn.style.display = disp;
            speedResetBtn.style.display = disp;
        });

        let controlsEl = document.querySelector("#altern-controls");
        controlsEl.style.left = (localStorage.getItem('extensionDrag')?.split(",")[0] + "px") || "0%";
        controlsEl.style.top = (localStorage.getItem('extensionDrag')?.split(",")[1] + "px") || "50%";


    } catch (error) {
        console.warn(error);
    }
}

function insertControls() {
    document.body.insertAdjacentHTML(
        "afterbegin",
        `

<div id="snackbar" class="mm-snackbar">Some text some message..</div>
<div id="altern-controls" class="">
    <div class="dragger-btn" style="padding: 5px;">☰</div>
    <div class="altern-btn looper-btn">Loop</div>
    <div class="altern-btn pip-btn">Open PIP</div>
    <div class="altern-btn speed-down-btn">«</div>
    <div class="altern-btn speed-reset-btn">↺</div>
    <div class="altern-btn speed-up-btn">»</div>
</div>
<style>
    #altern-controls {
        padding: 5px;
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 5px;
        position: fixed;
        top: 50%;
        left: 0px;
        margin: 5px;
        background-color: #ffffff;
        color: #000000;
        border-radius: 10px;
        z-index: 99999;
    }

    .altern-btn {
        display: -webkit-box;
        -webkit-line-clamp: 3;
        -webkit-box-orient: vertical;
        overflow: hidden;
        text-overflow: hidden;
        text-align: center;
        min-width: 20px;
        border: 1px solid #d99dff;
        padding: 5px;
        border-radius: 10px;
        background-color: #d99dff;
        cursor: pointer;
    }

    .altern-btn:hover {
        background-color: #c972ff;
    }

    .mm-wrap {
        display: flex;
        justify-content: center;
    }

    .mm-btn {
        margin: 5px;
        padding: 5px 10px;
        border-radius: 999px;
        background-color: #222;
        color: white;
        cursor: pointer;
        border: none;
        outline: none;
    }

    /* The snackbar - position it at the bottom and in the middle of the screen */
    #snackbar {
        visibility: hidden;
        /* Hidden by default. Visible on click */
        min-width: 150px;
        /* Set a default minimum width */
        background-color: #333;
        /* Black background color */
        color: #fff;
        /* White text color */
        text-align: center;
        /* Centered text */
        border-radius: 2px;
        /* Rounded borders */
        padding: 10px;
        /* Padding */
        position: fixed;
        /* Sit on top of the screen */
        z-index: 99999;
        /* Add a z-index if needed */
        left: 50%;
        /* Center the snackbar */
        top: 30px;
        /* 30px from the bottom */
        transform: translateX(-50%);
        border-radius: 5px;
    }

    /* Show the snackbar when clicking on a button (class added with JavaScript) */
    #snackbar.show {
        visibility: visible;
        /* Show the snackbar */
    }
</style>


        `
    );
}
async function callPIP() {

    let isPiPSupported = "pictureInPictureEnabled" in document,
        isPiPEnabled = document.pictureInPictureEnabled;

    if (!isPiPSupported)
        return showToast("Your browser does not support PIP");
    let video = document.querySelector(".html5-main-video");
    let toggleBtn = document.querySelector(".pip-btn");
    toggleBtn.disabled = true; //disable btn ,so that no multiple request are made
    try {
        if (video !== document.pictureInPictureElement) {
            await video.requestPictureInPicture();
            toggleBtn.textContent = "Exit PIP";
        }
        // If already playing exit mide
        else {
            await document.exitPictureInPicture();
            toggleBtn.textContent = "Open PIP";
        }
    } catch (error) {
        console.log(error);
    } finally {
        toggleBtn.disabled = false; //enable toggle at last
    }
}

function callLoop() {

    let video = document.querySelector(".html5-main-video");
    video.loop = !video.loop;
    localStorage.extensionLoop = video.loop;
    showToast(video.loop ? "Loop Turned On" : "Loop Turned Off", video.loop ? "#00aa00" : "");
    loopBtnStateUpdate();

}


function loopBtnStateUpdate() {
    if (!document.querySelector(".looper-btn")) return;
    if (localStorage.extensionLoop == "true") {
        document.querySelector(".looper-btn").textContent = "UnLoop";
    } else {
        document.querySelector(".looper-btn").textContent = "Loop";
    }
}
function showToast(msg, bgCol) {
    var x = document.querySelector(".mm-snackbar");
    x.classList.add("show");
    x.textContent = msg;
    if (bgCol) x.style.backgroundColor = bgCol;
    else x.style.backgroundColor = "";
    if (window.toastinterval) clearTimeout(window.toastinterval);
    window.toastinterval = setTimeout(function () {
        x.classList.remove("show");
    }, 2000);
}


let changeQuantity = 0.25;
function callSpeedUp() {
    let video = document.querySelector(".html5-main-video");
    video.playbackRate += changeQuantity;
    showToast("Playback Speed Increased To " + video.playbackRate, "#00aa00");
}
function callSpeedDown() {
    let video = document.querySelector(".html5-main-video");
    video.playbackRate -= changeQuantity;
    showToast("Playback Speed Decreased To " + video.playbackRate, "#00aa00");
}

function callSpeedReset() {
    let video = document.querySelector(".html5-main-video");
    video.playbackRate = 1;
    showToast("Playback Speed Reset To " + video.playbackRate, "#00aa00");
}

function callDrag(event) {
    let controlsEl = document.querySelector("#altern-controls");
    let dragger = document.querySelector(".dragger-btn");
    let [newX, newY] = [event.clientX, event.clientY];
    let [targetX, targetY] = [newX - dragger.offsetWidth, newY - dragger.offsetHeight];
    if (newX - dragger.offsetWidth < 0) targetX = 0;
    if (newY - dragger.offsetHeight < 0) targetY = 0;
    if (newX + controlsEl.offsetWidth - dragger.offsetWidth > window.innerWidth) targetX = window.innerWidth - controlsEl.offsetWidth;
    if (newY + controlsEl.offsetHeight - dragger.offsetHeight > window.innerHeight) targetY = window.innerHeight - controlsEl.offsetHeight;
    controlsEl.style.left = targetX + "px";
    controlsEl.style.top = targetY + "px";
    localStorage.extensionDrag = newX + "," + newY;
    // showToast("Dragged To " + controlsEl.style.left + " " + controlsEl.style.top, "#00aa00");
}

document.addEventListener("visibilitychange", (e) => e.stopImmediatePropagation(), true);
Object.defineProperties(document, { hidden: { value: false }, visibilityState: { value: "visible" } });

const lactRefreshInterval = 5 * 60 * 1000; // 5 mins
const initialLactDelay = 1000;

// _lact last active maybe
function waitForYoutubeLactInit(delay = initialLactDelay) {
    if (window.hasOwnProperty("_lact")) {
        window.setInterval(() => {
            window._lact = Date.now();
        }, lactRefreshInterval);
    } else {
        window.setTimeout(() => waitForYoutubeLactInit(delay * 2), delay);
    }
}

waitForYoutubeLactInit();

function safe(fn) {
    try {
        fn();
    } catch (error) {
        console.warn(error);
    }
}


if (document.readyState === "complete") {
    callback();
} else {
    window.addEventListener("load", callback);
}