Instagram Video Controls with Persistent UI and Volume

Adds native controls to Instagram videos + draggable UI (remembers volume/mute/position)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Instagram Video Controls with Persistent UI and Volume
// @description  Adds native controls to Instagram videos + draggable UI (remembers volume/mute/position)
// @description  based on the script https://greasyfork.org/en/scripts/536598-instagram-video-controls
// @namespace    http://tampermonkey.net/
// @version      0.0.8
// @author       manaka
// @match        *://*.instagram.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    const POS_KEY = 'igvc-ui-position';
    const SETTINGS_KEY = 'igvc-settings';

    const settings = loadSettings();

    const debounce = (func, wait) => {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    };

    function loadSettings() {
        const saved = localStorage.getItem(SETTINGS_KEY);
        if (saved) {
            try {
                const parsed = JSON.parse(saved);
                return {
                    volume: typeof parsed.volume === 'number' ? parsed.volume : 0.5,
                    muted: typeof parsed.muted === 'boolean' ? parsed.muted : false
                };
            } catch {
                return { volume: 0.5, muted: false };
            }
        }
        return { volume: 0.5, muted: false };
    }

    function saveSettings() {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
    }

    const createControlUI = () => {
        const container = document.createElement('div');
        container.id = 'igvc-floating-ui';
        container.style.position = 'fixed';
        container.style.top = '10px';
        container.style.right = '10px';
        container.style.zIndex = '9999';
        container.style.background = 'rgba(0,0,0,0.7)';
        container.style.color = '#fff';
        container.style.padding = '6px 10px';
        container.style.borderRadius = '8px';
        container.style.fontSize = '12px';
        container.style.fontFamily = 'sans-serif';
        container.style.cursor = 'move';
        container.style.userSelect = 'none';

        const savedPos = localStorage.getItem(POS_KEY);
        if (savedPos) {
            const { top, left } = JSON.parse(savedPos);
            container.style.top = top;
            container.style.left = left;
            container.style.right = 'auto';
        }

        const volumeLabel = document.createElement('label');
        volumeLabel.textContent = 'Volume: ';
        volumeLabel.style.marginRight = '4px';

        const volumeSlider = document.createElement('input');
        volumeSlider.type = 'range';
        volumeSlider.min = '0';
        volumeSlider.max = '1';
        volumeSlider.step = '0.01';
        volumeSlider.value = settings.volume;
        volumeSlider.style.verticalAlign = 'middle';

        const muteLabel = document.createElement('label');
        muteLabel.style.marginLeft = '10px';
        muteLabel.textContent = 'Mute';

        const muteCheckbox = document.createElement('input');
        muteCheckbox.type = 'checkbox';
        muteCheckbox.checked = settings.muted;
        muteCheckbox.style.marginLeft = '4px';

        volumeSlider.addEventListener('input', () => {
            settings.volume = parseFloat(volumeSlider.value);
            settings.muted = false;
            muteCheckbox.checked = false;
            saveSettings();
            applySettingsToAll();
        });

        muteCheckbox.addEventListener('change', () => {
            settings.muted = muteCheckbox.checked;
            saveSettings();
            applySettingsToAll();
        });

        container.appendChild(volumeLabel);
        container.appendChild(volumeSlider);
        container.appendChild(muteLabel);
        container.appendChild(muteCheckbox);
        document.body.appendChild(container);

        makeDraggable(container, volumeSlider);
    };

    const makeDraggable = (element, ignoreElement) => {
        let offsetX, offsetY, isDragging = false;

        element.addEventListener('mousedown', (e) => {
            if (ignoreElement.contains(e.target)) return; // prevent drag on slider
            isDragging = true;
            offsetX = e.clientX - element.getBoundingClientRect().left;
            offsetY = e.clientY - element.getBoundingClientRect().top;
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });

        const onMouseMove = (e) => {
            if (!isDragging) return;
            const left = `${e.clientX - offsetX}px`;
            const top = `${e.clientY - offsetY}px`;
            element.style.left = left;
            element.style.top = top;
            element.style.right = 'auto';
        };

        const onMouseUp = () => {
            if (!isDragging) return;
            isDragging = false;
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
            localStorage.setItem(POS_KEY, JSON.stringify({
                top: element.style.top,
                left: element.style.left
            }));
        };
    };

    const applySettings = (video) => {
        video.muted = settings.muted;
        video.volume = settings.volume;
    };

    const monitorVideo = (video) => {
        if (video.dataset.igvcProcessed) return;
        video.dataset.igvcProcessed = 'true';

        video.setAttribute('controls', 'true');
        video.style.position = 'relative';
        video.style.zIndex = '1';

        if (location.pathname.startsWith('/stories/')) {
            video.style.height = 'calc(100% - 62px)';
        }

        applySettings(video);

        video.addEventListener('volumechange', () => {
            applySettings(video);
        });

        const originalPlay = video.play;
        video.play = function () {
            const result = originalPlay.apply(this, arguments);
            result.then(() => applySettings(this)).catch(() => {});
            return result;
        };
    };

    const applySettingsToAll = () => {
        document.querySelectorAll('video').forEach(applySettings);
    };

    const applyToAllVideos = () => {
        document.querySelectorAll('video').forEach(monitorVideo);
    };

    const init = () => {
        createControlUI();
        applyToAllVideos();
        const observer = new MutationObserver(debounce(applyToAllVideos, 300));
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    };

    init();
})();