- // ==UserScript==
- // @name Audio Controls with Auto-Play and Speed Management
- // @namespace http://tampermonkey.net/
- // @version 2.4
- // @description Controls audio playback with speed adjustment and enhanced auto-play methods
- // @author You
- // @match https://inovel*.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 = 3; // Maximum number of retry attempts
- const RETRY_DELAY = 1000; // Delay between retries in milliseconds
- const RATE_CHECK_INTERVAL = 800; // Check playback rate every 800ms
- const INACTIVITY_TIMEOUT = 7000; // Auto-minimize after 4 seconds of inactivity
-
- // New autoplay configuration
- const AUTOPLAY_METHOD_TIMEOUT = 2000; // Timeout between different autoplay methods
- const USE_INTERACTION_METHOD = true; // Method 1: User interaction simulation
- const USE_PROGRESSIVE_LOAD = true; // Method 2: Progressive loading
- const USE_AUDIO_CONTEXT = true; // Method 3: Web Audio API
-
- // 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;
- let inactivityTimer = null;
- let audioContext = null; // Store AudioContext instance
-
- // 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: 5px;
- min-width: 120px;
- `;
-
- // Create version display at the top (more compact)
- const versionDisplay = document.createElement('div');
- versionDisplay.style.cssText = `
- color: #aaaaaa;
- font-size: 9px;
- text-align: right;
- margin: 0 0 2px 0;
- font-style: italic;
- `;
- versionDisplay.textContent = `v2.4`;
- 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: 12px;
- text-align: center;
- margin-bottom: 5px;
- font-weight: bold;
- height: 18px; /* Fixed height to prevent layout shifts */
- `;
- countdownDisplay.textContent = '';
- controlPanel.appendChild(countdownDisplay);
-
- // Create play/pause button (icon only)
- const playPauseButton = document.createElement('button');
- playPauseButton.style.cssText = `
- background-color: #4CAF50;
- border: none;
- color: white;
- padding: 8px 0;
- text-align: center;
- font-size: 18px;
- border-radius: 4px;
- cursor: pointer;
- width: 100%;
- `;
- playPauseButton.innerHTML = '▶️';
- controlPanel.appendChild(playPauseButton);
-
- // Create speed control container
- const speedControlContainer = document.createElement('div');
- speedControlContainer.style.cssText = `
- display: flex;
- gap: 5px;
- width: 100%;
- `;
- controlPanel.appendChild(speedControlContainer);
-
- // Create speed down button (icon only)
- const speedDownButton = document.createElement('button');
- speedDownButton.style.cssText = `
- background-color: #795548;
- border: none;
- color: white;
- padding: 6px 0;
- text-align: center;
- font-size: 18px;
- border-radius: 4px;
- cursor: pointer;
- flex: 1;
- `;
- speedDownButton.innerHTML = '🐢';
- speedControlContainer.appendChild(speedDownButton);
-
- // Create speed up button (icon only)
- const speedUpButton = document.createElement('button');
- speedUpButton.style.cssText = `
- background-color: #009688;
- border: none;
- color: white;
- padding: 6px 0;
- text-align: center;
- font-size: 18px;
- border-radius: 4px;
- cursor: pointer;
- flex: 1;
- `;
- speedUpButton.innerHTML = '🐇';
- speedControlContainer.appendChild(speedUpButton);
-
- // Create speed display
- const speedDisplay = document.createElement('div');
- speedDisplay.style.cssText = `
- color: white;
- font-size: 12px;
- text-align: center;
- margin-top: 2px;
- `;
- speedDisplay.textContent = `${playbackRate.toFixed(1)}x`;
- controlPanel.appendChild(speedDisplay);
-
- // Create minimize button (icon only)
- const minimizeButton = document.createElement('button');
- minimizeButton.style.cssText = `
- background-color: #607D8B;
- border: none;
- color: white;
- padding: 4px 0;
- text-align: center;
- font-size: 14px;
- border-radius: 4px;
- cursor: pointer;
- margin-top: 5px;
- `;
- minimizeButton.innerHTML = '−';
- controlPanel.appendChild(minimizeButton);
-
- // Function to reset the inactivity timer
- function resetInactivityTimer() {
- if (inactivityTimer) {
- clearTimeout(inactivityTimer);
- inactivityTimer = null;
- }
-
- if (!isMinimized) {
- inactivityTimer = setTimeout(() => {
- toggleMinimized(); // Auto-minimize after timeout
- }, INACTIVITY_TIMEOUT);
- }
- }
-
- // Function to toggle between expanded and minimized views
- function toggleMinimized() {
- isMinimized = !isMinimized;
- updateViewState();
-
- // Reset inactivity timer when toggling
- resetInactivityTimer();
-
- // 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';
-
- // Clear any inactivity timer
- if (inactivityTimer) {
- clearTimeout(inactivityTimer);
- inactivityTimer = null;
- }
- } 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';
- }
-
- // Start inactivity timer
- resetInactivityTimer();
- }
- }
-
- // 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();
- }
- }
- });
-
- // Reset inactivity timer on user interaction with the control panel
- controlPanel.addEventListener('mouseenter', resetInactivityTimer);
- controlPanel.addEventListener('mousemove', resetInactivityTimer);
- controlPanel.addEventListener('click', resetInactivityTimer);
- controlPanel.addEventListener('touchstart', resetInactivityTimer);
-
- // Add click event for minimize button
- minimizeButton.addEventListener('click', toggleMinimized);
-
- // Method 1: Enhanced User Interaction Simulation
- function autoPlayWithInteraction() {
- if (!audioElement) return Promise.reject('No audio element found');
-
- return new Promise((resolve, reject) => {
- // Update UI to show we're using this method
- countdownDisplay.textContent = 'Method 1...';
-
- // Create and trigger various user interaction events
- const interactionEvents = ['touchend', 'click', 'keydown'];
- interactionEvents.forEach(eventType => {
- document.dispatchEvent(new Event(eventType, { bubbles: true }));
- });
-
- // Force load to ensure content is ready
- try {
- audioElement.load();
- } catch (e) {
- console.log('[Audio Controls] Load error:', e);
- }
-
- // Ensure volume is set to user's preferred level
- audioElement.volume = 1.0;
-
- // Ensure playback rate is set
- audioElement.playbackRate = playbackRate;
-
- // Try playback with interaction flag
- audioElement.play()
- .then(() => {
- console.log('[Audio Controls] Method 1 successful');
- resolve();
- })
- .catch(err => {
- console.log('[Audio Controls] Method 1 failed:', err);
- reject(err);
- });
- });
- }
-
- // Method 2: Progressive Media Loading Strategy
- function autoPlayWithProgressiveLoading() {
- if (!audioElement) return Promise.reject('No audio element found');
-
- return new Promise((resolve, reject) => {
- // Update UI
- countdownDisplay.textContent = 'Method 2...';
-
- // Store original values to restore later
- const originalVolume = audioElement.volume || 1.0;
- let volumeSetSuccessful = false;
- let loadTriggered = false;
- let canPlayHandlerSet = false;
-
- // Function to attempt playback once media can play
- const onCanPlay = () => {
- // Start with very low volume
- try {
- audioElement.volume = 0.001;
- volumeSetSuccessful = true;
- } catch (volErr) {
- console.log('[Audio Controls] Could not set volume:', volErr);
- }
-
- // Clean up handler to avoid duplicate calls
- if (canPlayHandlerSet) {
- audioElement.removeEventListener('canplay', onCanPlay);
- canPlayHandlerSet = false;
- }
-
- // Attempt playback
- audioElement.play()
- .then(() => {
- if (volumeSetSuccessful) {
- // Gradually restore volume
- const volumeIncrease = setInterval(() => {
- if (audioElement.volume < originalVolume) {
- audioElement.volume = Math.min(originalVolume, audioElement.volume + 0.1);
- } else {
- clearInterval(volumeIncrease);
- }
- }, 200);
- }
-
- console.log('[Audio Controls] Method 2 successful');
- resolve();
- })
- .catch(err => {
- console.log('[Audio Controls] Method 2 failed:', err);
- // Restore volume regardless
- if (volumeSetSuccessful) {
- audioElement.volume = originalVolume;
- }
- reject(err);
- });
- };
-
- // Set up timeout to avoid hanging
- const timeout = setTimeout(() => {
- if (canPlayHandlerSet) {
- audioElement.removeEventListener('canplay', onCanPlay);
- canPlayHandlerSet = false;
- }
-
- // Make one final direct attempt before rejecting
- audioElement.play()
- .then(resolve)
- .catch(err => reject('Timed out waiting for canplay event: ' + err));
-
- // Restore volume
- if (volumeSetSuccessful) {
- audioElement.volume = originalVolume;
- }
- }, 3000);
-
- try {
- // Listen for media ready state
- audioElement.addEventListener('canplay', onCanPlay);
- canPlayHandlerSet = true;
-
- // Force reload to trigger events
- try {
- audioElement.load();
- loadTriggered = true;
- console.log('[Audio Controls] Load triggered for Method 2');
- } catch (e) {
- console.log('[Audio Controls] Load failed:', e);
- }
-
- // If currentTime > 0, we're resuming, so try direct play as well
- if (audioElement.currentTime > 0) {
- // Try direct playback too for quicker resume
- audioElement.play()
- .then(() => {
- clearTimeout(timeout);
- if (canPlayHandlerSet) {
- audioElement.removeEventListener('canplay', onCanPlay);
- }
- resolve();
- })
- .catch(err => {
- console.log('[Audio Controls] Direct resume attempt failed:', err);
- // Continue waiting for canplay event
- });
- }
- } catch (e) {
- clearTimeout(timeout);
- reject(e);
- }
- });
- }
-
- // Method 3: Audio Context API Fallback
- function autoPlayWithAudioContext() {
- if (!audioElement) return Promise.reject('No audio element found');
-
- return new Promise((resolve, reject) => {
- // Update UI
- countdownDisplay.textContent = 'Method 3...';
-
- try {
- // Create audio context if not already created
- if (!audioContext) {
- const AudioContext = window.AudioContext || window.webkitAudioContext;
- if (!AudioContext) {
- return reject('AudioContext not supported');
- }
-
- audioContext = new AudioContext();
- }
-
- // Resume context if suspended
- if (audioContext.state === 'suspended') {
- audioContext.resume();
- }
-
- // Create a silent buffer to unlock audio context
- const buffer = audioContext.createBuffer(1, 1, 22050);
- const source = audioContext.createBufferSource();
- source.buffer = buffer;
- source.connect(audioContext.destination);
- source.start(0);
-
- // Try using a different approach with media source
- try {
- // Disconnect any existing connections
- audioElement._mediaSource = audioElement._mediaSource || audioContext.createMediaElementSource(audioElement);
- audioElement._mediaSource.connect(audioContext.destination);
- } catch (sourceErr) {
- // If we already created a media source (which can only be done once),
- // this will error but we can ignore it
- console.log('[Audio Controls] Media source already created:', sourceErr);
- }
-
- // Ensure correct playback rate
- audioElement.playbackRate = playbackRate;
-
- // Try to play using standard method after unlocking
- audioElement.play()
- .then(() => {
- console.log('[Audio Controls] Method 3 successful');
- resolve();
- })
- .catch(err => {
- console.log('[Audio Controls] Method 3 failed:', err);
- reject(err);
- });
- } catch (e) {
- console.log('[Audio Controls] Audio context error:', e);
- reject(e);
- }
- });
- }
-
- // Function to attempt playback with all methods sequentially
- function attemptAutoPlay() {
- // Reset retry counter
- retryAttempts = 0;
-
- // Update UI to show we're attempting playback
- playPauseButton.innerHTML = '⏳';
- countdownDisplay.textContent = 'Starting...';
-
- // Special case for resuming from a non-zero position
- const isResuming = audioElement && audioElement.currentTime > 0 && !audioElement.ended;
-
- // Chain promises to try each method in sequence
- let playPromise;
-
- if (isResuming) {
- // If resuming, attempt direct playback first
- playPromise = new Promise((resolve, reject) => {
- console.log("[Audio Controls] Attempting direct resume from:", audioElement.currentTime);
- countdownDisplay.textContent = 'Resuming...';
-
- audioElement.play()
- .then(() => {
- console.log("[Audio Controls] Direct resume successful");
- resolve();
- })
- .catch(err => {
- console.log("[Audio Controls] Direct resume failed:", err);
- reject(err);
- });
- });
- } else if (USE_INTERACTION_METHOD) {
- playPromise = autoPlayWithInteraction();
- } else {
- playPromise = Promise.reject('Method 1 disabled');
- }
-
- // Try Method 2 if initial method fails
- playPromise
- .catch(err => {
- console.log('[Audio Controls] Trying next method...');
- if (USE_PROGRESSIVE_LOAD) {
- return new Promise(resolve => {
- // Add a short delay before trying the next method
- setTimeout(() => {
- autoPlayWithProgressiveLoading().then(resolve).catch(err => {
- throw err;
- });
- }, 500);
- });
- }
- return Promise.reject('Method 2 disabled');
- })
- // Try Method 3 if Method 2 fails
- .catch(err => {
- console.log('[Audio Controls] Trying final method...');
- if (USE_AUDIO_CONTEXT) {
- return new Promise(resolve => {
- // Add a short delay before trying the next method
- setTimeout(() => {
- autoPlayWithAudioContext().then(resolve).catch(err => {
- throw err;
- });
- }, 500);
- });
- }
- return Promise.reject('Method 3 disabled');
- })
- // Handle final success or failure
- .then(() => {
- // Success with any method
- updatePlayPauseButton();
- countdownDisplay.textContent = '';
- hasUserInteracted = true;
- })
- .catch(err => {
- console.log('[Audio Controls] All auto-play methods failed:', err);
- // All methods failed, show message and enable manual play
- countdownDisplay.textContent = 'Tap to play';
- playPauseButton.innerHTML = '▶️';
- updatePlayPauseButton();
- });
- }
-
- // 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) {
- // 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;
-
- // Use new autoplay function when countdown reaches zero
- if (audioElement) {
- attemptAutoPlay();
- }
- }
- }, 1000);
- }
-
- // Function to update countdown display
- function updateCountdownDisplay(seconds) {
- countdownDisplay.textContent = `Auto ${seconds}s`;
- }
-
- // Function to play audio with retry mechanism (legacy - kept for backward compatibility)
- function playAudioWithRetry() {
- attemptAutoPlay();
- }
-
- // Function to initialize audio controls
- function initializeAudio() {
- if (!audioElement) return;
-
- // Immediately set playback rate
- audioElement.playbackRate = playbackRate;
-
- // Set preload to auto for better playback
- audioElement.preload = 'auto';
-
- // 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();
- });
-
- // 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();
-
- // 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.innerHTML = '▶️';
- playPauseButton.style.backgroundColor = '#4CAF50';
- } else {
- playPauseButton.innerHTML = '⏸️';
- playPauseButton.style.backgroundColor = '#F44336';
-
- // If playing, clear countdown
- if (countdownTimer) {
- clearInterval(countdownTimer);
- countdownTimer = null;
- countdownDisplay.textContent = '';
- }
- }
-
- // Update bubble icon
- updateBubbleIcon();
- }
-
- // Function to create a "resume" toast notification
- function showResumeToast(position) {
- // Create and style the toast
- const toast = document.createElement('div');
- toast.style.cssText = `
- position: fixed;
- bottom: 80px;
- left: 50%;
- transform: translateX(-50%);
- background-color: rgba(0, 0, 0, 0.8);
- color: white;
- padding: 10px 15px;
- border-radius: 5px;
- font-size: 14px;
- z-index: 10000;
- opacity: 0;
- transition: opacity 0.3s ease;
- `;
-
- // Format the time nicely
- const formattedTime = formatTime(position);
- toast.textContent = `Resuming from ${formattedTime}`;
-
- // Add to document
- document.body.appendChild(toast);
-
- // Fade in
- setTimeout(() => {
- toast.style.opacity = '1';
- }, 10);
-
- // Remove after 2 seconds
- setTimeout(() => {
- toast.style.opacity = '0';
- setTimeout(() => {
- document.body.removeChild(toast);
- }, 300);
- }, 2000);
- }
-
- // Function to toggle play/pause with better resume handling
- function togglePlayPause() {
- if (!audioElement) return;
-
- // Reset inactivity timer on user interaction
- resetInactivityTimer();
-
- // Set flag for user interaction
- hasUserInteracted = true;
-
- // Ensure playback rate is set correctly
- if (audioElement.playbackRate !== playbackRate) {
- audioElement.playbackRate = playbackRate;
- }
-
- if (audioElement.paused) {
- // Check if currentTime is > 0, indicating playback was started before
- if (audioElement.currentTime > 0 && !audioElement.ended) {
- // Show resume toast if resuming from a significant position (more than 3 seconds in)
- if (audioElement.currentTime > 3) {
- showResumeToast(audioElement.currentTime);
- }
-
- // Simply resume playback without using autoplay methods
- console.log("[Audio Controls] Resuming playback from position:", audioElement.currentTime);
-
- // Update UI immediately to provide feedback
- playPauseButton.innerHTML = '⏳';
- countdownDisplay.textContent = 'Resuming...';
-
- audioElement.play()
- .then(() => {
- updatePlayPauseButton();
- countdownDisplay.textContent = '';
- })
- .catch(err => {
- console.log("[Audio Controls] Resume failed, trying autoplay methods:", err);
- attemptAutoPlay();
- });
- } else {
- // If starting from beginning or after ended, try all methods
- attemptAutoPlay();
- }
- } else {
- audioElement.pause();
- updatePlayPauseButton();
- }
- }
-
- // Function to update playback speed
- function updatePlaybackSpeed(newRate) {
- // Reset inactivity timer on user interaction
- resetInactivityTimer();
-
- playbackRate = newRate;
-
- // Apply to all audio elements immediately
- applyPlaybackRateToAllAudio();
-
- // Update display
- speedDisplay.textContent = `${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);
-
- // Make bubble clickable with smart behavior
- bubbleView.addEventListener('click', function() {
- if (audioElement) {
- if (audioElement.paused) {
- // If audio exists and is paused, try to play it
- togglePlayPause();
-
- // After attempting to play, check if we're still paused (play failed)
- // and expand the controls in that case for more options
- setTimeout(() => {
- if (audioElement.paused) {
- toggleMinimized();
- }
- }, 300);
- } else {
- // If we're playing, expand the panel
- toggleMinimized();
- }
- } else {
- // If no audio, just expand
- toggleMinimized();
- }
- });
-
- // 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");
-
- // Try to unlock audio context early for iOS
- try {
- const AudioContext = window.AudioContext || window.webkitAudioContext;
- if (AudioContext) {
- audioContext = new AudioContext();
-
- // Create and play silent buffer
- const buffer = audioContext.createBuffer(1, 1, 22050);
- const source = audioContext.createBufferSource();
- source.buffer = buffer;
- source.connect(audioContext.destination);
- source.start(0);
-
- // Resume if needed
- if (audioContext.state === 'suspended') {
- audioContext.resume();
- }
-
- console.log("[Audio Controls] AudioContext initialized:", audioContext.state);
- }
- } catch (e) {
- console.log("[Audio Controls] Early AudioContext initialization error:", e);
- }
-
- // Set up global event handlers for iOS audio unlocking
- const unlockEvents = ['touchstart', 'touchend', 'mousedown', 'keydown'];
- const unlockAudio = function() {
- if (audioContext && audioContext.state === 'suspended') {
- audioContext.resume();
- }
-
- // Remove these event listeners once used
- unlockEvents.forEach(event => {
- document.removeEventListener(event, unlockAudio);
- });
- };
-
- // Add unlock event listeners
- unlockEvents.forEach(event => {
- document.addEventListener(event, unlockAudio, false);
- });
-
- // 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();
- })();