Audio Controls with Auto-Play and Speed Management

Controls audio playback with speed adjustment and auto-play

Versione datata 18/04/2025. Vedi la nuova versione l'ultima versione.

// ==UserScript==
// @name         Audio Controls with Auto-Play and Speed Management
// @namespace    http://tampermonkey.net/
// @version      1.10
// @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
    const RATE_CHECK_INTERVAL = 800; // Check playback rate every 800ms

    // 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 rateCheckInterval = null;
    let retryAttempts = 0;
    let hasUserInteracted = false;
    let lastRateApplication = 0;

    // 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 version display at the top
    const versionDisplay = document.createElement('div');
    versionDisplay.style.cssText = `
        color: #aaaaaa;
        font-size: 10px;
        text-align: center;
        margin-bottom: 5px;
        font-style: italic;
    `;
    versionDisplay.textContent = `Version: 1.8`;
    controlPanel.appendChild(versionDisplay);

    // 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 = '🔊';  // Will be updated based on audio state
    bubbleView.appendChild(bubbleIcon);

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

        // Only allow vertical movement (Y-axis)
        const newTop = e.clientY - dragOffsetY;

        // Keep within viewport bounds
        const maxY = window.innerHeight - bubbleView.offsetHeight;

        // Only update Y position, keep X position the same
        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 (only top changes, left stays the same)
            bubblePosition = {
                top: mainContainer.style.top,
                left: bubblePosition.left // Keep the same left position
            };
            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];

        // Only allow vertical movement (Y-axis)
        const newTop = touch.clientY - dragOffsetY;

        // Keep within viewport bounds
        const maxY = window.innerHeight - bubbleView.offsetHeight;

        // Only update Y position, keep X position the same
        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 (only top changes, left stays the same)
            bubblePosition = {
                top: mainContainer.style.top,
                left: bubblePosition.left // Keep the same left position
            };
            localStorage.setItem('audio_bubble_position', JSON.stringify(bubblePosition));

            // If touch distance was very small, treat as click
            const touchMoved = 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);

    // NEW FUNCTION: Apply playback rate to all audio elements
    function applyPlaybackRateToAllAudio() {
        const now = Date.now();
        // Throttle frequent applications (but still allow force flag to override)
        if ((now - lastRateApplication) < 500) return;

        lastRateApplication = now;
        const allAudioElements = document.querySelectorAll(AUDIO_SELECTOR);

        if (allAudioElements.length > 0) {
            allAudioElements.forEach(audio => {
                if (audio.playbackRate !== playbackRate) {
                    audio.playbackRate = playbackRate;
                    console.log(`[Audio Controls] Applied rate ${playbackRate.toFixed(1)}x to audio element`);
                }
            });

            // If our main audio element isn't set yet, use the first one found
            if (!audioElement && allAudioElements.length > 0) {
                audioElement = allAudioElements[0];
                initializeAudio();
            }
        }
    }

    // Function to find audio element with immediate rate application
    function findAudioElement() {
        const allAudio = document.querySelectorAll(AUDIO_SELECTOR);

        if (allAudio.length > 0) {
            // Apply rate to all audio elements found
            applyPlaybackRateToAllAudio();

            // If we haven't set our main audio element yet, do so now
            if (!audioElement) {
                audioElement = allAudio[0];
                initializeAudio();
                // Make sure bubble icon gets updated immediately
                updateBubbleIcon();
                return true;
            } else {
                // Check if our tracked audio element has changed
                if (audioElement !== allAudio[0]) {
                    audioElement = allAudio[0];
                    initializeAudio();
                    updateBubbleIcon();
                }
            }
        }

        // Try again after a short delay if no audio found
        setTimeout(findAudioElement, 300);
        return false;
    }

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

        // Ensure correct playback rate is set before playing
        if (audioElement.playbackRate !== playbackRate) {
            audioElement.playbackRate = playbackRate;
        }

        // 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;

        // Immediately set 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 listener to ensure playback rate is maintained
        audioElement.addEventListener('ratechange', function() {
            // If something else changed the rate, reset it to our value
            if (this.playbackRate !== playbackRate) {
                console.log("[Audio Controls] Rate changed externally, resetting to", playbackRate);
                this.playbackRate = playbackRate;
            }
        });

        // Add event listeners
        audioElement.addEventListener('play', function() {
            updatePlayPauseButton();
            // Update bubble icon specifically
            updateBubbleIcon();
        });

        audioElement.addEventListener('pause', function() {
            updatePlayPauseButton();
            // Update bubble icon specifically
            updateBubbleIcon();
        });

        audioElement.addEventListener('ended', function() {
            updatePlayPauseButton();
            // Update bubble icon specifically
            updateBubbleIcon();
        });

        // iOS-specific: preload audio when possible
        if (isIOSSafari) {
            audioElement.preload = 'auto';
            audioElement.load();
        }

        // Initial update of bubble icon
        updateBubbleIcon();
    }

    // Function to handle audio loaded event
    function handleAudioLoaded() {
        if (!audioElement) return;

        // Ensure playback rate is set
        audioElement.playbackRate = playbackRate;

        // Update bubble icon based on current state
        updateBubbleIcon();

        // 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 the bubble icon based on audio state
    function updateBubbleIcon() {
        if (!audioElement) {
            bubbleIcon.innerHTML = '🔊'; // Default icon when no audio
            return;
        }

        if (audioElement.paused) {
            bubbleIcon.innerHTML = '▶️'; // Play icon when paused (showing what will happen on click)
        } else {
            bubbleIcon.innerHTML = '⏸️'; // Pause icon when playing (showing what will happen on click)
        }
    }

    // Function to update play/pause button state
    function updatePlayPauseButton() {
        if (!audioElement) return;

        // Ensure playback rate is correct
        if (audioElement.playbackRate !== playbackRate) {
            audioElement.playbackRate = playbackRate;
        }

        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 = '';
            }
        }

        // Update bubble icon
        updateBubbleIcon();
    }

    // Function to toggle play/pause - optimized for iOS
    function togglePlayPause() {
        if (!audioElement) return;

        // Set flag for user interaction (important for iOS)
        hasUserInteracted = true;

        // Ensure playback rate is set correctly
        if (audioElement.playbackRate !== playbackRate) {
            audioElement.playbackRate = playbackRate;
        }

        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;

        // Apply to all audio elements immediately
        applyPlaybackRateToAllAudio();

        // 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);

    // Start periodic rate check interval and UI updates
    function startRateCheckInterval() {
        if (rateCheckInterval) {
            clearInterval(rateCheckInterval);
        }

        rateCheckInterval = setInterval(() => {
            applyPlaybackRateToAllAudio();

            // Update bubble icon even when minimized
            if (isMinimized && audioElement) {
                updateBubbleIcon();
            }
        }, RATE_CHECK_INTERVAL);
    }

    // Create an observer to watch for new audio elements
    const audioObserver = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            if (mutation.addedNodes.length) {
                let foundNewAudio = false;

                mutation.addedNodes.forEach(function(node) {
                    if (node.nodeName === 'AUDIO' ||
                        (node.nodeType === 1 && node.querySelector(AUDIO_SELECTOR))) {
                        foundNewAudio = true;
                    }
                });

                if (foundNewAudio) {
                    // Immediately apply rate to any new audio elements
                    applyPlaybackRateToAllAudio();

                    // Reset audio element and reinitialize if needed
                    if (!audioElement) {
                        findAudioElement();
                    }
                }
            }
        });
    });

    // Function to immediately apply playback rate when the DOM is ready
    function onDOMReady() {
        console.log("[Audio Controls] DOM Content Loaded - initializing audio controls");

        // Immediately try to find and configure audio
        applyPlaybackRateToAllAudio();
        findAudioElement();

        // Start the rate check interval
        startRateCheckInterval();

        // Start observing the document
        audioObserver.observe(document.body, { childList: true, subtree: true });
    }

    // Initialize as soon as the DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', onDOMReady);
    } else {
        // DOM already loaded, initialize immediately
        onDOMReady();
    }

    // Double-check when page is fully loaded
    window.addEventListener('load', function() {
        console.log("[Audio Controls] Window loaded - ensuring audio playback rate");
        applyPlaybackRateToAllAudio();
    });

    // Initialize the view state
    updateViewState();
})();