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 }); + } } } }