Fix video/audio desync on Firefox in web video player

Two issues were causing audio to drift out of sync with video on Firefox:

1. fastSeek() seeks video to the nearest keyframe but keeps audio at the
   precise timestamp, causing progressive drift. Disable fastSeek on Firefox
   and use currentTime for precise seeking instead.

2. Low-quality segment flushing only flushed video buffers, leaving stale
   audio data that could desync from newly-loaded video. Flush both audio
   and video buffers together when upgrading quality.

https://claude.ai/code/session_01LGFSY2GgQJ3iw5Cp4XwiuL
This commit is contained in:
Claude
2026-03-12 21:31:31 +00:00
parent 35cb2bcf94
commit 6d5a8b48fc
2 changed files with 17 additions and 2 deletions
@@ -169,11 +169,19 @@ function useHLS({
continue continue
} }
// Flush both video and audio buffers to keep them in sync.
// Flushing only video leaves stale audio data that can desync,
// particularly on Firefox.
hls.trigger(Hls.Events.BUFFER_FLUSHING, { hls.trigger(Hls.Events.BUFFER_FLUSHING, {
startOffset: lowQualFrag.start, startOffset: lowQualFrag.start,
endOffset: lowQualFrag.end, endOffset: lowQualFrag.end,
type: 'video', type: 'video',
}) })
hls.trigger(Hls.Events.BUFFER_FLUSHING, {
startOffset: lowQualFrag.start,
endOffset: lowQualFrag.end,
type: 'audio',
})
flushed.push(lowQualFrag) flushed.push(lowQualFrag)
} }
@@ -269,6 +277,11 @@ function useHLS({
endOffset: lowQualFrag.end, endOffset: lowQualFrag.end,
type: 'video', type: 'video',
}) })
hls.trigger(Hls.Events.BUFFER_FLUSHING, {
startOffset: lowQualFrag.start,
endOffset: lowQualFrag.end,
type: 'audio',
})
setLowQualityFragments([]) setLowQualityFragments([])
} }
}) })
@@ -27,7 +27,7 @@ import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Paus
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_WEB_MOBILE_IOS, IS_WEB_TOUCH_DEVICE} from '#/env' import {IS_WEB_FIREFOX, IS_WEB_MOBILE_IOS, IS_WEB_TOUCH_DEVICE} from '#/env'
import {GifPresentationControls} from '../../GifPresentationControls' import {GifPresentationControls} from '../../GifPresentationControls'
import {TimeIndicator} from '../TimeIndicator' import {TimeIndicator} from '../TimeIndicator'
import {ControlButton} from './ControlButton' import {ControlButton} from './ControlButton'
@@ -196,7 +196,9 @@ export function Controls({
const onSeek = useCallback( const onSeek = useCallback(
(time: number) => { (time: number) => {
if (!videoRef.current) return if (!videoRef.current) return
if (videoRef.current.fastSeek) { // Avoid fastSeek on Firefox - it seeks video to the nearest keyframe
// but keeps audio at the precise time, causing audio/video desync
if (videoRef.current.fastSeek && !IS_WEB_FIREFOX) {
videoRef.current.fastSeek(time) videoRef.current.fastSeek(time)
} else { } else {
videoRef.current.currentTime = time videoRef.current.currentTime = time