// ==UserScript==
// @name Instagram Video Controls - Enhanced State Management
// @namespace http://tampermonkey.net/
// @version 2.7
// @description Video controls for Instagram with persistent state and improved muting prevention
// @match https://www.instagram.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Immediate execution to handle any pre-existing videos
const handleExistingVideos = () => {
document.querySelectorAll('video').forEach(video => {
video.muted = false;
video.removeAttribute('muted');
// Override muted property for existing videos
Object.defineProperty(video, 'muted', {
configurable: true,
get: function() {
return this._muted || false;
},
set: function(value) {
// Only allow unmuting
if (value === false) {
this._muted = false;
}
return false;
}
});
// Prevent muted attribute
const originalSetAttribute = video.setAttribute;
video.setAttribute = function(name, value) {
if (name === 'muted') return;
originalSetAttribute.call(this, name, value);
};
});
};
// Run immediately for any existing videos
handleExistingVideos();
// Early muting prevention for new videos
const preventMuting = () => {
const originalCreateElement = document.createElement;
document.createElement = function(tag) {
const element = originalCreateElement.call(document, tag);
if (tag.toLowerCase() === 'video') {
// Immediately set muted to false
element.muted = false;
// Override muted property
Object.defineProperty(element, 'muted', {
configurable: true,
get: function() {
return this._muted || false;
},
set: function(value) {
// Only allow unmuting
if (value === false) {
this._muted = false;
}
return false;
}
});
// Prevent muted attribute
const originalSetAttribute = element.setAttribute;
element.setAttribute = function(name, value) {
if (name === 'muted') return;
originalSetAttribute.call(this, name, value);
};
}
return element;
};
};
// Additional observer to catch dynamically added video elements
const observeVideoElements = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeName === 'VIDEO') {
node.muted = false;
node.removeAttribute('muted');
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
};
// Initialize muting prevention immediately
preventMuting();
observeVideoElements();
// UI Constants
const UI = {
colors: {
primary: '#0095f6',
background: 'rgba(0,0,0,0.8)',
text: '#ffffff',
hover: 'rgba(255,255,255,0.1)'
},
sizes: {
buttonSize: '32px',
fontSize: {
normal: '14px',
large: '20px'
},
controlHeight: '48px',
timelineHeight: '3px',
timelineActiveHeight: '5px'
},
styles: {
button: {
background: 'none',
border: 'none',
color: '#ffffff',
cursor: 'pointer',
padding: '0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'opacity 0.2s'
},
container: {
display: 'flex',
position: 'relative'
}
}
};
// Video State Management
const VideoState = {
preferences: {
volume: 1,
speed: 1,
backgroundPlay: false,
isMuted: false,
lastUpdate: Date.now()
},
activeControlInstances: new Set(),
saveTimeout: null,
lastKnownGoodState: null,
initialize() {
try {
// Load saved preferences
const saved = localStorage.getItem('igVideoPreferences');
if (saved) {
const parsedPrefs = JSON.parse(saved);
// Validate and merge saved preferences
this.preferences = {
volume: this.isValidVolume(parsedPrefs.volume) ? parsedPrefs.volume : this.preferences.volume,
speed: this.isValidSpeed(parsedPrefs.speed) ? parsedPrefs.speed : this.preferences.speed,
backgroundPlay: typeof parsedPrefs.backgroundPlay === 'boolean' ? parsedPrefs.backgroundPlay : this.preferences.backgroundPlay,
isMuted: typeof parsedPrefs.isMuted === 'boolean' ? parsedPrefs.isMuted : this.preferences.isMuted,
lastUpdate: Date.now()
};
this.lastKnownGoodState = { ...this.preferences };
}
// Set up observers
this.setupLazyLoadDetection();
this.setupIntersectionObserver();
window.addEventListener('igVideoStateChange', this.broadcastStateChange.bind(this));
window.addEventListener('beforeunload', () => {
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
this.savePreferences();
}
});
} catch (e) {
console.error('Error initializing video state:', e);
this.savePreferences();
}
},
setupLazyLoadDetection() {
// Watch for new video elements being added
const videoObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeName === 'VIDEO') {
// Immediately prevent muting for new videos
this.preventMutingForVideo(node);
// Force state application after a short delay
setTimeout(() => {
this.enforceStateOnVideo(node);
}, 50); // Small delay to ensure video is initialized
} else if (node.querySelectorAll) {
// Check for videos in added containers
node.querySelectorAll('video').forEach(video => {
this.preventMutingForVideo(video);
setTimeout(() => {
this.enforceStateOnVideo(video);
}, 50);
});
}
});
}
}
});
videoObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src', 'style']
});
},
setupIntersectionObserver() {
// Watch for videos coming into view
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.target.nodeName === 'VIDEO') {
// When a video comes into view, ensure state is correct
this.enforceStateOnVideo(entry.target);
}
});
}, {
rootMargin: '50px 0px', // Start checking slightly before video comes into view
threshold: 0.1
});
// Observe all existing videos
document.querySelectorAll('video').forEach(video => {
intersectionObserver.observe(video);
});
// Watch for new videos to observe
const videoObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeName === 'VIDEO') {
intersectionObserver.observe(node);
} else if (node.querySelectorAll) {
node.querySelectorAll('video').forEach(video => {
intersectionObserver.observe(video);
});
}
});
});
});
videoObserver.observe(document.body, {
childList: true,
subtree: true
});
},
preventMutingForVideo(video) {
// Immediate unmuting
video.muted = false;
video.removeAttribute('muted');
// Override muted property
Object.defineProperty(video, 'muted', {
configurable: true,
get: function() {
return this._muted || false;
},
set: function(value) {
// Only allow unmuting
if (value === false) {
this._muted = false;
}
return false;
}
});
// Prevent muted attribute
const originalSetAttribute = video.setAttribute;
video.setAttribute = function(name, value) {
if (name === 'muted') return;
originalSetAttribute.call(this, name, value);
};
},
enforceStateOnVideo(video) {
if (!video || !this.lastKnownGoodState) return;
// Find the control instance for this video
let controlInstance = null;
this.activeControlInstances.forEach(instance => {
if (instance.video === video) {
controlInstance = instance;
}
});
if (controlInstance) {
controlInstance.forceApplyState(this.lastKnownGoodState);
} else {
// If no control instance exists yet, apply state directly
video.muted = this.lastKnownGoodState.isMuted;
video.volume = this.lastKnownGoodState.volume;
video.playbackRate = this.lastKnownGoodState.speed;
}
},
isValidVolume(vol) {
return typeof vol === 'number' && vol >= 0 && vol <= 1;
},
isValidSpeed(speed) {
const validSpeeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
return validSpeeds.includes(speed);
},
savePreferences() {
try {
// Update lastUpdate timestamp
this.preferences.lastUpdate = Date.now();
localStorage.setItem('igVideoPreferences', JSON.stringify(this.preferences));
// Update last known good state
this.lastKnownGoodState = { ...this.preferences };
} catch (e) {
console.error('Error saving preferences:', e);
}
},
update(key, value) {
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
let isValid = true;
if (key === 'volume') {
isValid = this.isValidVolume(value);
} else if (key === 'speed') {
isValid = this.isValidSpeed(value);
}
if (isValid) {
this.preferences[key] = value;
this.preferences.lastUpdate = Date.now();
this.lastKnownGoodState = { ...this.preferences };
this.saveTimeout = setTimeout(() => {
this.savePreferences();
window.dispatchEvent(new CustomEvent('igVideoStateChange', {
detail: { ...this.preferences }
}));
}, 300);
}
},
registerInstance(controlInstance) {
this.activeControlInstances.add(controlInstance);
controlInstance.applyState(this.preferences);
},
unregisterInstance(controlInstance) {
this.activeControlInstances.delete(controlInstance);
},
broadcastStateChange(event) {
const newState = event.detail;
this.activeControlInstances.forEach(instance => {
instance.applyState(newState);
});
}
};
// DOM Utilities
const DOMUtils = {
createButton(options = {}) {
const button = document.createElement('button');
button.className = options.className || '';
Object.assign(button.style, {
...UI.styles.button,
...options.style
});
if (options.innerHTML) button.innerHTML = options.innerHTML;
if (options.onclick) button.addEventListener('click', options.onclick);
return button;
},
createContainer(options = {}) {
const container = document.createElement('div');
container.className = options.className || '';
Object.assign(container.style, {
...UI.styles.container,
...options.style
});
return container;
},
setupHoverMenu(control, menuContainer, delay = 500) {
let timeout;
const showMenu = () => {
clearTimeout(timeout);
menuContainer.style.display = menuContainer.dataset.displayType || 'block';
};
const hideMenu = () => {
timeout = setTimeout(() => {
menuContainer.style.display = 'none';
}, delay);
};
control.addEventListener('mouseenter', showMenu);
control.addEventListener('mouseleave', hideMenu);
menuContainer.addEventListener('mouseenter', () => clearTimeout(timeout));
menuContainer.addEventListener('mouseleave', hideMenu);
return { showMenu, hideMenu };
},
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${String(secs).padStart(2, '0')}`;
}
};
// Video Controls Class
class VideoControls {
constructor(videoElement) {
this.video = videoElement;
this.isDragging = false;
this.eventListeners = new Set();
VideoState.registerInstance(this);
this.container = this.createControlsContainer();
this.initializeVideoState();
}
addEventListener(element, type, handler) {
element.addEventListener(type, handler);
this.eventListeners.add({ element, type, handler });
}
removeAllEventListeners() {
this.eventListeners.forEach(({ element, type, handler }) => {
element.removeEventListener(type, handler);
});
this.eventListeners.clear();
}
createControlComponent(options) {
const control = DOMUtils.createContainer({
className: options.className,
style: { marginRight: '12px', ...options.style }
});
const button = DOMUtils.createButton({
className: options.buttonClassName,
style: options.buttonStyle,
innerHTML: options.buttonContent,
onclick: options.onClick
});
control.appendChild(button);
if (options.menu) {
control.appendChild(options.menu);
if (options.useHoverMenu) {
DOMUtils.setupHoverMenu(control, options.menu);
}
}
return { control, button };
}
createControlsContainer() {
const container = DOMUtils.createContainer({
className: 'ig-video-control',
style: {
width: '100%',
height: UI.sizes.controlHeight,
background: UI.colors.background,
display: 'flex',
flexDirection: 'column',
zIndex: '9999999',
position: 'relative',
pointerEvents: 'all'
}
});
const timeline = this.createTimeline();
const controls = this.createControlsRow();
container.appendChild(timeline);
container.appendChild(controls);
return container;
}
createControlsRow() {
const row = DOMUtils.createContainer({
className: 'ig-video-controls-row',
style: {
display: 'flex',
alignItems: 'center',
padding: '0 12px',
height: '28px',
position: 'relative',
zIndex: '9999999'
}
});
const controls = [
this.createPlayButton(),
this.createTimeDisplay(),
this.createSpeedControl(),
this.createBackgroundPlayControl(),
this.createVolumeControl()
];
controls.forEach(control => row.appendChild(control));
return row;
}
createPlayButton() {
const updatePlayButton = (button) => {
button.innerHTML = this.video.paused ? '⏵️' : '⏸️';
};
const { control, button } = this.createControlComponent({
className: 'ig-video-play-control',
buttonClassName: 'ig-video-control-button',
buttonStyle: {
fontSize: '24px',
width: UI.sizes.buttonSize,
height: UI.sizes.buttonSize
},
buttonContent: this.video.paused ? '⏵️' : '⏸️',
onClick: (e) => {
e.stopPropagation();
if (this.video.paused) {
this.video.play();
} else {
this.video.pause();
}
}
});
this.addEventListener(this.video, 'play', () => updatePlayButton(button));
this.addEventListener(this.video, 'pause', () => updatePlayButton(button));
return control;
}
createTimeDisplay() {
const display = document.createElement('span');
display.className = 'ig-video-time-display';
Object.assign(display.style, {
color: UI.colors.text,
fontSize: UI.sizes.fontSize.normal,
marginRight: '12px',
fontFamily: 'monospace'
});
const updateTime = () => {
const current = Math.floor(this.video.currentTime);
const total = Math.floor(this.video.duration);
display.textContent = `${DOMUtils.formatTime(current)} / ${DOMUtils.formatTime(total)}`;
};
this.addEventListener(this.video, 'timeupdate', updateTime);
this.addEventListener(this.video, 'loadedmetadata', updateTime);
updateTime();
return display;
}
createSpeedControl() {
const options = this.createSpeedOptions();
options.style.display = 'none';
return this.createControlComponent({
className: 'ig-video-speed-control',
buttonClassName: 'ig-video-speed-button',
buttonStyle: {
fontSize: UI.sizes.fontSize.normal,
padding: '4px 8px'
},
buttonContent: `${this.video.playbackRate}x`,
menu: options,
useHoverMenu: false,
onClick: (e) => {
e.stopPropagation();
const menu = options;
const isVisible = menu.style.display === 'flex';
menu.style.display = isVisible ? 'none' : 'flex';
if (!isVisible) {
const closeMenu = (event) => {
if (!menu.contains(event.target)) {
menu.style.display = 'none';
document.removeEventListener('click', closeMenu);
}
};
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
}
}
}).control;
}
createSpeedOptions() {
const container = document.createElement('div');
container.className = 'ig-video-speed-options';
Object.assign(container.style, {
position: 'absolute',
top: 'calc(100% + 5px)',
left: '50%',
transform: 'translateX(-50%)',
background: UI.colors.background,
borderRadius: '4px',
display: 'none',
flexDirection: 'column',
minWidth: '80px',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
zIndex: '10000001'
});
[0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2].forEach(speed => {
const option = DOMUtils.createButton({
className: 'ig-video-speed-option',
style: {
padding: '8px 16px',
fontSize: UI.sizes.fontSize.normal,
width: '100%',
textAlign: 'center',
background: 'none',
border: 'none',
color: '#ffffff',
cursor: 'pointer'
},
innerHTML: `${speed}x`,
onclick: (e) => {
e.stopPropagation();
this.video.playbackRate = speed;
VideoState.update('speed', speed);
container.style.display = 'none';
}
});
container.appendChild(option);
});
return container;
}
createVolumeControl() {
const sliderContainer = document.createElement('div');
sliderContainer.className = 'ig-video-volume-slider-container';
Object.assign(sliderContainer.style, {position: 'absolute',
left: '25px',
top: '50%',
transform: 'translateY(-50%)',
background: UI.colors.background,
padding: '10px',
borderRadius: '4px',
display: 'none'
});
const slider = this.createVolumeSlider();
sliderContainer.appendChild(slider);
const { control } = this.createControlComponent({
className: 'ig-video-volume-control',
buttonClassName: 'ig-video-volume-button',
buttonStyle: {
fontSize: UI.sizes.fontSize.large,
width: UI.sizes.buttonSize,
height: UI.sizes.buttonSize
},
buttonContent: this.video.muted ? '🔇' : '🔊',
onClick: (e) => {
e.stopPropagation();
this.video.muted = !this.video.muted;
VideoState.update('isMuted', this.video.muted);
this.updateVolumeUI();
},
menu: sliderContainer,
useHoverMenu: true
});
return control;
}
createVolumeSlider() {
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '100';
slider.value = this.video.volume * 100;
slider.className = 'ig-video-slider';
Object.assign(slider.style, {
width: '100px',
height: '4px',
background: 'rgba(255,255,255,0.2)',
cursor: 'pointer'
});
this.addEventListener(slider, 'input', (e) => {
const value = e.target.value / 100;
this.video.volume = value;
this.video.muted = value === 0;
VideoState.update('volume', value);
VideoState.update('isMuted', this.video.muted);
this.updateVolumeUI();
});
return slider;
}
createBackgroundPlayControl() {
const updateBgPlayButton = (button) => {
button.innerHTML = VideoState.preferences.backgroundPlay ? '🔓' : '🔒';
button.title = VideoState.preferences.backgroundPlay ?
'Video will continue in background' :
'Video will pause in background';
button.style.opacity = VideoState.preferences.backgroundPlay ? '1' : '0.7';
};
const { control, button } = this.createControlComponent({
className: 'ig-video-bgplay-control',
buttonClassName: 'ig-video-control-button',
buttonStyle: {
fontSize: UI.sizes.fontSize.normal,
padding: '4px 8px',
opacity: VideoState.preferences.backgroundPlay ? '1' : '0.7'
},
buttonContent: VideoState.preferences.backgroundPlay ? '🔓' : '🔒',
onClick: (e) => {
e.stopPropagation();
const newState = !VideoState.preferences.backgroundPlay;
VideoState.update('backgroundPlay', newState);
updateBgPlayButton(button);
newState ? this.enableBackgroundPlay() : this.disableBackgroundPlay();
}
});
return control;
}
createTimeline() {
const timeline = document.createElement('div');
timeline.className = 'ig-video-timeline';
Object.assign(timeline.style, {
width: '100%',
height: UI.sizes.timelineHeight,
background: 'rgba(255,255,255,0.2)',
position: 'relative',
transition: 'height 0.1s'
});
const progress = document.createElement('div');
progress.className = 'ig-video-progress';
Object.assign(progress.style, {
height: '100%',
background: UI.colors.primary,
width: '0%',
position: 'absolute',
top: '0',
left: '0'
});
const seekHandle = document.createElement('div');
seekHandle.className = 'ig-video-seek-handle';
Object.assign(seekHandle.style, {
width: '12px',
height: '12px',
background: UI.colors.primary,
borderRadius: '50%',
position: 'absolute',
right: '-6px',
top: '50%',
transform: 'translateY(-50%) scale(0)',
transition: 'transform 0.1s'
});
const tooltip = document.createElement('div');
tooltip.className = 'ig-video-tooltip';
Object.assign(tooltip.style, {
position: 'absolute',
background: UI.colors.background,
color: UI.colors.text,
padding: '4px 8px',
borderRadius: '4px',
fontSize: UI.sizes.fontSize.normal,
bottom: '100%',
transform: 'translateX(-50%)',
display: 'none',
zIndex: '10000000',
pointerEvents: 'none',
whiteSpace: 'nowrap',
marginBottom: '8px'
});
progress.appendChild(seekHandle);
timeline.appendChild(progress);
const container = DOMUtils.createContainer({
className: 'ig-video-timeline-container',
style: {
width: '100%',
height: '20px',
position: 'relative',
cursor: 'pointer',
padding: '8px 0',
zIndex: '9999999'
}
});
container.appendChild(timeline);
container.appendChild(tooltip);
this.setupTimelineEvents(container, timeline, progress, seekHandle, tooltip);
return container;
}
setupTimelineEvents(container, timeline, progress, seekHandle, tooltip) {
const updateTimelinePosition = (e) => {
if (!this.isDragging) return;
const rect = timeline.getBoundingClientRect();
const pos = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const newTime = this.video.duration * pos;
progress.style.width = `${pos * 100}%`;
tooltip.style.left = `${pos * 100}%`;
tooltip.textContent = DOMUtils.formatTime(newTime);
this.video.currentTime = newTime;
};
this.addEventListener(container, 'mousedown', (e) => {
e.stopPropagation();
this.isDragging = true;
timeline.style.height = UI.sizes.timelineActiveHeight;
seekHandle.style.transform = 'translateY(-50%) scale(1)';
updateTimelinePosition(e);
const handleMouseMove = (e) => updateTimelinePosition(e);
const handleMouseUp = () => {
this.isDragging = false;
timeline.style.height = UI.sizes.timelineHeight;
seekHandle.style.transform = 'translateY(-50%) scale(0)';
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
this.addEventListener(container, 'mousemove', (e) => {
const rect = timeline.getBoundingClientRect();
const pos = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
if (!this.isDragging) {
tooltip.style.display = 'block';
tooltip.style.left = `${pos * 100}%`;
const previewTime = this.video.duration * pos;
tooltip.textContent = DOMUtils.formatTime(previewTime);
}
});
this.addEventListener(container, 'mouseleave', () => {
if (!this.isDragging) {
tooltip.style.display = 'none';
timeline.style.height = UI.sizes.timelineHeight;
seekHandle.style.transform = 'translateY(-50%) scale(0)';
}
});
this.addEventListener(container, 'mouseenter', () => {
if (!this.isDragging) {
timeline.style.height = UI.sizes.timelineActiveHeight;
seekHandle.style.transform = 'translateY(-50%) scale(1)';
}
});
this.addEventListener(this.video, 'timeupdate', () => {
if (!this.isDragging) {
const progressValue = (this.video.currentTime / this.video.duration) * 100;
progress.style.width = `${progressValue}%`;
}
});
}
initializeVideoState() {
if (this.video) {
// Force unmute and set initial volume
this.video.muted = false;
this.video.removeAttribute('muted');
if (this.video.volume === 0) {
this.video.volume = VideoState.preferences.volume;
}
this.setupVideoListeners();
this.applyState(VideoState.preferences);
}
}
setupVideoListeners() {
this.addEventListener(this.video, 'volumechange', () => {
if (this.video.muted !== VideoState.preferences.isMuted) {
VideoState.update('isMuted', this.video.muted);
this.updateVolumeUI();
}
if (!this.video.muted && this.video.volume !== VideoState.preferences.volume) {
VideoState.update('volume', this.video.volume);
this.updateVolumeUI();
}
});
this.addEventListener(this.video, 'ratechange', () => {
if (this.video.playbackRate !== VideoState.preferences.speed) {
VideoState.update('speed', this.video.playbackRate);
this.updateSpeedUI();
}
});
}
applyState(state) {
if (this.video) {
this.video.playbackRate = state.speed;
this.video.volume = state.volume;
this.video.muted = state.isMuted;
if (this.container) {
this.updateVolumeUI();
this.updateSpeedUI();
this.updateBackgroundPlayUI();
}
state.backgroundPlay ? this.enableBackgroundPlay() : this.disableBackgroundPlay();
}
}
forceApplyState(state) {
if (this.video) {
// Force apply state regardless of current values
this.video.muted = state.isMuted;
this.video.volume = state.volume;
this.video.playbackRate = state.speed;
// Force update UI elements
if (this.container) {
const volumeButton = this.container.querySelector('.ig-video-volume-button');
const volumeSlider = this.container.querySelector('.ig-video-slider');
const speedButton = this.container.querySelector('.ig-video-speed-button');
const bgPlayButton = this.container.querySelector('.ig-video-bgplay-control .ig-video-control-button');
if (volumeButton) volumeButton.innerHTML = state.isMuted ? '🔇' : '🔊';
if (volumeSlider) volumeSlider.value = state.isMuted ? 0 : state.volume * 100;
if (speedButton) speedButton.innerHTML = `${state.speed}x`;
if (bgPlayButton) {
bgPlayButton.innerHTML = state.backgroundPlay ? '🔓' : '🔒';
bgPlayButton.style.opacity = state.backgroundPlay ? '1' : '0.7';
}
}
// Enforce background play state
state.backgroundPlay ? this.enableBackgroundPlay() : this.disableBackgroundPlay();
}
}
updateVolumeUI() {
const button = this.container.querySelector('.ig-video-volume-button');
const slider = this.container.querySelector('.ig-video-slider');
if (button) button.innerHTML = this.video.muted ? '🔇' : '🔊';
if (slider) slider.value = this.video.muted ? 0 : this.video.volume * 100;
}
updateSpeedUI() {
const button = this.container.querySelector('.ig-video-speed-button');
if (button) button.innerHTML = `${this.video.playbackRate}x`;
}
updateBackgroundPlayUI() {
const button = this.container.querySelector('.ig-video-bgplay-control .ig-video-control-button');
if (button) {
button.innerHTML = VideoState.preferences.backgroundPlay ? '🔓' : '🔒';
button.style.opacity = VideoState.preferences.backgroundPlay ? '1' : '0.7';
}
}
enableBackgroundPlay() {
if (!this.video._originalPause) {
this.video._originalPause = this.video.pause;
this.video.pause = () => {
if (document.visibilityState === 'hidden' && !this.video.ended) {
return Promise.resolve();
}
return this.video._originalPause.call(this.video);
};
}
}
disableBackgroundPlay() {
if (this.video._originalPause) {
this.video.pause = this.video._originalPause;
delete this.video._originalPause;
}
}
destroy() {
VideoState.unregisterInstance(this);
this.removeAllEventListeners();
if (this.container) {
this.container.remove();
}
}
}
// Add global styles
const addStyles = () => {
const styles = `
.ig-video-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: ${UI.colors.primary};
border-radius: 50%;
cursor: pointer;
}
.ig-video-slider::-moz-range-thumb {
width: 12px;
height: 12px;
background: ${UI.colors.primary};
border-radius: 50%;
cursor: pointer;
border: none;
}
.ig-video-control-button:hover {
opacity: 0.8;
}
.ig-video-speed-option:hover {
background: ${UI.colors.hover};
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
};
// Initialize video controls
const initVideoControls = () => {
const processedVideos = new WeakSet();
const addControlsToVideo = (videoElement) => {
if (processedVideos.has(videoElement)) return;
const videoContainer = videoElement.closest('div[class*="x5yr21d"][class*="x1uhb9sk"]');
if (!videoContainer) return;
// Early unmuting
videoElement.muted = false;
videoElement.removeAttribute('muted');
processedVideos.add(videoElement);
const controls = new VideoControls(videoElement);
const controlsWrapper = DOMUtils.createContainer({
className: 'ig-video-controls-wrapper',
style: {
width: '100%',
position: 'relative',
zIndex: '9999999'
}
});
controlsWrapper.appendChild(controls.container);
videoContainer.parentElement.insertBefore(controlsWrapper, videoContainer);
videoContainer.style.position = 'relative';
videoContainer.style.zIndex = '1';
const observer = new MutationObserver((mutations) => {
if (!document.contains(videoElement)) {
controls.destroy();
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
};
// Main observer
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeName === 'VIDEO') {
addControlsToVideo(node);
} else if (node.querySelectorAll) {
node.querySelectorAll('video').forEach(addControlsToVideo);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src', 'style', 'class']
});
// Process existing videos
document.querySelectorAll('video').forEach(addControlsToVideo);
};
// Initialize everything
const initialize = () => {
VideoState.initialize();
addStyles();
initVideoControls();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();