Audio Controls with Auto-Play and Speed Management

Controls audio playback with speed adjustment and auto-play

当前为 2025-04-17 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Audio Controls with Auto-Play and Speed Management
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Controls audio playback with speed adjustment and auto-play
// @author       You
// @match        https://inovel12.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();
})();