Greasy Fork 支持简体中文。

Audio Controls with Auto-Play and Speed Management

Controls audio playback with speed adjustment and enhanced auto-play methods

  1. // ==UserScript==
  2. // @name Audio Controls with Auto-Play and Speed Management
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.4
  5. // @description Controls audio playback with speed adjustment and enhanced auto-play methods
  6. // @author You
  7. // @match https://inovel*.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // Configuration
  15. const DEFAULT_PLAYBACK_RATE = 0.7;
  16. const AUTO_PLAY_DELAY = 5000; // 5 seconds
  17. const AUDIO_SELECTOR = 'audio[controls]';
  18. const MAX_RETRY_ATTEMPTS = 3; // Maximum number of retry attempts
  19. const RETRY_DELAY = 1000; // Delay between retries in milliseconds
  20. const RATE_CHECK_INTERVAL = 800; // Check playback rate every 800ms
  21. const INACTIVITY_TIMEOUT = 7000; // Auto-minimize after 4 seconds of inactivity
  22.  
  23. // New autoplay configuration
  24. const AUTOPLAY_METHOD_TIMEOUT = 2000; // Timeout between different autoplay methods
  25. const USE_INTERACTION_METHOD = true; // Method 1: User interaction simulation
  26. const USE_PROGRESSIVE_LOAD = true; // Method 2: Progressive loading
  27. const USE_AUDIO_CONTEXT = true; // Method 3: Web Audio API
  28.  
  29. // State variables
  30. let audioElement = null;
  31. let playbackRate = parseFloat(localStorage.getItem('audio_playback_rate')) || DEFAULT_PLAYBACK_RATE;
  32. let isMinimized = localStorage.getItem('audio_controls_minimized') === 'true' || false;
  33. let countdownTimer = null;
  34. let rateCheckInterval = null;
  35. let retryAttempts = 0;
  36. let hasUserInteracted = false;
  37. let lastRateApplication = 0;
  38. let inactivityTimer = null;
  39. let audioContext = null; // Store AudioContext instance
  40.  
  41. // Position settings - load from localStorage or use defaults
  42. let bubblePosition = JSON.parse(localStorage.getItem('audio_bubble_position')) || { top: '20px', left: '20px' };
  43.  
  44. // Create main container for all controls
  45. const mainContainer = document.createElement('div');
  46. mainContainer.style.cssText = `
  47. position: fixed;
  48. z-index: 9999;
  49. font-family: Arial, sans-serif;
  50. `;
  51. document.body.appendChild(mainContainer);
  52.  
  53. // Create the expanded control panel
  54. const controlPanel = document.createElement('div');
  55. controlPanel.className = 'audio-control-panel';
  56. controlPanel.style.cssText = `
  57. background-color: rgba(0, 0, 0, 0.7);
  58. padding: 10px;
  59. border-radius: 8px;
  60. display: flex;
  61. flex-direction: column;
  62. gap: 5px;
  63. min-width: 120px;
  64. `;
  65.  
  66. // Create version display at the top (more compact)
  67. const versionDisplay = document.createElement('div');
  68. versionDisplay.style.cssText = `
  69. color: #aaaaaa;
  70. font-size: 9px;
  71. text-align: right;
  72. margin: 0 0 2px 0;
  73. font-style: italic;
  74. `;
  75. versionDisplay.textContent = `v2.4`;
  76. controlPanel.appendChild(versionDisplay);
  77.  
  78. // Create the minimized bubble view
  79. const bubbleView = document.createElement('div');
  80. bubbleView.className = 'audio-bubble';
  81. bubbleView.style.cssText = `
  82. width: 40px;
  83. height: 40px;
  84. border-radius: 50%;
  85. background-color: rgba(0, 0, 0, 0.7);
  86. display: flex;
  87. justify-content: center;
  88. align-items: center;
  89. cursor: pointer;
  90. user-select: none;
  91. `;
  92.  
  93. // Create bubble icon
  94. const bubbleIcon = document.createElement('div');
  95. bubbleIcon.style.cssText = `
  96. font-size: 20px;
  97. color: white;
  98. `;
  99. bubbleIcon.innerHTML = '🔊'; // Will be updated based on audio state
  100. bubbleView.appendChild(bubbleIcon);
  101.  
  102. // Create countdown/message display
  103. const countdownDisplay = document.createElement('div');
  104. countdownDisplay.style.cssText = `
  105. color: #ffcc00;
  106. font-size: 12px;
  107. text-align: center;
  108. margin-bottom: 5px;
  109. font-weight: bold;
  110. height: 18px; /* Fixed height to prevent layout shifts */
  111. `;
  112. countdownDisplay.textContent = '';
  113. controlPanel.appendChild(countdownDisplay);
  114.  
  115. // Create play/pause button (icon only)
  116. const playPauseButton = document.createElement('button');
  117. playPauseButton.style.cssText = `
  118. background-color: #4CAF50;
  119. border: none;
  120. color: white;
  121. padding: 8px 0;
  122. text-align: center;
  123. font-size: 18px;
  124. border-radius: 4px;
  125. cursor: pointer;
  126. width: 100%;
  127. `;
  128. playPauseButton.innerHTML = '▶️';
  129. controlPanel.appendChild(playPauseButton);
  130.  
  131. // Create speed control container
  132. const speedControlContainer = document.createElement('div');
  133. speedControlContainer.style.cssText = `
  134. display: flex;
  135. gap: 5px;
  136. width: 100%;
  137. `;
  138. controlPanel.appendChild(speedControlContainer);
  139.  
  140. // Create speed down button (icon only)
  141. const speedDownButton = document.createElement('button');
  142. speedDownButton.style.cssText = `
  143. background-color: #795548;
  144. border: none;
  145. color: white;
  146. padding: 6px 0;
  147. text-align: center;
  148. font-size: 18px;
  149. border-radius: 4px;
  150. cursor: pointer;
  151. flex: 1;
  152. `;
  153. speedDownButton.innerHTML = '🐢';
  154. speedControlContainer.appendChild(speedDownButton);
  155.  
  156. // Create speed up button (icon only)
  157. const speedUpButton = document.createElement('button');
  158. speedUpButton.style.cssText = `
  159. background-color: #009688;
  160. border: none;
  161. color: white;
  162. padding: 6px 0;
  163. text-align: center;
  164. font-size: 18px;
  165. border-radius: 4px;
  166. cursor: pointer;
  167. flex: 1;
  168. `;
  169. speedUpButton.innerHTML = '🐇';
  170. speedControlContainer.appendChild(speedUpButton);
  171.  
  172. // Create speed display
  173. const speedDisplay = document.createElement('div');
  174. speedDisplay.style.cssText = `
  175. color: white;
  176. font-size: 12px;
  177. text-align: center;
  178. margin-top: 2px;
  179. `;
  180. speedDisplay.textContent = `${playbackRate.toFixed(1)}x`;
  181. controlPanel.appendChild(speedDisplay);
  182.  
  183. // Create minimize button (icon only)
  184. const minimizeButton = document.createElement('button');
  185. minimizeButton.style.cssText = `
  186. background-color: #607D8B;
  187. border: none;
  188. color: white;
  189. padding: 4px 0;
  190. text-align: center;
  191. font-size: 14px;
  192. border-radius: 4px;
  193. cursor: pointer;
  194. margin-top: 5px;
  195. `;
  196. minimizeButton.innerHTML = '−';
  197. controlPanel.appendChild(minimizeButton);
  198.  
  199. // Function to reset the inactivity timer
  200. function resetInactivityTimer() {
  201. if (inactivityTimer) {
  202. clearTimeout(inactivityTimer);
  203. inactivityTimer = null;
  204. }
  205.  
  206. if (!isMinimized) {
  207. inactivityTimer = setTimeout(() => {
  208. toggleMinimized(); // Auto-minimize after timeout
  209. }, INACTIVITY_TIMEOUT);
  210. }
  211. }
  212.  
  213. // Function to toggle between expanded and minimized views
  214. function toggleMinimized() {
  215. isMinimized = !isMinimized;
  216. updateViewState();
  217.  
  218. // Reset inactivity timer when toggling
  219. resetInactivityTimer();
  220.  
  221. // Save state to localStorage
  222. localStorage.setItem('audio_controls_minimized', isMinimized);
  223. }
  224.  
  225. // Function to update the current view based on minimized state
  226. function updateViewState() {
  227. // Clear the container first
  228. while (mainContainer.firstChild) {
  229. mainContainer.removeChild(mainContainer.firstChild);
  230. }
  231.  
  232. if (isMinimized) {
  233. // Show bubble view
  234. mainContainer.appendChild(bubbleView);
  235.  
  236. // Set position based on saved values
  237. mainContainer.style.top = bubblePosition.top;
  238. mainContainer.style.left = bubblePosition.left;
  239. mainContainer.style.right = 'auto';
  240. mainContainer.style.bottom = 'auto';
  241.  
  242. // Clear any inactivity timer
  243. if (inactivityTimer) {
  244. clearTimeout(inactivityTimer);
  245. inactivityTimer = null;
  246. }
  247. } else {
  248. // Show expanded control panel
  249. mainContainer.appendChild(controlPanel);
  250.  
  251. // If coming from minimized state, place in the same position
  252. // Otherwise use default bottom right
  253. if (bubblePosition) {
  254. mainContainer.style.top = bubblePosition.top;
  255. mainContainer.style.left = bubblePosition.left;
  256. mainContainer.style.right = 'auto';
  257. mainContainer.style.bottom = 'auto';
  258. } else {
  259. mainContainer.style.top = 'auto';
  260. mainContainer.style.left = 'auto';
  261. mainContainer.style.right = '20px';
  262. mainContainer.style.bottom = '20px';
  263. }
  264.  
  265. // Start inactivity timer
  266. resetInactivityTimer();
  267. }
  268. }
  269.  
  270. // Make only the bubble draggable
  271. let isDragging = false;
  272. let dragOffsetX = 0;
  273. let dragOffsetY = 0;
  274.  
  275. bubbleView.addEventListener('mousedown', function(e) {
  276. // Only initiate drag if user holds for a brief moment
  277. setTimeout(() => {
  278. if (e.buttons === 1) { // Left mouse button
  279. isDragging = true;
  280. dragOffsetX = e.clientX - mainContainer.getBoundingClientRect().left;
  281. dragOffsetY = e.clientY - mainContainer.getBoundingClientRect().top;
  282. bubbleView.style.cursor = 'grabbing';
  283. }
  284. }, 100);
  285. });
  286.  
  287. document.addEventListener('mousemove', function(e) {
  288. if (!isDragging || !isMinimized) return;
  289.  
  290. e.preventDefault();
  291.  
  292. // Only allow vertical movement (Y-axis)
  293. const newTop = e.clientY - dragOffsetY;
  294.  
  295. // Keep within viewport bounds
  296. const maxY = window.innerHeight - bubbleView.offsetHeight;
  297.  
  298. // Only update Y position, keep X position the same
  299. mainContainer.style.top = `${Math.max(0, Math.min(maxY, newTop))}px`;
  300. });
  301.  
  302. document.addEventListener('mouseup', function(event) {
  303. if (isDragging && isMinimized) {
  304. isDragging = false;
  305. bubbleView.style.cursor = 'pointer';
  306.  
  307. // Save the position (only top changes, left stays the same)
  308. bubblePosition = {
  309. top: mainContainer.style.top,
  310. left: bubblePosition.left // Keep the same left position
  311. };
  312. localStorage.setItem('audio_bubble_position', JSON.stringify(bubblePosition));
  313.  
  314. // Prevent click if we were dragging
  315. event.preventDefault();
  316. return false;
  317. } else if (isMinimized && (event.target === bubbleView || bubbleView.contains(event.target))) {
  318. // If it was a click (not drag) on the bubble, expand
  319. toggleMinimized();
  320. }
  321. });
  322.  
  323. // Add touch support for mobile devices - only for bubble
  324. bubbleView.addEventListener('touchstart', function(e) {
  325. const touch = e.touches[0];
  326. isDragging = true;
  327. dragOffsetX = touch.clientX - mainContainer.getBoundingClientRect().left;
  328. dragOffsetY = touch.clientY - mainContainer.getBoundingClientRect().top;
  329.  
  330. // Prevent scrolling while dragging
  331. e.preventDefault();
  332. });
  333.  
  334. document.addEventListener('touchmove', function(e) {
  335. if (!isDragging || !isMinimized) return;
  336.  
  337. const touch = e.touches[0];
  338.  
  339. // Only allow vertical movement (Y-axis)
  340. const newTop = touch.clientY - dragOffsetY;
  341.  
  342. // Keep within viewport bounds
  343. const maxY = window.innerHeight - bubbleView.offsetHeight;
  344.  
  345. // Only update Y position, keep X position the same
  346. mainContainer.style.top = `${Math.max(0, Math.min(maxY, newTop))}px`;
  347.  
  348. // Prevent scrolling while dragging
  349. e.preventDefault();
  350. });
  351.  
  352. document.addEventListener('touchend', function(event) {
  353. if (isDragging && isMinimized) {
  354. isDragging = false;
  355.  
  356. // Save the position (only top changes, left stays the same)
  357. bubblePosition = {
  358. top: mainContainer.style.top,
  359. left: bubblePosition.left // Keep the same left position
  360. };
  361. localStorage.setItem('audio_bubble_position', JSON.stringify(bubblePosition));
  362.  
  363. // If touch distance was very small, treat as click
  364. const touchMoved = Math.abs(event.changedTouches[0].clientY - (parseInt(mainContainer.style.top) + dragOffsetY)) > 5;
  365.  
  366. if (!touchMoved && (event.target === bubbleView || bubbleView.contains(event.target))) {
  367. toggleMinimized();
  368. }
  369. }
  370. });
  371.  
  372. // Reset inactivity timer on user interaction with the control panel
  373. controlPanel.addEventListener('mouseenter', resetInactivityTimer);
  374. controlPanel.addEventListener('mousemove', resetInactivityTimer);
  375. controlPanel.addEventListener('click', resetInactivityTimer);
  376. controlPanel.addEventListener('touchstart', resetInactivityTimer);
  377.  
  378. // Add click event for minimize button
  379. minimizeButton.addEventListener('click', toggleMinimized);
  380.  
  381. // Method 1: Enhanced User Interaction Simulation
  382. function autoPlayWithInteraction() {
  383. if (!audioElement) return Promise.reject('No audio element found');
  384.  
  385. return new Promise((resolve, reject) => {
  386. // Update UI to show we're using this method
  387. countdownDisplay.textContent = 'Method 1...';
  388.  
  389. // Create and trigger various user interaction events
  390. const interactionEvents = ['touchend', 'click', 'keydown'];
  391. interactionEvents.forEach(eventType => {
  392. document.dispatchEvent(new Event(eventType, { bubbles: true }));
  393. });
  394.  
  395. // Force load to ensure content is ready
  396. try {
  397. audioElement.load();
  398. } catch (e) {
  399. console.log('[Audio Controls] Load error:', e);
  400. }
  401.  
  402. // Ensure volume is set to user's preferred level
  403. audioElement.volume = 1.0;
  404.  
  405. // Ensure playback rate is set
  406. audioElement.playbackRate = playbackRate;
  407.  
  408. // Try playback with interaction flag
  409. audioElement.play()
  410. .then(() => {
  411. console.log('[Audio Controls] Method 1 successful');
  412. resolve();
  413. })
  414. .catch(err => {
  415. console.log('[Audio Controls] Method 1 failed:', err);
  416. reject(err);
  417. });
  418. });
  419. }
  420.  
  421. // Method 2: Progressive Media Loading Strategy
  422. function autoPlayWithProgressiveLoading() {
  423. if (!audioElement) return Promise.reject('No audio element found');
  424.  
  425. return new Promise((resolve, reject) => {
  426. // Update UI
  427. countdownDisplay.textContent = 'Method 2...';
  428.  
  429. // Store original values to restore later
  430. const originalVolume = audioElement.volume || 1.0;
  431. let volumeSetSuccessful = false;
  432. let loadTriggered = false;
  433. let canPlayHandlerSet = false;
  434.  
  435. // Function to attempt playback once media can play
  436. const onCanPlay = () => {
  437. // Start with very low volume
  438. try {
  439. audioElement.volume = 0.001;
  440. volumeSetSuccessful = true;
  441. } catch (volErr) {
  442. console.log('[Audio Controls] Could not set volume:', volErr);
  443. }
  444.  
  445. // Clean up handler to avoid duplicate calls
  446. if (canPlayHandlerSet) {
  447. audioElement.removeEventListener('canplay', onCanPlay);
  448. canPlayHandlerSet = false;
  449. }
  450.  
  451. // Attempt playback
  452. audioElement.play()
  453. .then(() => {
  454. if (volumeSetSuccessful) {
  455. // Gradually restore volume
  456. const volumeIncrease = setInterval(() => {
  457. if (audioElement.volume < originalVolume) {
  458. audioElement.volume = Math.min(originalVolume, audioElement.volume + 0.1);
  459. } else {
  460. clearInterval(volumeIncrease);
  461. }
  462. }, 200);
  463. }
  464.  
  465. console.log('[Audio Controls] Method 2 successful');
  466. resolve();
  467. })
  468. .catch(err => {
  469. console.log('[Audio Controls] Method 2 failed:', err);
  470. // Restore volume regardless
  471. if (volumeSetSuccessful) {
  472. audioElement.volume = originalVolume;
  473. }
  474. reject(err);
  475. });
  476. };
  477.  
  478. // Set up timeout to avoid hanging
  479. const timeout = setTimeout(() => {
  480. if (canPlayHandlerSet) {
  481. audioElement.removeEventListener('canplay', onCanPlay);
  482. canPlayHandlerSet = false;
  483. }
  484.  
  485. // Make one final direct attempt before rejecting
  486. audioElement.play()
  487. .then(resolve)
  488. .catch(err => reject('Timed out waiting for canplay event: ' + err));
  489.  
  490. // Restore volume
  491. if (volumeSetSuccessful) {
  492. audioElement.volume = originalVolume;
  493. }
  494. }, 3000);
  495.  
  496. try {
  497. // Listen for media ready state
  498. audioElement.addEventListener('canplay', onCanPlay);
  499. canPlayHandlerSet = true;
  500.  
  501. // Force reload to trigger events
  502. try {
  503. audioElement.load();
  504. loadTriggered = true;
  505. console.log('[Audio Controls] Load triggered for Method 2');
  506. } catch (e) {
  507. console.log('[Audio Controls] Load failed:', e);
  508. }
  509.  
  510. // If currentTime > 0, we're resuming, so try direct play as well
  511. if (audioElement.currentTime > 0) {
  512. // Try direct playback too for quicker resume
  513. audioElement.play()
  514. .then(() => {
  515. clearTimeout(timeout);
  516. if (canPlayHandlerSet) {
  517. audioElement.removeEventListener('canplay', onCanPlay);
  518. }
  519. resolve();
  520. })
  521. .catch(err => {
  522. console.log('[Audio Controls] Direct resume attempt failed:', err);
  523. // Continue waiting for canplay event
  524. });
  525. }
  526. } catch (e) {
  527. clearTimeout(timeout);
  528. reject(e);
  529. }
  530. });
  531. }
  532.  
  533. // Method 3: Audio Context API Fallback
  534. function autoPlayWithAudioContext() {
  535. if (!audioElement) return Promise.reject('No audio element found');
  536.  
  537. return new Promise((resolve, reject) => {
  538. // Update UI
  539. countdownDisplay.textContent = 'Method 3...';
  540.  
  541. try {
  542. // Create audio context if not already created
  543. if (!audioContext) {
  544. const AudioContext = window.AudioContext || window.webkitAudioContext;
  545. if (!AudioContext) {
  546. return reject('AudioContext not supported');
  547. }
  548.  
  549. audioContext = new AudioContext();
  550. }
  551.  
  552. // Resume context if suspended
  553. if (audioContext.state === 'suspended') {
  554. audioContext.resume();
  555. }
  556.  
  557. // Create a silent buffer to unlock audio context
  558. const buffer = audioContext.createBuffer(1, 1, 22050);
  559. const source = audioContext.createBufferSource();
  560. source.buffer = buffer;
  561. source.connect(audioContext.destination);
  562. source.start(0);
  563.  
  564. // Try using a different approach with media source
  565. try {
  566. // Disconnect any existing connections
  567. audioElement._mediaSource = audioElement._mediaSource || audioContext.createMediaElementSource(audioElement);
  568. audioElement._mediaSource.connect(audioContext.destination);
  569. } catch (sourceErr) {
  570. // If we already created a media source (which can only be done once),
  571. // this will error but we can ignore it
  572. console.log('[Audio Controls] Media source already created:', sourceErr);
  573. }
  574.  
  575. // Ensure correct playback rate
  576. audioElement.playbackRate = playbackRate;
  577.  
  578. // Try to play using standard method after unlocking
  579. audioElement.play()
  580. .then(() => {
  581. console.log('[Audio Controls] Method 3 successful');
  582. resolve();
  583. })
  584. .catch(err => {
  585. console.log('[Audio Controls] Method 3 failed:', err);
  586. reject(err);
  587. });
  588. } catch (e) {
  589. console.log('[Audio Controls] Audio context error:', e);
  590. reject(e);
  591. }
  592. });
  593. }
  594.  
  595. // Function to attempt playback with all methods sequentially
  596. function attemptAutoPlay() {
  597. // Reset retry counter
  598. retryAttempts = 0;
  599.  
  600. // Update UI to show we're attempting playback
  601. playPauseButton.innerHTML = '⏳';
  602. countdownDisplay.textContent = 'Starting...';
  603.  
  604. // Special case for resuming from a non-zero position
  605. const isResuming = audioElement && audioElement.currentTime > 0 && !audioElement.ended;
  606.  
  607. // Chain promises to try each method in sequence
  608. let playPromise;
  609.  
  610. if (isResuming) {
  611. // If resuming, attempt direct playback first
  612. playPromise = new Promise((resolve, reject) => {
  613. console.log("[Audio Controls] Attempting direct resume from:", audioElement.currentTime);
  614. countdownDisplay.textContent = 'Resuming...';
  615.  
  616. audioElement.play()
  617. .then(() => {
  618. console.log("[Audio Controls] Direct resume successful");
  619. resolve();
  620. })
  621. .catch(err => {
  622. console.log("[Audio Controls] Direct resume failed:", err);
  623. reject(err);
  624. });
  625. });
  626. } else if (USE_INTERACTION_METHOD) {
  627. playPromise = autoPlayWithInteraction();
  628. } else {
  629. playPromise = Promise.reject('Method 1 disabled');
  630. }
  631.  
  632. // Try Method 2 if initial method fails
  633. playPromise
  634. .catch(err => {
  635. console.log('[Audio Controls] Trying next method...');
  636. if (USE_PROGRESSIVE_LOAD) {
  637. return new Promise(resolve => {
  638. // Add a short delay before trying the next method
  639. setTimeout(() => {
  640. autoPlayWithProgressiveLoading().then(resolve).catch(err => {
  641. throw err;
  642. });
  643. }, 500);
  644. });
  645. }
  646. return Promise.reject('Method 2 disabled');
  647. })
  648. // Try Method 3 if Method 2 fails
  649. .catch(err => {
  650. console.log('[Audio Controls] Trying final method...');
  651. if (USE_AUDIO_CONTEXT) {
  652. return new Promise(resolve => {
  653. // Add a short delay before trying the next method
  654. setTimeout(() => {
  655. autoPlayWithAudioContext().then(resolve).catch(err => {
  656. throw err;
  657. });
  658. }, 500);
  659. });
  660. }
  661. return Promise.reject('Method 3 disabled');
  662. })
  663. // Handle final success or failure
  664. .then(() => {
  665. // Success with any method
  666. updatePlayPauseButton();
  667. countdownDisplay.textContent = '';
  668. hasUserInteracted = true;
  669. })
  670. .catch(err => {
  671. console.log('[Audio Controls] All auto-play methods failed:', err);
  672. // All methods failed, show message and enable manual play
  673. countdownDisplay.textContent = 'Tap to play';
  674. playPauseButton.innerHTML = '▶️';
  675. updatePlayPauseButton();
  676. });
  677. }
  678.  
  679. // Apply playback rate to all audio elements
  680. function applyPlaybackRateToAllAudio() {
  681. const now = Date.now();
  682. // Throttle frequent applications (but still allow force flag to override)
  683. if ((now - lastRateApplication) < 500) return;
  684.  
  685. lastRateApplication = now;
  686. const allAudioElements = document.querySelectorAll(AUDIO_SELECTOR);
  687.  
  688. if (allAudioElements.length > 0) {
  689. allAudioElements.forEach(audio => {
  690. if (audio.playbackRate !== playbackRate) {
  691. audio.playbackRate = playbackRate;
  692. console.log(`[Audio Controls] Applied rate ${playbackRate.toFixed(1)}x to audio element`);
  693. }
  694. });
  695.  
  696. // If our main audio element isn't set yet, use the first one found
  697. if (!audioElement && allAudioElements.length > 0) {
  698. audioElement = allAudioElements[0];
  699. initializeAudio();
  700. }
  701. }
  702. }
  703.  
  704. // Function to find audio element with immediate rate application
  705. function findAudioElement() {
  706. const allAudio = document.querySelectorAll(AUDIO_SELECTOR);
  707.  
  708. if (allAudio.length > 0) {
  709. // Apply rate to all audio elements found
  710. applyPlaybackRateToAllAudio();
  711.  
  712. // If we haven't set our main audio element yet, do so now
  713. if (!audioElement) {
  714. audioElement = allAudio[0];
  715. initializeAudio();
  716. // Make sure bubble icon gets updated immediately
  717. updateBubbleIcon();
  718. return true;
  719. } else {
  720. // Check if our tracked audio element has changed
  721. if (audioElement !== allAudio[0]) {
  722. audioElement = allAudio[0];
  723. initializeAudio();
  724. updateBubbleIcon();
  725. }
  726. }
  727. }
  728.  
  729. // Try again after a short delay if no audio found
  730. setTimeout(findAudioElement, 300);
  731. return false;
  732. }
  733.  
  734. // Function to format time in MM:SS
  735. function formatTime(seconds) {
  736. const minutes = Math.floor(seconds / 60);
  737. const secs = Math.floor(seconds % 60);
  738. return `${minutes}:${secs.toString().padStart(2, '0')}`;
  739. }
  740.  
  741. // Function to run the countdown timer
  742. function startCountdown(seconds) {
  743. // Clear any existing countdown
  744. if (countdownTimer) {
  745. clearInterval(countdownTimer);
  746. }
  747.  
  748. let remainingSeconds = seconds;
  749. updateCountdownDisplay(remainingSeconds);
  750.  
  751. countdownTimer = setInterval(() => {
  752. remainingSeconds--;
  753. updateCountdownDisplay(remainingSeconds);
  754.  
  755. if (remainingSeconds <= 0) {
  756. clearInterval(countdownTimer);
  757. countdownTimer = null;
  758.  
  759. // Use new autoplay function when countdown reaches zero
  760. if (audioElement) {
  761. attemptAutoPlay();
  762. }
  763. }
  764. }, 1000);
  765. }
  766.  
  767. // Function to update countdown display
  768. function updateCountdownDisplay(seconds) {
  769. countdownDisplay.textContent = `Auto ${seconds}s`;
  770. }
  771.  
  772. // Function to play audio with retry mechanism (legacy - kept for backward compatibility)
  773. function playAudioWithRetry() {
  774. attemptAutoPlay();
  775. }
  776.  
  777. // Function to initialize audio controls
  778. function initializeAudio() {
  779. if (!audioElement) return;
  780.  
  781. // Immediately set playback rate
  782. audioElement.playbackRate = playbackRate;
  783.  
  784. // Set preload to auto for better playback
  785. audioElement.preload = 'auto';
  786.  
  787. // Update UI based on current state
  788. updatePlayPauseButton();
  789.  
  790. // Get duration when metadata is loaded and start countdown
  791. if (audioElement.readyState >= 1) {
  792. handleAudioLoaded();
  793. } else {
  794. audioElement.addEventListener('loadedmetadata', handleAudioLoaded);
  795. }
  796.  
  797. // Add event listener to ensure playback rate is maintained
  798. audioElement.addEventListener('ratechange', function() {
  799. // If something else changed the rate, reset it to our value
  800. if (this.playbackRate !== playbackRate) {
  801. console.log("[Audio Controls] Rate changed externally, resetting to", playbackRate);
  802. this.playbackRate = playbackRate;
  803. }
  804. });
  805.  
  806. // Add event listeners
  807. audioElement.addEventListener('play', function() {
  808. updatePlayPauseButton();
  809. // Update bubble icon specifically
  810. updateBubbleIcon();
  811. });
  812.  
  813. audioElement.addEventListener('pause', function() {
  814. updatePlayPauseButton();
  815. // Update bubble icon specifically
  816. updateBubbleIcon();
  817. });
  818.  
  819. audioElement.addEventListener('ended', function() {
  820. updatePlayPauseButton();
  821. // Update bubble icon specifically
  822. updateBubbleIcon();
  823. });
  824.  
  825. // Initial update of bubble icon
  826. updateBubbleIcon();
  827. }
  828.  
  829. // Function to handle audio loaded event
  830. function handleAudioLoaded() {
  831. if (!audioElement) return;
  832.  
  833. // Ensure playback rate is set
  834. audioElement.playbackRate = playbackRate;
  835.  
  836. // Update bubble icon based on current state
  837. updateBubbleIcon();
  838.  
  839. // Start countdown for auto-play (5 seconds)
  840. const countdownSeconds = Math.floor(AUTO_PLAY_DELAY / 1000);
  841. startCountdown(countdownSeconds);
  842. }
  843.  
  844. // Function to update the bubble icon based on audio state
  845. function updateBubbleIcon() {
  846. if (!audioElement) {
  847. bubbleIcon.innerHTML = '🔊'; // Default icon when no audio
  848. return;
  849. }
  850.  
  851. if (audioElement.paused) {
  852. bubbleIcon.innerHTML = '▶️'; // Play icon when paused (showing what will happen on click)
  853. } else {
  854. bubbleIcon.innerHTML = '⏸️'; // Pause icon when playing (showing what will happen on click)
  855. }
  856. }
  857.  
  858. // Function to update play/pause button state
  859. function updatePlayPauseButton() {
  860. if (!audioElement) return;
  861.  
  862. // Ensure playback rate is correct
  863. if (audioElement.playbackRate !== playbackRate) {
  864. audioElement.playbackRate = playbackRate;
  865. }
  866.  
  867. if (audioElement.paused) {
  868. playPauseButton.innerHTML = '▶️';
  869. playPauseButton.style.backgroundColor = '#4CAF50';
  870. } else {
  871. playPauseButton.innerHTML = '⏸️';
  872. playPauseButton.style.backgroundColor = '#F44336';
  873.  
  874. // If playing, clear countdown
  875. if (countdownTimer) {
  876. clearInterval(countdownTimer);
  877. countdownTimer = null;
  878. countdownDisplay.textContent = '';
  879. }
  880. }
  881.  
  882. // Update bubble icon
  883. updateBubbleIcon();
  884. }
  885.  
  886. // Function to create a "resume" toast notification
  887. function showResumeToast(position) {
  888. // Create and style the toast
  889. const toast = document.createElement('div');
  890. toast.style.cssText = `
  891. position: fixed;
  892. bottom: 80px;
  893. left: 50%;
  894. transform: translateX(-50%);
  895. background-color: rgba(0, 0, 0, 0.8);
  896. color: white;
  897. padding: 10px 15px;
  898. border-radius: 5px;
  899. font-size: 14px;
  900. z-index: 10000;
  901. opacity: 0;
  902. transition: opacity 0.3s ease;
  903. `;
  904.  
  905. // Format the time nicely
  906. const formattedTime = formatTime(position);
  907. toast.textContent = `Resuming from ${formattedTime}`;
  908.  
  909. // Add to document
  910. document.body.appendChild(toast);
  911.  
  912. // Fade in
  913. setTimeout(() => {
  914. toast.style.opacity = '1';
  915. }, 10);
  916.  
  917. // Remove after 2 seconds
  918. setTimeout(() => {
  919. toast.style.opacity = '0';
  920. setTimeout(() => {
  921. document.body.removeChild(toast);
  922. }, 300);
  923. }, 2000);
  924. }
  925.  
  926. // Function to toggle play/pause with better resume handling
  927. function togglePlayPause() {
  928. if (!audioElement) return;
  929.  
  930. // Reset inactivity timer on user interaction
  931. resetInactivityTimer();
  932.  
  933. // Set flag for user interaction
  934. hasUserInteracted = true;
  935.  
  936. // Ensure playback rate is set correctly
  937. if (audioElement.playbackRate !== playbackRate) {
  938. audioElement.playbackRate = playbackRate;
  939. }
  940.  
  941. if (audioElement.paused) {
  942. // Check if currentTime is > 0, indicating playback was started before
  943. if (audioElement.currentTime > 0 && !audioElement.ended) {
  944. // Show resume toast if resuming from a significant position (more than 3 seconds in)
  945. if (audioElement.currentTime > 3) {
  946. showResumeToast(audioElement.currentTime);
  947. }
  948.  
  949. // Simply resume playback without using autoplay methods
  950. console.log("[Audio Controls] Resuming playback from position:", audioElement.currentTime);
  951.  
  952. // Update UI immediately to provide feedback
  953. playPauseButton.innerHTML = '⏳';
  954. countdownDisplay.textContent = 'Resuming...';
  955.  
  956. audioElement.play()
  957. .then(() => {
  958. updatePlayPauseButton();
  959. countdownDisplay.textContent = '';
  960. })
  961. .catch(err => {
  962. console.log("[Audio Controls] Resume failed, trying autoplay methods:", err);
  963. attemptAutoPlay();
  964. });
  965. } else {
  966. // If starting from beginning or after ended, try all methods
  967. attemptAutoPlay();
  968. }
  969. } else {
  970. audioElement.pause();
  971. updatePlayPauseButton();
  972. }
  973. }
  974.  
  975. // Function to update playback speed
  976. function updatePlaybackSpeed(newRate) {
  977. // Reset inactivity timer on user interaction
  978. resetInactivityTimer();
  979.  
  980. playbackRate = newRate;
  981.  
  982. // Apply to all audio elements immediately
  983. applyPlaybackRateToAllAudio();
  984.  
  985. // Update display
  986. speedDisplay.textContent = `${playbackRate.toFixed(1)}x`;
  987.  
  988. // Save to localStorage
  989. localStorage.setItem('audio_playback_rate', playbackRate);
  990. }
  991.  
  992. // Function to decrease playback speed
  993. function decreaseSpeed() {
  994. const newRate = Math.max(0.5, playbackRate - 0.1);
  995. updatePlaybackSpeed(newRate);
  996. }
  997.  
  998. // Function to increase playback speed
  999. function increaseSpeed() {
  1000. const newRate = Math.min(2.5, playbackRate + 0.1);
  1001. updatePlaybackSpeed(newRate);
  1002. }
  1003.  
  1004. // Set up event listeners for buttons
  1005. playPauseButton.addEventListener('click', togglePlayPause);
  1006. speedDownButton.addEventListener('click', decreaseSpeed);
  1007. speedUpButton.addEventListener('click', increaseSpeed);
  1008.  
  1009. // Make bubble clickable with smart behavior
  1010. bubbleView.addEventListener('click', function() {
  1011. if (audioElement) {
  1012. if (audioElement.paused) {
  1013. // If audio exists and is paused, try to play it
  1014. togglePlayPause();
  1015.  
  1016. // After attempting to play, check if we're still paused (play failed)
  1017. // and expand the controls in that case for more options
  1018. setTimeout(() => {
  1019. if (audioElement.paused) {
  1020. toggleMinimized();
  1021. }
  1022. }, 300);
  1023. } else {
  1024. // If we're playing, expand the panel
  1025. toggleMinimized();
  1026. }
  1027. } else {
  1028. // If no audio, just expand
  1029. toggleMinimized();
  1030. }
  1031. });
  1032.  
  1033. // Start periodic rate check interval and UI updates
  1034. function startRateCheckInterval() {
  1035. if (rateCheckInterval) {
  1036. clearInterval(rateCheckInterval);
  1037. }
  1038.  
  1039. rateCheckInterval = setInterval(() => {
  1040. applyPlaybackRateToAllAudio();
  1041.  
  1042. // Update bubble icon even when minimized
  1043. if (isMinimized && audioElement) {
  1044. updateBubbleIcon();
  1045. }
  1046. }, RATE_CHECK_INTERVAL);
  1047. }
  1048.  
  1049. // Create an observer to watch for new audio elements
  1050. const audioObserver = new MutationObserver(function(mutations) {
  1051. mutations.forEach(function(mutation) {
  1052. if (mutation.addedNodes.length) {
  1053. let foundNewAudio = false;
  1054.  
  1055. mutation.addedNodes.forEach(function(node) {
  1056. if (node.nodeName === 'AUDIO' ||
  1057. (node.nodeType === 1 && node.querySelector(AUDIO_SELECTOR))) {
  1058. foundNewAudio = true;
  1059. }
  1060. });
  1061.  
  1062. if (foundNewAudio) {
  1063. // Immediately apply rate to any new audio elements
  1064. applyPlaybackRateToAllAudio();
  1065.  
  1066. // Reset audio element and reinitialize if needed
  1067. if (!audioElement) {
  1068. findAudioElement();
  1069. }
  1070. }
  1071. }
  1072. });
  1073. });
  1074.  
  1075. // Function to immediately apply playback rate when the DOM is ready
  1076. function onDOMReady() {
  1077. console.log("[Audio Controls] DOM Content Loaded - initializing audio controls");
  1078.  
  1079. // Try to unlock audio context early for iOS
  1080. try {
  1081. const AudioContext = window.AudioContext || window.webkitAudioContext;
  1082. if (AudioContext) {
  1083. audioContext = new AudioContext();
  1084.  
  1085. // Create and play silent buffer
  1086. const buffer = audioContext.createBuffer(1, 1, 22050);
  1087. const source = audioContext.createBufferSource();
  1088. source.buffer = buffer;
  1089. source.connect(audioContext.destination);
  1090. source.start(0);
  1091.  
  1092. // Resume if needed
  1093. if (audioContext.state === 'suspended') {
  1094. audioContext.resume();
  1095. }
  1096.  
  1097. console.log("[Audio Controls] AudioContext initialized:", audioContext.state);
  1098. }
  1099. } catch (e) {
  1100. console.log("[Audio Controls] Early AudioContext initialization error:", e);
  1101. }
  1102.  
  1103. // Set up global event handlers for iOS audio unlocking
  1104. const unlockEvents = ['touchstart', 'touchend', 'mousedown', 'keydown'];
  1105. const unlockAudio = function() {
  1106. if (audioContext && audioContext.state === 'suspended') {
  1107. audioContext.resume();
  1108. }
  1109.  
  1110. // Remove these event listeners once used
  1111. unlockEvents.forEach(event => {
  1112. document.removeEventListener(event, unlockAudio);
  1113. });
  1114. };
  1115.  
  1116. // Add unlock event listeners
  1117. unlockEvents.forEach(event => {
  1118. document.addEventListener(event, unlockAudio, false);
  1119. });
  1120.  
  1121. // Immediately try to find and configure audio
  1122. applyPlaybackRateToAllAudio();
  1123. findAudioElement();
  1124.  
  1125. // Start the rate check interval
  1126. startRateCheckInterval();
  1127.  
  1128. // Start observing the document
  1129. audioObserver.observe(document.body, { childList: true, subtree: true });
  1130. }
  1131.  
  1132. // Initialize as soon as the DOM is ready
  1133. if (document.readyState === 'loading') {
  1134. document.addEventListener('DOMContentLoaded', onDOMReady);
  1135. } else {
  1136. // DOM already loaded, initialize immediately
  1137. onDOMReady();
  1138. }
  1139.  
  1140. // Double-check when page is fully loaded
  1141. window.addEventListener('load', function() {
  1142. console.log("[Audio Controls] Window loaded - ensuring audio playback rate");
  1143. applyPlaybackRateToAllAudio();
  1144. });
  1145.  
  1146. // Initialize the view state
  1147. updateViewState();
  1148. })();