// ==UserScript==
// @name Audio Controls with Auto-Play and Speed Management
// @namespace http://tampermonkey.net/
// @version 1.9
// @description Controls audio playback with speed adjustment and auto-play
// @author You
// @match https://inovel1*.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Configuration
const DEFAULT_PLAYBACK_RATE = 0.7;
const AUTO_PLAY_DELAY = 5000; // 5 seconds
const AUDIO_SELECTOR = 'audio[controls]';
const MAX_RETRY_ATTEMPTS = 5; // Maximum number of retry attempts
const RETRY_DELAY = 1000; // Delay between retries in milliseconds
// Device detection
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const isIOSSafari = isIOS && isSafari;
// State variables
let audioElement = null;
let playbackRate = parseFloat(localStorage.getItem('audio_playback_rate')) || DEFAULT_PLAYBACK_RATE;
let isMinimized = localStorage.getItem('audio_controls_minimized') === 'true' || false;
let countdownTimer = null;
let retryAttempts = 0;
let hasUserInteracted = false;
// Position settings - load from localStorage or use defaults
let bubblePosition = JSON.parse(localStorage.getItem('audio_bubble_position')) || { top: '20px', left: '20px' };
// Create main container for all controls
const mainContainer = document.createElement('div');
mainContainer.style.cssText = `
position: fixed;
z-index: 9999;
font-family: Arial, sans-serif;
`;
document.body.appendChild(mainContainer);
// Create the expanded control panel
const controlPanel = document.createElement('div');
controlPanel.className = 'audio-control-panel';
controlPanel.style.cssText = `
background-color: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 180px;
`;
// Create the minimized bubble view
const bubbleView = document.createElement('div');
bubbleView.className = 'audio-bubble';
bubbleView.style.cssText = `
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
user-select: none;
`;
// Create bubble icon
const bubbleIcon = document.createElement('div');
bubbleIcon.style.cssText = `
font-size: 20px;
color: white;
`;
bubbleIcon.innerHTML = '🔊';
bubbleView.appendChild(bubbleIcon);
// Create duration display
const durationDisplay = document.createElement('div');
durationDisplay.style.cssText = `
color: white;
font-size: 14px;
text-align: center;
margin-bottom: 5px;
`;
durationDisplay.textContent = 'Audio Duration: --:--';
controlPanel.appendChild(durationDisplay);
// Create countdown/message display
const countdownDisplay = document.createElement('div');
countdownDisplay.style.cssText = `
color: #ffcc00;
font-size: 14px;
text-align: center;
margin-bottom: 8px;
font-weight: bold;
height: 20px; /* Fixed height to prevent layout shifts */
`;
countdownDisplay.textContent = '';
controlPanel.appendChild(countdownDisplay);
// Create play/pause button with larger size for iOS
const playPauseButton = document.createElement('button');
playPauseButton.style.cssText = `
background-color: #4CAF50;
border: none;
color: white;
padding: ${isIOSSafari ? '12px 15px' : '8px 12px'};
text-align: center;
font-size: ${isIOSSafari ? '16px' : '14px'};
border-radius: 4px;
cursor: pointer;
width: 100%;
font-weight: ${isIOSSafari ? 'bold' : 'normal'};
`;
playPauseButton.textContent = '▶️ Play';
controlPanel.appendChild(playPauseButton);
// Create special instruction for iOS if needed
if (isIOSSafari) {
const iosInstruction = document.createElement('div');
iosInstruction.style.cssText = `
color: #ff9800;
font-size: 12px;
text-align: center;
margin: 5px 0;
font-style: italic;
`;
iosInstruction.textContent = 'Tap Play button to start audio (iOS requires manual activation)';
controlPanel.appendChild(iosInstruction);
}
// Create speed control container
const speedControlContainer = document.createElement('div');
speedControlContainer.style.cssText = `
display: flex;
gap: 8px;
width: 100%;
`;
controlPanel.appendChild(speedControlContainer);
// Create speed down button
const speedDownButton = document.createElement('button');
speedDownButton.style.cssText = `
background-color: #795548;
border: none;
color: white;
padding: 8px 12px;
text-align: center;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
flex: 1;
`;
speedDownButton.textContent = '🐢 Slower';
speedControlContainer.appendChild(speedDownButton);
// Create speed up button
const speedUpButton = document.createElement('button');
speedUpButton.style.cssText = `
background-color: #009688;
border: none;
color: white;
padding: 8px 12px;
text-align: center;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
flex: 1;
`;
speedUpButton.textContent = '🐇 Faster';
speedControlContainer.appendChild(speedUpButton);
// Create speed display
const speedDisplay = document.createElement('div');
speedDisplay.style.cssText = `
color: white;
font-size: 14px;
text-align: center;
margin-top: 5px;
`;
speedDisplay.textContent = `Speed: ${playbackRate.toFixed(1)}x`;
controlPanel.appendChild(speedDisplay);
// Create minimize button
const minimizeButton = document.createElement('button');
minimizeButton.style.cssText = `
background-color: #607D8B;
border: none;
color: white;
padding: 6px 10px;
text-align: center;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
margin-top: 8px;
`;
minimizeButton.textContent = '− Minimize';
controlPanel.appendChild(minimizeButton);
// Function to toggle between expanded and minimized views
function toggleMinimized() {
isMinimized = !isMinimized;
updateViewState();
// Save state to localStorage
localStorage.setItem('audio_controls_minimized', isMinimized);
}
// Function to update the current view based on minimized state
function updateViewState() {
// Clear the container first
while (mainContainer.firstChild) {
mainContainer.removeChild(mainContainer.firstChild);
}
if (isMinimized) {
// Show bubble view
mainContainer.appendChild(bubbleView);
// Set position based on saved values
mainContainer.style.top = bubblePosition.top;
mainContainer.style.left = bubblePosition.left;
mainContainer.style.right = 'auto';
mainContainer.style.bottom = 'auto';
} else {
// Show expanded control panel
mainContainer.appendChild(controlPanel);
// If coming from minimized state, place in the same position
// Otherwise use default bottom right
if (bubblePosition) {
mainContainer.style.top = bubblePosition.top;
mainContainer.style.left = bubblePosition.left;
mainContainer.style.right = 'auto';
mainContainer.style.bottom = 'auto';
} else {
mainContainer.style.top = 'auto';
mainContainer.style.left = 'auto';
mainContainer.style.right = '20px';
mainContainer.style.bottom = '20px';
}
}
}
// Make only the bubble draggable
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
bubbleView.addEventListener('mousedown', function(e) {
// Only initiate drag if user holds for a brief moment
setTimeout(() => {
if (e.buttons === 1) { // Left mouse button
isDragging = true;
dragOffsetX = e.clientX - mainContainer.getBoundingClientRect().left;
dragOffsetY = e.clientY - mainContainer.getBoundingClientRect().top;
bubbleView.style.cursor = 'grabbing';
}
}, 100);
});
document.addEventListener('mousemove', function(e) {
if (!isDragging || !isMinimized) return;
e.preventDefault();
// Calculate new position
const newLeft = e.clientX - dragOffsetX;
const newTop = e.clientY - dragOffsetY;
// Keep within viewport bounds
const maxX = window.innerWidth - bubbleView.offsetWidth;
const maxY = window.innerHeight - bubbleView.offsetHeight;
mainContainer.style.left = `${Math.max(0, Math.min(maxX, newLeft))}px`;
mainContainer.style.top = `${Math.max(0, Math.min(maxY, newTop))}px`;
});
document.addEventListener('mouseup', function(event) {
if (isDragging && isMinimized) {
isDragging = false;
bubbleView.style.cursor = 'pointer';
// Save the position
bubblePosition = {
top: mainContainer.style.top,
left: mainContainer.style.left
};
localStorage.setItem('audio_bubble_position', JSON.stringify(bubblePosition));
// Prevent click if we were dragging
event.preventDefault();
return false;
} else if (isMinimized && (event.target === bubbleView || bubbleView.contains(event.target))) {
// If it was a click (not drag) on the bubble, expand
toggleMinimized();
}
});
// Add touch support for mobile devices - only for bubble
bubbleView.addEventListener('touchstart', function(e) {
const touch = e.touches[0];
isDragging = true;
dragOffsetX = touch.clientX - mainContainer.getBoundingClientRect().left;
dragOffsetY = touch.clientY - mainContainer.getBoundingClientRect().top;
// Prevent scrolling while dragging
e.preventDefault();
});
document.addEventListener('touchmove', function(e) {
if (!isDragging || !isMinimized) return;
const touch = e.touches[0];
// Calculate new position
const newLeft = touch.clientX - dragOffsetX;
const newTop = touch.clientY - dragOffsetY;
// Keep within viewport bounds
const maxX = window.innerWidth - bubbleView.offsetWidth;
const maxY = window.innerHeight - bubbleView.offsetHeight;
mainContainer.style.left = `${Math.max(0, Math.min(maxX, newLeft))}px`;
mainContainer.style.top = `${Math.max(0, Math.min(maxY, newTop))}px`;
// Prevent scrolling while dragging
e.preventDefault();
});
document.addEventListener('touchend', function(event) {
if (isDragging && isMinimized) {
isDragging = false;
// Save the position
bubblePosition = {
top: mainContainer.style.top,
left: mainContainer.style.left
};
localStorage.setItem('audio_bubble_position', JSON.stringify(bubblePosition));
// If touch distance was very small, treat as click
const touchMoved = Math.abs(event.changedTouches[0].clientX - (parseInt(mainContainer.style.left) + dragOffsetX)) > 5 ||
Math.abs(event.changedTouches[0].clientY - (parseInt(mainContainer.style.top) + dragOffsetY)) > 5;
if (!touchMoved && (event.target === bubbleView || bubbleView.contains(event.target))) {
toggleMinimized();
}
}
});
// Add click event for minimize button
minimizeButton.addEventListener('click', toggleMinimized);
// Function to find audio element
function findAudioElement() {
const audio = document.querySelector(AUDIO_SELECTOR);
if (audio && audio !== audioElement) {
audioElement = audio;
initializeAudio();
}
if (!audioElement) {
// If not found, try again in 500ms
setTimeout(findAudioElement, 500);
}
}
// Function to format time in MM:SS
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
// Function to run the countdown timer
function startCountdown(seconds) {
// Skip countdown for iOS Safari since we need user interaction
if (isIOSSafari) {
countdownDisplay.textContent = 'Tap Play button to start audio';
return;
}
// Clear any existing countdown
if (countdownTimer) {
clearInterval(countdownTimer);
}
let remainingSeconds = seconds;
updateCountdownDisplay(remainingSeconds);
countdownTimer = setInterval(() => {
remainingSeconds--;
updateCountdownDisplay(remainingSeconds);
if (remainingSeconds <= 0) {
clearInterval(countdownTimer);
countdownTimer = null;
// Play the audio when countdown reaches zero
if (audioElement) {
// Reset retry counter before attempting to play
retryAttempts = 0;
playAudioWithRetry();
}
}
}, 1000);
}
// Function to update countdown display
function updateCountdownDisplay(seconds) {
countdownDisplay.textContent = `Auto-play in ${seconds} seconds`;
}
// Function to play audio with retry mechanism
function playAudioWithRetry() {
if (!audioElement) return;
// For iOS Safari, we need direct user interaction - don't auto-retry
if (isIOSSafari && !hasUserInteracted) {
countdownDisplay.textContent = 'Tap Play button to start audio';
return;
}
// For iOS Safari, try to load() before play() to ensure content is ready
if (isIOSSafari) {
audioElement.load();
}
// Attempt to play the audio
audioElement.play()
.then(() => {
// Success - update UI and reset retry counter
updatePlayPauseButton();
countdownDisplay.textContent = ''; // Clear countdown display
retryAttempts = 0;
hasUserInteracted = true;
})
.catch(err => {
console.log('Audio play error:', err);
// For iOS Safari, we need to wait for user interaction
if (isIOSSafari) {
countdownDisplay.textContent = 'Tap Play button to start audio';
return;
}
// Error playing audio - retry if under max attempts
retryAttempts++;
if (retryAttempts <= MAX_RETRY_ATTEMPTS) {
// Update the countdown display with retry information
countdownDisplay.textContent = `Auto-play blocked. Retrying (${retryAttempts}/${MAX_RETRY_ATTEMPTS})...`;
// Try to play again after a delay
setTimeout(() => {
togglePlayPause(); // This will call play() again
}, RETRY_DELAY);
} else {
// Max retries reached
countdownDisplay.textContent = 'Auto-play failed. Tap Play button to start.';
retryAttempts = 0;
}
});
}
// Function to initialize audio controls
function initializeAudio() {
if (!audioElement) return;
// Set saved playback rate
audioElement.playbackRate = playbackRate;
// Update UI based on current state
updatePlayPauseButton();
// Get duration when metadata is loaded and start countdown
if (audioElement.readyState >= 1) {
handleAudioLoaded();
} else {
audioElement.addEventListener('loadedmetadata', handleAudioLoaded);
}
// Add event listeners
audioElement.addEventListener('play', updatePlayPauseButton);
audioElement.addEventListener('pause', updatePlayPauseButton);
audioElement.addEventListener('ended', updatePlayPauseButton);
// iOS-specific: preload audio when possible
if (isIOSSafari) {
audioElement.preload = 'auto';
audioElement.load();
}
}
// Function to handle audio loaded event
function handleAudioLoaded() {
if (!audioElement) return;
// Update duration display
if (!isNaN(audioElement.duration)) {
durationDisplay.textContent = `Audio Duration: ${formatTime(audioElement.duration)}`;
}
// For iOS Safari, don't start countdown - wait for user interaction
if (isIOSSafari) {
countdownDisplay.textContent = 'Tap Play button to start audio';
return;
}
// Start countdown for auto-play (5 seconds)
const countdownSeconds = Math.floor(AUTO_PLAY_DELAY / 1000);
startCountdown(countdownSeconds);
}
// Function to update play/pause button state
function updatePlayPauseButton() {
if (!audioElement) return;
if (audioElement.paused) {
playPauseButton.textContent = '▶️ Play';
playPauseButton.style.backgroundColor = '#4CAF50';
} else {
playPauseButton.textContent = '⏸️ Pause';
playPauseButton.style.backgroundColor = '#F44336';
// If playing, clear countdown
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
countdownDisplay.textContent = '';
}
}
}
// Function to toggle play/pause - optimized for iOS
function togglePlayPause() {
if (!audioElement) return;
// Set flag for user interaction (important for iOS)
hasUserInteracted = true;
if (audioElement.paused) {
// For iOS Safari, need to try additional methods
if (isIOSSafari) {
// Make sure audio is loaded
audioElement.load();
// For iOS, try to unlock audio context if possible
unlockAudioContext();
// Try to play with normal method
audioElement.play()
.then(() => {
updatePlayPauseButton();
countdownDisplay.textContent = '';
})
.catch(err => {
console.log('iOS play error:', err);
countdownDisplay.textContent = 'Playback error. Try again.';
});
} else {
// Normal browsers - try to play with retry mechanism
playAudioWithRetry();
}
} else {
audioElement.pause();
updatePlayPauseButton();
}
}
// Special function to try to unlock audio context on iOS
function unlockAudioContext() {
// Create a silent audio buffer
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
const audioCtx = new AudioContext();
const buffer = audioCtx.createBuffer(1, 1, 22050);
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
source.start(0);
// Resume audio context if suspended
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
} catch (e) {
console.log('Audio context unlock error:', e);
}
}
// Function to update playback speed
function updatePlaybackSpeed(newRate) {
playbackRate = newRate;
// Update audio element if exists
if (audioElement) {
audioElement.playbackRate = playbackRate;
}
// Update display
speedDisplay.textContent = `Speed: ${playbackRate.toFixed(1)}x`;
// Save to localStorage
localStorage.setItem('audio_playback_rate', playbackRate);
}
// Function to decrease playback speed
function decreaseSpeed() {
const newRate = Math.max(0.5, playbackRate - 0.1);
updatePlaybackSpeed(newRate);
}
// Function to increase playback speed
function increaseSpeed() {
const newRate = Math.min(2.5, playbackRate + 0.1);
updatePlaybackSpeed(newRate);
}
// Set up event listeners for buttons
playPauseButton.addEventListener('click', togglePlayPause);
speedDownButton.addEventListener('click', decreaseSpeed);
speedUpButton.addEventListener('click', increaseSpeed);
// Create an observer to watch for new audio elements
const audioObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeName === 'AUDIO' ||
(node.nodeType === 1 && node.querySelector(AUDIO_SELECTOR))) {
// Reset audio element and reinitialize
audioElement = null;
findAudioElement();
}
});
}
});
});
// Start observing the document
audioObserver.observe(document.body, { childList: true, subtree: true });
// Initialize the view state
updateViewState();
// Start finding audio element
findAudioElement();
})();