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