Video Controls for Instagram

Instagram video controls with keyboard shortcuts and persistent unmute, originally by FXZFun (fxzfun.com).

// ==UserScript==
// @name         Video Controls for Instagram
// @namespace    https://github.com/appel/userscripts
// @version      1.1
// @description  Instagram video controls with keyboard shortcuts and persistent unmute, originally by FXZFun (fxzfun.com).
// @author       Ap
// @match        https://www.instagram.com/
// @match        https://www.instagram.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=instagram.com
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    if (!document.head.innerHTML.includes("::-webkit-media-controls")) {
        GM_addStyle(`
            ::-webkit-media-controls {
                z-index: 999999;
                position: relative;
            }
            video::-webkit-media-controls {
                opacity: 1;
            }
            video:hover::-webkit-media-controls {
                opacity: 1;
            }
        `);
    }

    function attachPersistentUnmute(video) {
        if (video.dataset.stickyUnmuteListenerAdded === "true") return;
        video.addEventListener('volumechange', () => {
            if (!video.muted) {
                video.dataset.stickyUnmute = "true";
            }
            if (video.muted && video.dataset.stickyUnmute === "true") {
                setTimeout(() => { video.muted = false; }, 0);
            }
        });
        video.dataset.stickyUnmuteListenerAdded = "true";
    }

    function updateVideo(video) {
        video.controls = "controls";
        attachPersistentUnmute(video);
        if (video.closest('article') !== null) {
            video.setAttribute("loop", "true");
        } else {
            video.removeAttribute("loop");
        }
    }

    document.querySelectorAll("video").forEach(updateVideo);

    const observer = new MutationObserver((mutationsList) => {
        for (let mutation of mutationsList) {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.tagName === "VIDEO") {
                            updateVideo(node);
                        } else {
                            node.querySelectorAll("video").forEach(updateVideo);
                        }
                    }
                });
            }
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    function getClosestVideo() {
        let closestVideo = null;
        let closestDistance = Infinity;
        document.querySelectorAll("video").forEach(video => {
            const rect = video.getBoundingClientRect();
            const centerX = rect.left + rect.width / 2;
            const centerY = rect.top + rect.height / 2;
            const distance = Math.hypot(window.innerWidth / 2 - centerX, window.innerHeight / 2 - centerY);
            if (distance < closestDistance) {
                closestDistance = distance;
                closestVideo = video;
            }
        });
        return closestVideo;
    }

    document.addEventListener("keydown", function (event) {
        const activeEl = document.activeElement;
        if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) {
            return;
        }
        const video = getClosestVideo();
        if (!video) return;
        switch (event.key) {
            case "f":
                document.fullscreenElement ? document.exitFullscreen() : video.requestFullscreen();
                break;
            case "m":
                if (video.muted) {
                    video.muted = false;
                    video.dataset.stickyUnmute = "true";
                } else {
                    video.muted = true;
                    video.dataset.stickyUnmute = "";
                }
                break;
            case "k":
                video.paused ? video.play() : video.pause();
                setTimeout(() => {
                    video.muted = false;
                    video.dataset.stickyUnmute = "true";
                }, 100);
                break;
            case "j":
                video.currentTime = Math.max(0, video.currentTime - 10);
                break;
            case "l":
                video.currentTime = Math.min(video.duration, video.currentTime + 10);
                break;
            case ",":
                video.pause();
                video.currentTime = Math.max(0, video.currentTime - (1 / 30));
                break;
            case ".":
                video.pause();
                video.currentTime = Math.min(video.duration, video.currentTime + (1 / 30));
                break;
        }
    });
})();