Skip to content

Instantly share code, notes, and snippets.

@b3x206
Created May 6, 2024 08:23
Show Gist options
  • Save b3x206/3c4c191ce1a705bfaaa13f7db3fb191f to your computer and use it in GitHub Desktop.
Save b3x206/3c4c191ce1a705bfaaa13f7db3fb191f to your computer and use it in GitHub Desktop.
An userscript that shows a volume slider on videos displayed in instagram.
// ==UserScript==
// @name Instagram Video Volume Slider
// @description Adds a volume slider to any video shown.
// @author b3x
// @version 1.0
// @license MIT
// @include https://instagram.com/*
// @include https://*.instagram.com/*
// @namespace https://gist.github.com/b3x206
// @grant GM.getValue
// @grant GM.setValue
// @run-at document-end
// @icon https://upload.wikimedia.org/wikipedia/commons/a/a5/Instagram_icon.png
// ==/UserScript==
"use strict";
// Note(s) :
// 1) The video will not play sound until you have interacted with the video in some way (probably firefox/autoplay rule thing?)
// If this is an actual issue caused by this script instead of the autoplay rule it will be fixed.
// 2) GM.getValue and GM.setValue are async.
// Hmm, totally normal API design. I have no idea why I was this mad. Maybe because there's literally almost NO GreaseMonkey CLASS DOCS?
// This script involved of some suffering.. From firefox devtools lagging and stuttering to instagram's react stuff and to bad documentation.
(function () {
// If these are not defined, define a dummy function
// This is useful in such cases where you want to directly run the script inside the devtools
/*
if (this.GM === undefined) {
this.GM = {};
}
if (this.GM.getValue === undefined) {
this.GM.getValue = function (valueKey, defaultValue) {
console.log(`[dummy::GM_getValue] k:${valueKey}, default:${defaultValue}`);
return Promise.resolve(defaultValue);
};
}
if (this.GM.setValue === undefined) {
this.GM.setValue = function (valueKey, setValue) {
console.log(`[dummy::GM.setValue] k:${valueKey}, v:${setValue}`);
return new Promise(r => setTimeout(r, 10));
};
}
*/
const LastVolumeGMKey = "[insta-volume-slider(dot)js::LastVolume]";
/** @param {any[]} array Array to get the first element. (because no jquery) */
/** @returns {any} The first element in given array. */
function arrayFirst(array) {
if (typeof array !== "object") {
throw new Error("first is not on array.");
}
if (array.length === undefined) {
throw new Error("first is not on array.");
}
try {
return array[0];
} catch {
return null;
}
}
/**
* @summary A function used with event listeners to optimize.
* This function checks for n-ms waits after the event was called and invokes after the given wait.
* @param {*} func
* @param {Number} wait
* @param {Boolean} immediate
* @returns {function():void} The created debounce function. This function is a special function that waits until it's no longer called for 'wait'-ms time and invokes the 'func'.
*/
function debounce(func, wait, immediate) {
// current timeout in "function stack" (lol)
let timeout;
return function () {
let context = this, args = arguments;
let later = function () {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
let callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
};
/**
* Injects the volume button HTML + CSS.
* @param {HTMLVideoElement} videoElement Element to inject to it's button.
*/
async function injectVolumeButton(videoElement) {
// get from the common parent, this is the button to add the on hover and the children that has the slider.
/** @type {HTMLButtonElement} */
const muteButtonElement = arrayFirst(videoElement.parentNode.getElementsByTagName("button"));
if (muteButtonElement === null) {
console.warn(`Given video element ${videoElement} does not have a button tagged element in mutual parent ${videoElement.parentNode}`);
return;
}
// !! TODO !! : Most likely the muteButtonElement.onclick is frozen, (using Object.freeze)
// To avoid this thing, most likely have to create the button from scratch..
// The stupid button overrides all types of muting, this is because react is a mistake.
muteButtonElement.onclick = async (_) => {
let prevGMValue = Number.parseFloat(await GM.getValue(LastVolumeGMKey, 0.0));
if (Number.isNaN(prevGMValue) || !Number.isFinite(prevGMValue)) {
prevGMValue = 0.0;
}
if (prevGMValue < (Number.EPSILON * 4.0)) {
prevGMValue = 1.0;
}
// set volume to 0 if it's higher than 0
// to 1 if it's equal to 0
videoElement.volume = (videoElement.volume < (Number.EPSILON * 4.0)) ? prevGMValue : 0.0;
await GM.setValue(LastVolumeGMKey, videoElement.volume);
};
let lastLoadedVolume = Number.parseFloat(await GM.getValue(LastVolumeGMKey, videoElement.volume));
if (Number.isNaN(lastLoadedVolume) || !Number.isFinite(lastLoadedVolume)) {
console.log(`[insta-volume-slider] last loaded volume isn't a finite number or is NaN : ${lastLoadedVolume}, getValue returned ${GM.getValue(LastVolumeGMKey, videoElement.volume)}`);
lastLoadedVolume = videoElement.volume;
await GM.setValue(LastVolumeGMKey, videoElement.volume);
}
// Add the slider towards the left of the element.
const sliderElement = document.createElement("input");
sliderElement.setAttribute("type", "range");
sliderElement.setAttribute("min", "0");
sliderElement.setAttribute("max", "100");
sliderElement.value = lastLoadedVolume * 100.0;
sliderElement.style.display = "none";
sliderElement.style.marginBottom = "auto";
// sliderElement.ondrag = (_) => {
sliderElement.addEventListener("drag", (_) => {
videoElement.volume = sliderElement.value / 100.0;
});
// };
sliderElement.addEventListener("change", debounce(() => {
videoElement.volume = sliderElement.value / 100.0;
GM.setValue(LastVolumeGMKey, videoElement.volume);
}, 5));
// listen to the mute button hover thing. (this probably can be done wholly using CSS, but i really don't care..)
muteButtonElement.parentNode.addEventListener("pointerover", (_) => {
// show the slider
sliderElement.style.display = "unset";
});
muteButtonElement.parentNode.addEventListener("pointerleave", (e) => {
// hide the slider
sliderElement.style.display = "none";
});
// Append the slider and actually make it position good
// Since i don't know how flex css works i have to style this in a disgusting way..
// Or maybe they are trying their hardest to make their website a miserable experience so you end up using the data collection app on your phone instead.
// (and then they proceed to dmca open source clients)
muteButtonElement.parentNode.appendChild(sliderElement);
muteButtonElement.parentNode.style.flexDirection = "row-reverse";
muteButtonElement.parentNode.style.gap = "10px";
videoElement.volume = lastLoadedVolume;
}
// HTMLVideoElement.volume goes between 0 ~ 1
// maybe use a hashmap?
const injectedElementsList = [];
setInterval(async () => {
const elements = document.getElementsByTagName("video");
for (let i = 0; i < elements.length; i++) {
if (elements[i] !== null && elements[i] !== undefined && !injectedElementsList.includes(elements[i])) {
await injectVolumeButton(elements[i]);
injectedElementsList.push(elements[i]);
}
}
// clean the injectedElementsList (.filter() filters out false values)
injectedElementsList.filter((value) => value !== null && value !== undefined);
}, 300);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment