From 4642cae94b72c7e83e7885a6ea326ec337128408 Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Sat, 11 Oct 2025 00:17:02 +0300 Subject: [PATCH] Improve autoplay handling for Firefox compatibility Adds Firefox-specific detection and logic to the AutoplayHandler to better handle autoplay restrictions in Firefox. This includes user gesture detection, delayed playback attempts, custom error handling, and user notifications to prompt interaction for enabling playback and sound. The changes ensure a more reliable autoplay experience across browsers, especially addressing Firefox's stricter autoplay policies. --- .../video-js/src/utils/AutoplayHandler.js | 220 +++++++++++++++--- 1 file changed, 184 insertions(+), 36 deletions(-) diff --git a/frontend-tools/video-js/src/utils/AutoplayHandler.js b/frontend-tools/video-js/src/utils/AutoplayHandler.js index 204bf36e..212946d3 100644 --- a/frontend-tools/video-js/src/utils/AutoplayHandler.js +++ b/frontend-tools/video-js/src/utils/AutoplayHandler.js @@ -3,9 +3,33 @@ export class AutoplayHandler { this.player = player; this.mediaData = mediaData; this.userPreferences = userPreferences; + this.isFirefox = this.detectFirefox(); + } + + detectFirefox() { + return ( + typeof navigator !== 'undefined' && + navigator.userAgent && + navigator.userAgent.toLowerCase().indexOf('firefox') > -1 + ); } hasUserInteracted() { + // Firefox-specific user interaction detection + if (this.isFirefox) { + return ( + // Check if user has explicitly interacted + sessionStorage.getItem('userInteracted') === 'true' || + // Firefox-specific: Check if document has been clicked/touched + sessionStorage.getItem('firefoxUserGesture') === 'true' || + // More reliable focus check for Firefox + (document.hasFocus() && document.visibilityState === 'visible') || + // Check if any user event has been registered + this.checkFirefoxUserGesture() + ); + } + + // Original detection for other browsers return ( document.hasFocus() || document.visibilityState === 'visible' || @@ -13,42 +37,135 @@ export class AutoplayHandler { ); } + checkFirefoxUserGesture() { + // Firefox requires actual user gesture for autoplay + // This checks if we've detected any user interaction events + try { + const hasGesture = document.createElement('video').play(); + return hasGesture && typeof hasGesture.then === 'function'; + } catch { + return false; + } + } + async handleAutoplay() { // Don't attempt autoplay if already playing or loading if (!this.player.paused() || this.player.seeking()) { return; } + // Firefox-specific delay to ensure player is ready + if (this.isFirefox) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + // Define variables outside try block so they're accessible in catch const userInteracted = this.hasUserInteracted(); const savedMuteState = this.userPreferences.getPreference('muted'); try { - // Respect user's saved mute preference, but try unmuted if user interacted and hasn't explicitly muted - if (!this.mediaData.urlMuted && userInteracted && savedMuteState !== true) { + // Firefox-specific: Always start muted if no user interaction + if (this.isFirefox && !userInteracted) { + this.player.muted(true); + } else if (!this.mediaData.urlMuted && userInteracted && savedMuteState !== true) { this.player.muted(false); } // First attempt: try to play with current mute state - await this.player.play(); - } catch (error) { - // Fallback to muted autoplay unless user explicitly wants to stay unmuted - if (!this.player.muted()) { - try { - this.player.muted(true); - await this.player.play(); + const playPromise = this.player.play(); - // Only try to restore sound if user hasn't explicitly saved mute=true - if (savedMuteState !== true) { - this.restoreSound(userInteracted); + // Firefox-specific promise handling + if (this.isFirefox && playPromise && typeof playPromise.then === 'function') { + await playPromise; + } else if (playPromise) { + await playPromise; + } + } catch (error) { + // Firefox-specific error handling + if (this.isFirefox) { + await this.handleFirefoxAutoplayError(error, userInteracted, savedMuteState); + } else { + // Fallback to muted autoplay unless user explicitly wants to stay unmuted + if (!this.player.muted()) { + try { + this.player.muted(true); + await this.player.play(); + + // Only try to restore sound if user hasn't explicitly saved mute=true + if (savedMuteState !== true) { + this.restoreSound(userInteracted); + } + } catch { + // console.error('❌ Even muted autoplay was blocked:', mutedError.message); } - } catch (mutedError) { - // console.error('❌ Even muted autoplay was blocked:', mutedError.message); } } } } + async handleFirefoxAutoplayError(error, userInteracted, savedMuteState) { + // Firefox requires muted autoplay in most cases + if (!this.player.muted()) { + try { + this.player.muted(true); + + // Add a small delay for Firefox + await new Promise((resolve) => setTimeout(resolve, 50)); + + const mutedPlayPromise = this.player.play(); + if (mutedPlayPromise && typeof mutedPlayPromise.then === 'function') { + await mutedPlayPromise; + } + + // Only try to restore sound if user hasn't explicitly saved mute=true + if (savedMuteState !== true) { + this.restoreSound(userInteracted); + } + } catch { + // Even muted autoplay failed - set up user interaction listeners + this.setupFirefoxInteractionListeners(); + } + } else { + // Already muted but still failed - set up interaction listeners + this.setupFirefoxInteractionListeners(); + } + } + + setupFirefoxInteractionListeners() { + if (!this.isFirefox) return; + + const enablePlayback = async () => { + try { + sessionStorage.setItem('firefoxUserGesture', 'true'); + sessionStorage.setItem('userInteracted', 'true'); + + if (this.player && !this.player.isDisposed() && this.player.paused()) { + const playPromise = this.player.play(); + if (playPromise && typeof playPromise.then === 'function') { + await playPromise; + } + } + + // Remove listeners after successful interaction + document.removeEventListener('click', enablePlayback); + document.removeEventListener('keydown', enablePlayback); + document.removeEventListener('touchstart', enablePlayback); + } catch { + // Interaction still didn't work, keep listeners active + } + }; + + // Set up interaction listeners for Firefox + document.addEventListener('click', enablePlayback, { once: true }); + document.addEventListener('keydown', enablePlayback, { once: true }); + document.addEventListener('touchstart', enablePlayback, { once: true }); + + // Show Firefox-specific notification + if (this.player && !this.player.isDisposed()) { + this.player.trigger('notify', '🦊 Firefox: Click to enable playback'); + } + } + restoreSound(userInteracted) { const restoreSound = () => { if (this.player && !this.player.isDisposed()) { @@ -57,31 +174,62 @@ export class AutoplayHandler { } }; - // Try to restore sound immediately if user has interacted - if (userInteracted) { - setTimeout(restoreSound, 100); + // Firefox-specific sound restoration + if (this.isFirefox) { + // Firefox needs more time and user interaction verification + if (userInteracted || sessionStorage.getItem('firefoxUserGesture') === 'true') { + setTimeout(restoreSound, 200); // Longer delay for Firefox + } else { + // Show Firefox-specific notification + setTimeout(() => { + if (this.player && !this.player.isDisposed()) { + this.player.trigger('notify', '🦊 Firefox: Click to enable sound'); + } + }, 1500); // Longer delay for Firefox notification + + // Set up Firefox-specific interaction listeners + const enableSound = () => { + restoreSound(); + // Mark Firefox user interaction + sessionStorage.setItem('userInteracted', 'true'); + sessionStorage.setItem('firefoxUserGesture', 'true'); + // Remove listeners + document.removeEventListener('click', enableSound); + document.removeEventListener('keydown', enableSound); + document.removeEventListener('touchstart', enableSound); + }; + + document.addEventListener('click', enableSound, { once: true }); + document.addEventListener('keydown', enableSound, { once: true }); + document.addEventListener('touchstart', enableSound, { once: true }); + } } else { - // Show notification for manual interaction - setTimeout(() => { - if (this.player && !this.player.isDisposed()) { - this.player.trigger('notify', '🔇 Click anywhere to enable sound'); - } - }, 1000); + // Original behavior for other browsers + if (userInteracted) { + setTimeout(restoreSound, 100); + } else { + // Show notification for manual interaction + setTimeout(() => { + if (this.player && !this.player.isDisposed()) { + this.player.trigger('notify', '🔇 Click anywhere to enable sound'); + } + }, 1000); - // Set up interaction listeners - const enableSound = () => { - restoreSound(); - // Mark user interaction for future videos - sessionStorage.setItem('userInteracted', 'true'); - // Remove listeners - document.removeEventListener('click', enableSound); - document.removeEventListener('keydown', enableSound); - document.removeEventListener('touchstart', enableSound); - }; + // Set up interaction listeners + const enableSound = () => { + restoreSound(); + // Mark user interaction for future videos + sessionStorage.setItem('userInteracted', 'true'); + // Remove listeners + document.removeEventListener('click', enableSound); + document.removeEventListener('keydown', enableSound); + document.removeEventListener('touchstart', enableSound); + }; - document.addEventListener('click', enableSound, { once: true }); - document.addEventListener('keydown', enableSound, { once: true }); - document.addEventListener('touchstart', enableSound, { once: true }); + document.addEventListener('click', enableSound, { once: true }); + document.addEventListener('keydown', enableSound, { once: true }); + document.addEventListener('touchstart', enableSound, { once: true }); + } } } }