- // ==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();
- })();