Instagram Volume Control Enhanced

Adjust default volume & speed on Instagram videos — collapsible panel, mute toggle, % display, property-level volume lock. Full story support.

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 or Violentmonkey 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 Volume Control Enhanced
// @namespace    https://greasyfork.org/fr/scripts/498295-instagram-volume-control-enhanced
// @version      2.2
// @description  Adjust default volume & speed on Instagram videos — collapsible panel, mute toggle, % display, property-level volume lock. Full story support.
// @author       gl4_manu
// @match        https://www.instagram.com/*
// @license      MIT
// @resource     logo https://i.ibb.co/FkksNxfd/sound.png
// @grant        GM_getResourceURL
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    // =========================================================================
    //  CONFIGURATION
    // =========================================================================
    const CONFIG = Object.freeze({
        STORAGE: Object.freeze({
            VOLUME:    'igvc_volume',
            SPEED:     'igvc_speed',
            MUTED:     'igvc_muted',
            COLLAPSED: 'igvc_collapsed',
        }),
        DEFAULTS: Object.freeze({
            VOLUME:    0.05,
            SPEED:     1.0,
            MUTED:     false,
            COLLAPSED: false,
        }),
        SPEEDS:          [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
        DEBOUNCE_MS:     150,
        SWEEP_INTERVAL:  500,
        THEME: Object.freeze({
            BG:         'rgba(18,18,20,0.92)',
            BORDER:     'rgba(255,255,255,0.08)',
            ACCENT:     '#FFD700',
            ACCENT_ALT: '#FF4500',
            TEXT_DIM:   '#888',
            RADIUS:     '14px',
            FONT:       "'SF Pro Display','Segoe UI',system-ui,sans-serif",
        }),
    });

    // =========================================================================
    //  STORAGE
    // =========================================================================
    const Storage = {
        get(key, fallback) {
            try {
                const raw = localStorage.getItem(key);
                return raw === null ? fallback : JSON.parse(raw);
            } catch {
                return fallback;
            }
        },
        set(key, value) {
            try { localStorage.setItem(key, JSON.stringify(value)); } catch { /* quota */ }
        },
    };

    // =========================================================================
    //  STATE
    // =========================================================================
    const State = (() => {
        const s = {
            volume:    Storage.get(CONFIG.STORAGE.VOLUME,    CONFIG.DEFAULTS.VOLUME),
            speed:     Storage.get(CONFIG.STORAGE.SPEED,     CONFIG.DEFAULTS.SPEED),
            muted:     Storage.get(CONFIG.STORAGE.MUTED,     CONFIG.DEFAULTS.MUTED),
            collapsed: Storage.get(CONFIG.STORAGE.COLLAPSED, CONFIG.DEFAULTS.COLLAPSED),
        };

        function persist(key, value) {
            s[key] = value;
            Storage.set(CONFIG.STORAGE[key.toUpperCase()], value);
        }

        return {
            get volume()    { return s.volume; },
            get speed()     { return s.speed; },
            get muted()     { return s.muted; },
            get collapsed() { return s.collapsed; },

            setVolume(v) {
                persist('volume', v);
                if (v > 0 && s.muted) persist('muted', false);
            },
            setSpeed(v)     { persist('speed', v); },
            setMuted(v)     { persist('muted', v); },
            setCollapsed(v) { persist('collapsed', v); },

            reset() {
                persist('volume', CONFIG.DEFAULTS.VOLUME);
                persist('speed',  CONFIG.DEFAULTS.SPEED);
                persist('muted',  CONFIG.DEFAULTS.MUTED);
            },
        };
    })();

    // =========================================================================
    //  NATIVE DESCRIPTORS (captured before Instagram loads)
    // =========================================================================
    const Native = Object.freeze({
        volume: Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'volume'),
        muted:  Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'muted'),
        play:   HTMLMediaElement.prototype.play,
    });

    // =========================================================================
    //  VIDEO CONTROLLER
    // =========================================================================
    const VideoCtrl = (() => {
        /** Write directly via native setter, bypassing our own property lock. */
        function forceApply(video) {
            Native.volume.set.call(video, State.volume);
            Native.muted.set.call(video, State.muted);
            if (Math.abs(video.playbackRate - State.speed) > 0.001) {
                video.playbackRate = State.speed;
            }
        }

        /** Install a property-level lock on a fresh video element. */
        function lock(video) {
            if (video._igvcLocked) return;
            video._igvcLocked = true;

            forceApply(video);
            _defineProps(video);

            const guard = () => requestAnimationFrame(() => forceApply(video));
            const events = ['play', 'playing', 'loadedmetadata', 'loadeddata',
                            'canplay', 'volumechange', 'emptied'];
            events.forEach(e => video.addEventListener(e, guard, true));
        }

        /** Re-assert property descriptors (they can be deleted by framework re-renders). */
        function _defineProps(video) {
            Object.defineProperty(video, 'volume', {
                get: () => State.volume,
                set: () => {},
                configurable: true,
            });
            Object.defineProperty(video, 'muted', {
                get: () => State.muted,
                set: () => {},
                configurable: true,
            });
        }

        /** Re-lock a video that lost its descriptors. */
        function relockIfNeeded(video) {
            const d = Object.getOwnPropertyDescriptor(video, 'volume');
            if (!d || typeof d.get !== 'function') {
                video._igvcLocked = false;
                lock(video);
            } else {
                forceApply(video);
            }
        }

        return {
            lock,
            relockIfNeeded,
            applyToAll() {
                document.querySelectorAll('video').forEach(v =>
                    v._igvcLocked ? relockIfNeeded(v) : lock(v)
                );
            },
        };
    })();

    // =========================================================================
    //  INTERCEPT play() — catches stories before MutationObserver fires
    // =========================================================================
    HTMLMediaElement.prototype.play = function (...args) {
        VideoCtrl.lock(this);
        return Native.play.apply(this, args);
    };

    // =========================================================================
    //  OBSERVERS
    // =========================================================================
    const Observers = (() => {
        function debounce(fn, ms) {
            let t;
            return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
        }

        return {
            startMutation() {
                const handler = debounce((mutations) => {
                    for (const { addedNodes } of mutations) {
                        for (const node of addedNodes) {
                            if (node.nodeType !== Node.ELEMENT_NODE) continue;
                            const videos = node.tagName === 'VIDEO'
                                ? [node]
                                : [...node.querySelectorAll('video')];
                            videos.forEach(v => VideoCtrl.lock(v));
                        }
                    }
                }, CONFIG.DEBOUNCE_MS);

                new MutationObserver(handler).observe(document.body, {
                    childList: true, subtree: true,
                });
            },

            startSweep() {
                setInterval(() => VideoCtrl.applyToAll(), CONFIG.SWEEP_INTERVAL);
            },
        };
    })();

    // =========================================================================
    //  UI
    // =========================================================================
    const UI = (() => {
        const { THEME } = CONFIG;

        // ── Helpers ──────────────────────────────────────────────────────────
        function el(tag, styles = {}, props = {}) {
            const n = document.createElement(tag);
            Object.assign(n.style, styles);
            Object.assign(n, props);
            return n;
        }

        function css(node, styles) { Object.assign(node.style, styles); }

        function injectSliderCSS() {
            if (document.getElementById('igvc-style')) return;
            const s = document.createElement('style');
            s.id = 'igvc-style';
            s.textContent = `
                .igvc-slider{-webkit-appearance:none;appearance:none;width:100%;height:5px;
                  border-radius:4px;outline:none;cursor:pointer;
                  background:linear-gradient(to right,#FFD700,#FF4500)}
                .igvc-slider::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;
                  border-radius:50%;background:#FFD700;
                  box-shadow:0 0 4px rgba(255,215,0,.6);cursor:pointer;transition:transform .15s}
                .igvc-slider::-webkit-slider-thumb:hover{transform:scale(1.3)}
                .igvc-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
                  background:#FFD700;cursor:pointer;border:none}
            `;
            document.head.appendChild(s);
        }

        // ── Draggable wrapper ─────────────────────────────────────────────────
        function makeDraggable(handle, target) {
            let ox = 0, oy = 0, startX = 0, startY = 0, dragging = false;

            handle.addEventListener('mousedown', e => {
                if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') return;
                dragging = true;
                startX = e.clientX - ox;
                startY = e.clientY - oy;
                e.preventDefault();
            });

            document.addEventListener('mousemove', e => {
                if (!dragging) return;
                ox = e.clientX - startX;
                oy = e.clientY - startY;
                css(target, { transform: `translate(${ox}px,${oy}px)` });
            });

            document.addEventListener('mouseup', () => { dragging = false; });
        }

        // ── Build ─────────────────────────────────────────────────────────────
        function build() {
            injectSliderCSS();

            // Root wrapper (fixed anchor)
            const wrap = el('div', {
                position:   'fixed',
                bottom:     '90px',
                right:      '14px',
                zIndex:     '2147483647',
                fontFamily: THEME.FONT,
                userSelect: 'none',
            });

            // Panel
            const panel = el('div', {
                background:     THEME.BG,
                border:         `1px solid ${THEME.BORDER}`,
                borderRadius:   THEME.RADIUS,
                boxShadow:      '0 8px 32px rgba(0,0,0,.45)',
                backdropFilter: 'blur(12px)',
                padding:        '10px 14px',
                minWidth:       '260px',
                overflow:       'hidden',
                transition:     'all .25s ease',
            });

            // Header
            const header = el('div', {
                display:        'flex',
                alignItems:     'center',
                justifyContent: 'space-between',
                cursor:         'move',
                marginBottom:   '10px',
            });

            const titleWrap = el('div', { display: 'flex', alignItems: 'center', gap: '7px' });

            const logo = el('img', { width: '18px', height: '18px', objectFit: 'contain' });
            logo.src = GM_getResourceURL('logo');

            const title = el('span', {
                fontSize:      '12px',
                fontWeight:    '600',
                letterSpacing: '0.08em',
                textTransform: 'uppercase',
                color:         THEME.TEXT_DIM,
            });
            title.textContent = 'Video Controls';
            titleWrap.append(logo, title);

            const toggleBtn = el('button', {
                background:  'none',
                border:      'none',
                color:       THEME.ACCENT,
                fontSize:    '16px',
                cursor:      'pointer',
                padding:     '0',
                lineHeight:  '1',
                transition:  'transform .2s',
            });

            header.append(titleWrap, toggleBtn);

            // Body
            const body = el('div', {
                overflow:   'hidden',
                transition: 'max-height .3s ease, opacity .3s ease',
            });

            const applyCollapse = () => {
                body.style.maxHeight = State.collapsed ? '0'     : '200px';
                body.style.opacity   = State.collapsed ? '0'     : '1';
                toggleBtn.textContent = State.collapsed ? '+' : '-';
            };
            applyCollapse();

            header.addEventListener('click', e => {
                if (e.target === toggleBtn || e.target === header || e.target === titleWrap || titleWrap.contains(e.target)) {
                    State.setCollapsed(!State.collapsed);
                    applyCollapse();
                }
            });

            // ── Volume row ──────────────────────────────────────────────────
            const volRow = el('div', {
                display:       'flex',
                alignItems:    'center',
                gap:           '8px',
                marginBottom:  '10px',
            });

            const muteBtn = el('button', {
                background:  'none',
                border:      `1px solid ${THEME.BORDER}`,
                borderRadius:'6px',
                color:       THEME.ACCENT,
                fontSize:    '16px',
                cursor:      'pointer',
                padding:     '2px 6px',
                lineHeight:  '1.4',
                flexShrink:  '0',
            });

            const syncMuteBtn = () => {
                muteBtn.textContent = State.muted ? '🔇' : '🔊';
                muteBtn.title       = State.muted ? 'Unmute' : 'Mute';
            };
            syncMuteBtn();

            muteBtn.addEventListener('click', () => {
                State.setMuted(!State.muted);
                syncMuteBtn();
                VideoCtrl.applyToAll();
            });

            const volSlider = document.createElement('input');
            volSlider.className = 'igvc-slider';
            Object.assign(volSlider, { type: 'range', min: '0', max: '1', step: '0.01', value: State.volume });

            const volBadge = el('span', {
                fontSize:   '11px',
                fontWeight: '700',
                color:      THEME.ACCENT,
                minWidth:   '34px',
                textAlign:  'right',
                flexShrink: '0',
            });

            const syncBadge = v => { volBadge.textContent = `${Math.round(v * 100)}%`; };
            syncBadge(State.volume);

            volSlider.addEventListener('input', () => {
                const v = parseFloat(volSlider.value);
                State.setVolume(v);
                syncBadge(v);
                syncMuteBtn();   // mute may have been auto-cleared
                VideoCtrl.applyToAll();
            });

            volRow.append(muteBtn, volSlider, volBadge);

            // ── Speed row ───────────────────────────────────────────────────
            const speedRow = el('div', {
                display:      'flex',
                alignItems:   'center',
                gap:          '8px',
                marginBottom: '10px',
            });

            const speedLabel = el('span', { fontSize: '11px', color: THEME.TEXT_DIM, flexShrink: '0' });
            speedLabel.textContent = '⏩ Speed';

            const speedSelect = el('select', {
                background:   '#1a1a1c',
                color:        THEME.ACCENT,
                border:       `1px solid ${THEME.BORDER}`,
                borderRadius: '6px',
                padding:      '3px 6px',
                outline:      'none',
                cursor:       'pointer',
                fontSize:     '12px',
                fontWeight:   '600',
                flex:         '1',
                colorScheme:  'dark',
            });

            CONFIG.SPEEDS.forEach(s => {
                const opt = document.createElement('option');
                opt.value = s;
                opt.textContent = `${s}×`;
                if (s === State.speed) opt.selected = true;
                speedSelect.appendChild(opt);
            });

            speedSelect.addEventListener('change', () => {
                State.setSpeed(parseFloat(speedSelect.value));
                VideoCtrl.applyToAll();
            });

            speedRow.append(speedLabel, speedSelect);

            // ── Reset button ────────────────────────────────────────────────
            const resetBtn = el('button', {
                width:        '100%',
                background:   'rgba(255,215,0,.1)',
                color:        THEME.ACCENT,
                border:       '1px solid rgba(255,215,0,.3)',
                borderRadius: '7px',
                padding:      '5px 0',
                cursor:       'pointer',
                fontSize:     '12px',
                fontWeight:   '600',
                letterSpacing:'0.05em',
                transition:   'background .2s,color .2s',
            });
            resetBtn.textContent = 'Reset to defaults';

            resetBtn.addEventListener('mouseover', () =>
                css(resetBtn, { background: THEME.ACCENT_ALT, color: '#fff', border: `1px solid ${THEME.ACCENT_ALT}` }));
            resetBtn.addEventListener('mouseout', () =>
                css(resetBtn, { background: 'rgba(255,215,0,.1)', color: THEME.ACCENT, border: '1px solid rgba(255,215,0,.3)' }));

            resetBtn.addEventListener('click', () => {
                State.reset();
                volSlider.value    = CONFIG.DEFAULTS.VOLUME;
                speedSelect.value  = CONFIG.DEFAULTS.SPEED;
                syncBadge(CONFIG.DEFAULTS.VOLUME);
                syncMuteBtn();
                VideoCtrl.applyToAll();
            });

            // ── Assemble ────────────────────────────────────────────────────
            body.append(volRow, speedRow, resetBtn);
            panel.append(header, body);
            wrap.appendChild(panel);
            document.body.appendChild(wrap);

            makeDraggable(header, panel);
        }

        return { build };
    })();

    // =========================================================================
    //  INIT
    // =========================================================================
    function init() {
        UI.build();
        VideoCtrl.applyToAll();
        Observers.startMutation();
        Observers.startSweep();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();