Adjust default volume & speed on Instagram videos — collapsible panel, mute toggle, % display, property-level volume lock. Full story support.
// ==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();
}
})();