@@ -20,9 +20,11 @@ export const useSubtitles = ({
2020} : Props ) : ActiveCue | null => {
2121 const [ activeTrack , setActiveTrack ] = useState < TextTrack | null > ( null ) ;
2222 const [ activeCue , setActiveCue ] = useState < ActiveCue | null > ( null ) ;
23- // only show subtitles if the video is actively playing or if its paused
23+
24+ // Only show subtitles if the video is actively playing or if it's paused after having started
2425 const shouldShow = playerState === 'PLAYING' || currentTime > 0 ;
2526
27+ // Select and "wake" the single text track as soon as possible.
2628 useEffect ( ( ) => {
2729 if ( ! video ) return ;
2830
@@ -33,59 +35,107 @@ export const useSubtitles = ({
3335 if ( ! t ) return ;
3436
3537 // Trigger cue fetching immediately (don’t wait for play state)
36- if ( t . mode !== 'hidden' && t . mode !== 'showing' ) {
37- t . mode = 'hidden' ;
38- }
38+ if ( t . mode === 'disabled' ) t . mode = 'hidden' ;
3939
4040 setActiveTrack ( t ) ;
4141 } ;
4242
43- // 1) pick immediately if already present
43+ // 1) If already present
4444 pickTrack ( ) ;
4545
46- // 2) react when HLS adds tracks later (common on mobile)
46+ // 2) Track can appear later on mobile HLS
4747 const onAdd = ( ) => pickTrack ( ) ;
48- tracks . addEventListener ( 'addtrack' , onAdd ) ;
48+ // Some engines support addEventListener on TextTrackList; guard just in case
49+ ( tracks as unknown as EventTarget ) . addEventListener ?.(
50+ 'addtrack' ,
51+ onAdd as EventListener ,
52+ ) ;
4953
50- // 3) also after metadata (some browsers populate then)
54+ // 3) Some browsers populate textTracks on metadata
5155 const onMeta = ( ) => pickTrack ( ) ;
5256 video . addEventListener ( 'loadedmetadata' , onMeta ) ;
5357
5458 return ( ) => {
55- tracks . removeEventListener ( 'addtrack' , onAdd ) ;
59+ ( tracks as unknown as EventTarget ) . removeEventListener ?.(
60+ 'addtrack' ,
61+ onAdd as EventListener ,
62+ ) ;
5663 video . removeEventListener ( 'loadedmetadata' , onMeta ) ;
5764 } ;
5865 } , [ video ] ) ;
5966
67+ // Keep activeCue in sync; use cuechange + timeupdate fallback to avoid stalls on mobile.
6068 useEffect ( ( ) => {
6169 const track = activeTrack ;
6270
63- if ( ! track || ! shouldShow ) {
71+ if ( ! video || ! track ) {
6472 setActiveCue ( null ) ;
6573 return ;
6674 }
6775
68- // if we have a track and can show it, hide the native track
69- track . mode = 'hidden' ;
76+ // Ensure the browser continues fetching cues even if it flips the mode
77+ if ( track . mode === 'disabled' ) track . mode = 'hidden' ;
7078
71- const onCueChange = ( ) => {
79+ const computeActive = ( ) => {
80+ if ( ! shouldShow ) {
81+ // We still keep the track loading, but hide our custom renderer state
82+ setActiveCue ( null ) ;
83+ return ;
84+ }
85+
86+ // Prefer activeCues when available
7287 const list = track . activeCues ;
73- if ( ! list || list . length === 0 ) {
88+ if ( list && list . length > 0 ) {
89+ const cue = list [ 0 ] as VTTCue ;
90+ setActiveCue ( {
91+ startTime : cue . startTime ,
92+ endTime : cue . endTime ,
93+ text : cue . text ,
94+ } ) ;
7495 return ;
7596 }
76- const cue = list [ 0 ] as VTTCue ;
77- setActiveCue ( {
78- startTime : cue . startTime ,
79- endTime : cue . endTime ,
80- text : cue . text ,
81- } ) ;
97+
98+ // Fallback: derive from all cues + currentTime (helps when cuechange stalls on mobile)
99+ const cues = track . cues ;
100+ if ( cues ?. length != null ) {
101+ const t = video . currentTime ;
102+ let found : VTTCue | null = null ;
103+ // Typical subtitle counts are small; linear scan is fine.
104+ for ( let i = 0 ; i < cues . length ; i ++ ) {
105+ const c = cues [ i ] as VTTCue ;
106+ if ( t >= c . startTime && t < c . endTime ) {
107+ found = c ;
108+ break ;
109+ }
110+ }
111+ if ( found ) {
112+ setActiveCue ( {
113+ startTime : found . startTime ,
114+ endTime : found . endTime ,
115+ text : found . text ,
116+ } ) ;
117+ return ;
118+ }
119+ }
120+
121+ // No active cue → clear to avoid “stuck on first cue”
122+ setActiveCue ( null ) ;
82123 } ;
124+
125+ const onCueChange = ( ) => computeActive ( ) ;
126+ const onTimeUpdate = ( ) => computeActive ( ) ;
127+
83128 track . addEventListener ( 'cuechange' , onCueChange ) ;
84- onCueChange ( ) ;
129+ video . addEventListener ( 'timeupdate' , onTimeUpdate ) ;
130+
131+ // Kick once in case we're already mid-cue
132+ computeActive ( ) ;
133+
85134 return ( ) => {
86135 track . removeEventListener ( 'cuechange' , onCueChange ) ;
136+ video . removeEventListener ( 'timeupdate' , onTimeUpdate ) ;
87137 } ;
88- } , [ activeTrack , shouldShow ] ) ;
138+ } , [ activeTrack , video , shouldShow ] ) ;
89139
90140 return activeCue ;
91141} ;
0 commit comments