@@ -3,6 +3,15 @@ import type { SourceOptions, IKPlayerOptions } from '../../interfaces';
33import { ChapterMarker , parseChaptersFromVTT } from './chapterMarkerProgressBar' ;
44import { prepareChaptersVttSrc } from '../../utils' ;
55
6+ interface ChapterTrackMetadata {
7+ langCode : string ;
8+ chapterList : ChapterMarker [ ] ;
9+ vttUrl : string ;
10+ }
11+
12+ // Map to store chapter tracks by language
13+ const chapterTracksCache = new Map < string , ChapterTrackMetadata > ( ) ;
14+
615/**
716 * Clean up existing chapter text tracks from the player.
817 */
@@ -37,6 +46,147 @@ function cleanupChapterLabelDisplay(player: Player): void {
3746 }
3847}
3948
49+ /**
50+ * Clean up all chapter-related components from the player.
51+ * Removes chapter text tracks, label display, and progress bar control.
52+ */
53+ function cleanupChapters ( player : Player ) : void {
54+ cleanupChapterTextTracks ( player ) ;
55+ cleanupChapterLabelDisplay ( player ) ;
56+ const existing = player . getChild ( 'ChapterMarkersProgressBarControl' ) ;
57+ if ( existing ) {
58+ existing . dispose ( ) ;
59+ }
60+ }
61+
62+
63+ /**
64+ * Load chapters for a specific language
65+ */
66+ async function loadChaptersForLanguage (
67+ player : Player ,
68+ vttUrl : string ,
69+ langCode : string = 'en'
70+ ) : Promise < ChapterMarker [ ] > {
71+ // Check cache first
72+ if ( chapterTracksCache . has ( langCode ) ) {
73+ return chapterTracksCache . get ( langCode ) ! . chapterList ;
74+ }
75+
76+ try {
77+ const res = await fetch ( vttUrl ) ;
78+ if ( ! res . ok ) {
79+ player . log . warn ( `Chapter VTT fetch failed for ${ langCode } with status: (${ res . status } )` ) ;
80+ return [ ] ;
81+ }
82+ const data = await res . text ( ) ;
83+ const chapterList = parseChaptersFromVTT ( data ) ;
84+
85+ // Cache the chapters
86+ chapterTracksCache . set ( langCode , { langCode, chapterList, vttUrl } ) ;
87+
88+ return chapterList ;
89+ } catch ( e ) {
90+ player . log . error ( `Failed to fetch chapters for ${ langCode } : ${ e } ` ) ;
91+ return [ ] ;
92+ }
93+ }
94+
95+ /**
96+ * Switch chapters to match the active subtitle language
97+ */
98+ async function switchChaptersLanguage (
99+ player : Player ,
100+ langCode : string
101+ ) : Promise < void > {
102+ const metadata = chapterTracksCache . get ( langCode ) ;
103+
104+ if ( ! metadata ) {
105+ player . log . warn ( `No chapter track found for language: ${ langCode } ` ) ;
106+ return ;
107+ }
108+
109+ // Clean up existing chapters
110+ cleanupChapters ( player ) ;
111+
112+ // Apply new chapters
113+ applyChaptersToPlayer ( player , metadata . chapterList ) ;
114+ }
115+
116+ /**
117+ * Apply chapter list to player (extracted for reuse)
118+ */
119+ function applyChaptersToPlayer ( player : Player , chapterList : ChapterMarker [ ] ) : void {
120+ if ( chapterList . length === 0 ) return ;
121+
122+ try {
123+ const trackEl = player . addRemoteTextTrack (
124+ {
125+ kind : 'chapters' ,
126+ default : true ,
127+ } ,
128+ false
129+ ) as unknown as HTMLTrackElement ;
130+
131+ chapterList . forEach ( ( chapter ) => {
132+ const cue = new VTTCue ( chapter . startTime , chapter . endTime , chapter . label ) ;
133+ trackEl . track . addCue ( cue ) ;
134+ } ) ;
135+
136+ setupChapterLabelDisplay ( player , trackEl . track ) ;
137+
138+ // Update the chapters button
139+ const controlBar = player . getChild ( 'ControlBar' ) ;
140+ if ( controlBar ) {
141+ const chaptersButton = controlBar . getChild ( 'ChaptersButton' ) ;
142+ if ( chaptersButton && typeof ( chaptersButton as any ) . update === 'function' ) {
143+ ( chaptersButton as any ) . update ( ) ;
144+ }
145+ }
146+ } catch ( e ) {
147+ player . log . warn ( `Failed to create chapter text track: ${ e } ` ) ;
148+ }
149+
150+ const existing = player . getChild ( 'ChapterMarkersProgressBarControl' ) ;
151+ if ( existing ) {
152+ existing . dispose ( ) ;
153+ }
154+ player . addChild ( 'ChapterMarkersProgressBarControl' , { chapters : chapterList } ) ;
155+ }
156+
157+ /**
158+ * Setup listener for subtitle track changes to switch chapters accordingly
159+ */
160+ function setupSubtitleChapterSync ( player : Player ) : void {
161+ const textTracks = player . textTracks ( ) ;
162+
163+ textTracks . addEventListener ( 'change' , ( ) => {
164+ let foundActiveTrack = false ;
165+
166+ // Check for active subtitle track
167+ for ( let i = 0 ; i < textTracks . length ; i ++ ) {
168+ const track = textTracks [ i ] ;
169+
170+ // Find the active subtitle track
171+ if ( ( track . kind === 'subtitles' || track . kind === 'captions' ) && track . mode === 'showing' ) {
172+ const langCode = track . language || 'en' ;
173+
174+ // Switch chapters to match this language
175+ if ( chapterTracksCache . has ( langCode ) ) {
176+ switchChaptersLanguage ( player , langCode ) ;
177+ foundActiveTrack = true ;
178+ }
179+ break ;
180+ }
181+ }
182+
183+ // If no subtitle track is active (user turned off captions), revert to base chapters
184+ if ( ! foundActiveTrack && chapterTracksCache . has ( 'base' ) ) {
185+ switchChaptersLanguage ( player , 'base' ) ;
186+ }
187+ } ) ;
188+ }
189+
40190/**
41191 * Setup chapter label display in the control bar.
42192 * Displays the current chapter name and updates on cuechange events.
@@ -76,23 +226,20 @@ function setupChapterLabelDisplay(player: Player, chaptersTrack: TextTrack): voi
76226/**
77227 * Initialize chapter markers for the video player.
78228 * Supports three methods:
79- * 1. Auto-generate: chapters: true
229+ * 1. Auto-generate: chapters: true (with optional translations)
80230 * 2. VTT URL: chapters: { url: string }
81231 * 3. Manual object: chapters: { [timeInSec]: title }
82232 */
83233export async function initChapterMarkers (
84234 player : Player ,
85235 source : SourceOptions | SourceOptions [ ] | null ,
86- ikGlobalSettings : IKPlayerOptions ,
87- signerFn ?: ( src : string ) => Promise < string >
236+ ikGlobalSettings : IKPlayerOptions
88237) : Promise < void > {
89238
90- cleanupChapterTextTracks ( player ) ;
91- cleanupChapterLabelDisplay ( player ) ;
92- const existing = player . getChild ( 'ChapterMarkersProgressBarControl' ) ;
93- if ( existing ) {
94- existing . dispose ( ) ;
95- }
239+ cleanupChapters ( player ) ;
240+
241+ // Clear cache for new source
242+ chapterTracksCache . clear ( ) ;
96243
97244 if ( ! source ) return ;
98245
@@ -102,12 +249,13 @@ export async function initChapterMarkers(
102249 let chapterList : ChapterMarker [ ] = [ ] ;
103250
104251 if ( typeof src . chapters === 'object' && 'url' in src . chapters ) {
252+ // Manual VTT URL - load directly
105253 try {
106254 let vttUrl = src . chapters . url ;
107255
108- if ( signerFn ) {
256+ if ( ikGlobalSettings . signerFn ) {
109257 try {
110- vttUrl = await signerFn ( vttUrl ) ;
258+ vttUrl = await ikGlobalSettings . signerFn ( vttUrl ) ;
111259 } catch ( err ) {
112260 player . log . error ( `Failed to sign chapter VTT URL: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
113261 return ;
@@ -122,18 +270,19 @@ export async function initChapterMarkers(
122270 const data = await res . text ( ) ;
123271 chapterList = parseChaptersFromVTT ( data ) ;
124272 } catch ( e ) {
125- player . log . error ( `Failed to fetch chapters VTT: ${ e } ` ) ;
273+ player . log . error ( `Failed to fetch chapters VTT: ${ e instanceof Error ? e . message : String ( e ) } ` ) ;
126274 return ;
127275 }
128276 } else if ( typeof src . chapters === 'object' ) {
277+ // Manual chapter object - convert to ChapterMarker[]
129278 const entries = Object . entries ( src . chapters )
130279 . map ( ( [ time , label ] ) => ( { startTime : Number ( time ) , label : String ( label ) } ) )
131280 . sort ( ( a , b ) => a . startTime - b . startTime ) ;
132281
133282 const duration = player . duration ( ) || 0 ;
134283
135284 chapterList = entries . map ( ( entry , index ) => {
136- const endTime =
285+ const endTime =
137286 index < entries . length - 1
138287 ? entries [ index + 1 ] . startTime
139288 : duration || entry . startTime + 10 ;
@@ -144,55 +293,33 @@ export async function initChapterMarkers(
144293 } ;
145294 } ) ;
146295 } else if ( src . chapters === true ) {
296+ // Auto-generate chapters with translations support
147297 try {
148- const chaptersVttSrc = await prepareChaptersVttSrc ( src , ikGlobalSettings ) ;
149- const res = await fetch ( chaptersVttSrc ) ;
150- if ( ! res . ok ) {
151- player . log . warn ( `Default VTT fetch failed with status: (${ res . status } ); skipping chapters.` ) ;
152- return ;
298+ const { baseUrl, translatedUrls } = await prepareChaptersVttSrc ( src , ikGlobalSettings ) ;
299+
300+ // Load base language chapters
301+ chapterList = await loadChaptersForLanguage ( player , baseUrl , 'base' ) ;
302+
303+ // Pre-load translated chapters in background
304+ if ( translatedUrls . size > 0 ) {
305+ for ( const [ langCode , url ] of translatedUrls . entries ( ) ) {
306+ // Load in background without blocking
307+ loadChaptersForLanguage ( player , url , langCode ) . catch ( err => {
308+ player . log . warn ( `Failed to pre-load chapters for ${ langCode } :` , err ) ;
309+ } ) ;
310+ }
311+
312+ // Setup subtitle-chapter synchronization
313+ setupSubtitleChapterSync ( player ) ;
153314 }
154- const data = await res . text ( ) ;
155- chapterList = parseChaptersFromVTT ( data ) ;
156315 } catch ( e ) {
157316 player . log . error ( `Failed to fetch default chapters VTT: ${ e } ` ) ;
158317 return ;
159318 }
160319 }
161320
162- if ( chapterList . length ) {
163-
164- try {
165- const trackEl = player . addRemoteTextTrack (
166- {
167- kind : 'chapters' ,
168- default : true ,
169- } ,
170- false
171- ) as unknown as HTMLTrackElement ;
172-
173- chapterList . forEach ( ( chapter ) => {
174- const cue = new VTTCue ( chapter . startTime , chapter . endTime , chapter . label ) ;
175- trackEl . track . addCue ( cue ) ;
176- } ) ;
177-
178- setupChapterLabelDisplay ( player , trackEl . track ) ;
179-
180- const controlBar = player . getChild ( 'ControlBar' ) ;
181- if ( controlBar ) {
182- const chaptersButton = controlBar . getChild ( 'ChaptersButton' ) ;
183- if ( chaptersButton && typeof ( chaptersButton as any ) . update === 'function' ) {
184- ( chaptersButton as any ) . update ( ) ;
185- }
186- }
187- } catch ( e ) {
188- player . log . warn ( `Failed to create chapter text track: ${ e } ` ) ;
189- }
190-
191- const existing = player . getChild ( 'ChapterMarkersProgressBarControl' ) ;
192- if ( existing ) {
193- console . log ( 'Disposing existing chapter markers progress bar control' ) ;
194- existing . dispose ( ) ;
195- }
196- player . addChild ( 'ChapterMarkersProgressBarControl' , { chapters : chapterList } ) ;
321+ // Apply the base/default chapters
322+ if ( chapterList . length > 0 ) {
323+ applyChaptersToPlayer ( player , chapterList ) ;
197324 }
198325}
0 commit comments