11import type Player from 'video.js/dist/types/player' ;
22import type { SourceOptions , IKPlayerOptions } from '../../interfaces' ;
33import { ChapterMarker , parseChaptersFromVTT } from './chapterMarkerProgressBar' ;
4- import { prepareChaptersVttSrc } from '../../utils' ;
4+ import { prepareChaptersVttSrc , CleanupRegistry } from '../../utils' ;
55
66interface ChapterTrackMetadata {
77 langCode : string ;
@@ -12,6 +12,12 @@ interface ChapterTrackMetadata {
1212// Map to store chapter tracks by language
1313const chapterTracksCache = new Map < string , ChapterTrackMetadata > ( ) ;
1414
15+ // Cleanup registry for per-chapter resources (tracks, labels, progress bar)
16+ let perChapterCleanup : CleanupRegistry | null = null ;
17+
18+ // Cleanup registry for persistent resources (subtitle sync listener)
19+ let persistentCleanup : CleanupRegistry | null = null ;
20+
1521/**
1622 * Clean up existing chapter text tracks from the player.
1723 */
@@ -48,17 +54,39 @@ function cleanupChapterLabelDisplay(player: Player): void {
4854
4955/**
5056 * Clean up all chapter-related components from the player.
51- * Removes chapter text tracks, label display, and progress bar control.
57+ * Removes chapter text tracks, label display, progress bar control, and per-chapter event listeners.
58+ * Does NOT clean up persistent listeners (like subtitle sync).
5259 */
5360function cleanupChapters ( player : Player ) : void {
5461 cleanupChapterTextTracks ( player ) ;
5562 cleanupChapterLabelDisplay ( player ) ;
63+
64+ // Dispose per-chapter cleanups (cuechange listeners, etc.)
65+ if ( perChapterCleanup ) {
66+ perChapterCleanup . dispose ( ) ;
67+ perChapterCleanup = null ;
68+ }
69+
5670 const existing = player . getChild ( 'ChapterMarkersProgressBarControl' ) ;
5771 if ( existing ) {
5872 existing . dispose ( ) ;
5973 }
6074}
6175
76+ /**
77+ * Clean up ALL chapter resources including persistent listeners.
78+ * Should be called when completely removing chapters (e.g., new source).
79+ */
80+ function cleanupAllChapters ( player : Player ) : void {
81+ cleanupChapters ( player ) ;
82+
83+ // Dispose persistent cleanups (subtitle sync listener)
84+ if ( persistentCleanup ) {
85+ persistentCleanup . dispose ( ) ;
86+ persistentCleanup = null ;
87+ }
88+ }
89+
6290
6391/**
6492 * Load chapters for a specific language
@@ -160,12 +188,19 @@ function applyChaptersToPlayer(player: Player, chapterList: ChapterMarker[]): vo
160188function setupSubtitleChapterSync ( player : Player ) : void {
161189 const textTracks = player . textTracks ( ) ;
162190
163- textTracks . addEventListener ( 'change' , ( ) => {
191+ // Initialize persistent cleanup registry if needed
192+ if ( ! persistentCleanup ) {
193+ persistentCleanup = new CleanupRegistry ( ) ;
194+ }
195+
196+ const handler = ( ) => {
164197 let foundActiveTrack = false ;
165198
166199 // Check for active subtitle track
167- for ( let i = 0 ; i < textTracks . length ; i ++ ) {
168- const track = textTracks [ i ] ;
200+ // TextTrackList is array-like, iterate using index access
201+ const textTracksList = textTracks as unknown as TextTrack [ ] ;
202+ for ( let i = 0 ; i < textTracksList . length ; i ++ ) {
203+ const track = textTracksList [ i ] ;
169204
170205 // Find the active subtitle track
171206 if ( ( track . kind === 'subtitles' || track . kind === 'captions' ) && track . mode === 'showing' ) {
@@ -184,7 +219,10 @@ function setupSubtitleChapterSync(player: Player): void {
184219 if ( ! foundActiveTrack && chapterTracksCache . has ( 'base' ) ) {
185220 switchChaptersLanguage ( player , 'base' ) ;
186221 }
187- } ) ;
222+ } ;
223+
224+ // Register the event listener with persistent cleanup registry
225+ persistentCleanup . registerEventListener ( textTracks as unknown as EventTarget , 'change' , handler ) ;
188226}
189227
190228/**
@@ -211,7 +249,7 @@ function setupChapterLabelDisplay(player: Player, chaptersTrack: TextTrack): voi
211249 spacer . classList . add ( 'vjs-control-bar-chapter-wrapper' ) ;
212250 spacer . appendChild ( controlBarChapterHolder ) ;
213251
214- chaptersTrack . addEventListener ( 'cuechange' , ( ) => {
252+ const cueChangeHandler = ( ) => {
215253 // Safari needs Array.from() for activeCues
216254 const activeCues = Array . from ( chaptersTrack . activeCues ) ;
217255 if ( activeCues . length > 0 ) {
@@ -220,7 +258,13 @@ function setupChapterLabelDisplay(player: Player, chaptersTrack: TextTrack): voi
220258 } else {
221259 controlBarChapterHolder . innerText = '' ;
222260 }
223- } ) ;
261+ } ;
262+
263+ // Register the cuechange listener with per-chapter cleanup registry
264+ if ( ! perChapterCleanup ) {
265+ perChapterCleanup = new CleanupRegistry ( ) ;
266+ }
267+ perChapterCleanup . registerEventListener ( chaptersTrack , 'cuechange' , cueChangeHandler ) ;
224268}
225269
226270/**
@@ -236,7 +280,8 @@ export async function initChapterMarkers(
236280 ikGlobalSettings : IKPlayerOptions
237281) : Promise < void > {
238282
239- cleanupChapters ( player ) ;
283+ // Clean up ALL chapters including persistent listeners for new source
284+ cleanupAllChapters ( player ) ;
240285
241286 // Clear cache for new source
242287 chapterTracksCache . clear ( ) ;
0 commit comments