unified-video-framework 1.4.403 → 1.4.405

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,82 +1,43 @@
1
1
  /**
2
2
  * Live Stream Ads Manager
3
- *
4
- * Extends GoogleAdsManager to support live streaming ad insertion
5
- *
6
- * Supported Modes:
7
- * 1. Metadata-based: Detects #EXT-X-DATERANGE or ID3 tags in HLS stream
8
- * 2. Periodic: Shows ads every X seconds of playback
9
- * 3. Manual: Programmatic ad break triggering
3
+ * Extends GoogleAdsManager with periodic ad break support for live streams
10
4
  */
11
5
 
12
6
  import { GoogleAdsManager, GoogleAdsConfig } from './GoogleAdsManager';
13
7
 
14
- // HLS.js event types (if using HLS.js)
15
- declare const Hls: any;
16
-
17
- export type LiveAdBreakMode = 'metadata' | 'periodic' | 'manual' | 'hybrid';
18
-
19
8
  export interface LiveStreamAdsConfig extends GoogleAdsConfig {
20
- // Live stream specific options
21
- liveAdBreakMode?: LiveAdBreakMode;
22
-
23
- // For 'periodic' mode: Insert ads every X seconds of playback
24
- periodicAdInterval?: number; // Default: 300 (5 minutes)
25
-
26
- // For 'metadata' mode: Configure metadata detection
27
- metadataConfig?: {
28
- detectDateRange?: boolean; // Detect #EXT-X-DATERANGE tags (default: true)
29
- detectID3?: boolean; // Detect ID3 timed metadata (default: true)
30
- adClassNames?: string[]; // CLASS values to detect (default: ['com.google.ads', 'ads'])
31
- };
32
-
33
- // Advanced options
34
- syncToLiveEdge?: boolean; // Jump to live edge after ad (default: false for DVR-style)
35
- liveEdgeOffset?: number; // Seconds behind live edge (default: 3)
36
- pauseStreamDuringAd?: boolean; // Pause stream during ad (default: true - DVR-style)
9
+ liveAdBreakMode?: 'periodic' | 'manual';
10
+ periodicAdInterval?: number; // Seconds between ads (default: 300)
11
+ syncToLiveEdge?: boolean; // Jump to live edge after ad (default: false)
12
+ liveEdgeOffset?: number; // Seconds behind live edge (default: 3)
13
+ pauseStreamDuringAd?: boolean; // Pause stream download during ads (default: true)
37
14
 
38
15
  // Callbacks
39
- onLiveAdBreakDetected?: (metadata: any) => void;
40
16
  onAdBreakScheduled?: (scheduledTime: number) => void;
41
- onLiveEdgeSync?: (newPosition: number) => void;
42
17
  }
43
18
 
44
19
  export class LiveStreamAdsManager extends GoogleAdsManager {
45
20
  private liveConfig: LiveStreamAdsConfig;
46
-
47
- // Periodic mode state
48
- private periodicAdTimer: NodeJS.Timeout | null = null;
49
- private lastAdBreakTime: number = 0;
50
- private playbackStartTime: number = 0;
51
-
52
- // Metadata mode state
21
+ private isLiveStream = false;
22
+ private periodicAdTimer: any = null;
23
+ private lastAdBreakTime = 0;
24
+ private playbackStartTime = 0;
25
+ private streamStartTime = 0;
53
26
  private hlsInstance: any = null;
54
- private processedCuePoints: Set<string> = new Set();
55
- private metadataTrack: TextTrack | null = null;
56
-
57
- // Live stream state
58
- private isLiveStream: boolean = false;
59
- private streamStartTime: number = 0;
60
27
 
61
28
  constructor(video: HTMLVideoElement, adContainer: HTMLElement, config: LiveStreamAdsConfig) {
62
29
  super(video, adContainer, config);
63
30
  this.liveConfig = {
64
- liveAdBreakMode: 'metadata',
65
- periodicAdInterval: 300, // Default: 5 minutes
66
- syncToLiveEdge: false, // Default: DVR-style (user sees all content)
31
+ periodicAdInterval: 300,
32
+ syncToLiveEdge: false,
67
33
  liveEdgeOffset: 3,
68
- pauseStreamDuringAd: true, // Default: Pause stream during ad (DVR-style)
69
- metadataConfig: {
70
- detectDateRange: true,
71
- detectID3: true,
72
- adClassNames: ['com.google.ads', 'ads', 'ad-break']
73
- },
34
+ pauseStreamDuringAd: true,
74
35
  ...config
75
36
  };
76
37
  }
77
38
 
78
39
  /**
79
- * Initialize live stream ads with specified mode
40
+ * Initialize live stream ads with periodic mode
80
41
  */
81
42
  async initializeLiveAds(hlsInstance?: any): Promise<void> {
82
43
  // Initialize parent (Google IMA SDK)
@@ -87,344 +48,95 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
87
48
  this.playbackStartTime = this.video.currentTime || 0;
88
49
  this.streamStartTime = Date.now();
89
50
 
90
- const mode = this.liveConfig.liveAdBreakMode || 'metadata';
51
+ const mode = this.liveConfig.liveAdBreakMode || 'periodic';
91
52
 
92
53
  console.log(`🔴 Live Stream Ads initialized`);
93
54
  console.log(` Mode: ${mode}`);
94
- console.log(` Stream start time: ${this.playbackStartTime}s`);
95
-
96
- // Setup based on mode
97
- switch (mode) {
98
- case 'metadata':
99
- this.setupMetadataListener();
100
- break;
101
-
102
- case 'periodic':
103
- this.setupPeriodicAdBreaks();
104
- break;
105
-
106
- case 'hybrid':
107
- // Both metadata and periodic
108
- this.setupMetadataListener();
109
- this.setupPeriodicAdBreaks();
110
- break;
111
-
112
- case 'manual':
113
- console.log('Manual mode - use triggerAdBreak() to show ads');
114
- break;
115
- }
116
- }
117
-
118
- /**
119
- * ========================================================================
120
- * OPTION 2: METADATA-BASED AD INSERTION
121
- * ========================================================================
122
- * Detects #EXT-X-DATERANGE tags or ID3 metadata in HLS stream
123
- */
124
- private setupMetadataListener(): void {
125
- console.log('📡 Setting up metadata listener for live ad cues');
126
-
127
- const config = this.liveConfig.metadataConfig!;
55
+ console.log(` Periodic interval: ${this.liveConfig.periodicAdInterval}s`);
128
56
 
129
- // Method 1: HLS.js metadata events
130
- if (this.hlsInstance && typeof Hls !== 'undefined') {
131
- this.setupHlsJsMetadata();
57
+ if (mode === 'periodic') {
58
+ this.setupPeriodicAdBreaks();
59
+ } else {
60
+ console.log('Manual mode - call triggerAdBreak() to show ads');
132
61
  }
133
-
134
- // Method 2: Native HLS (Safari, iOS) - TextTrack metadata
135
- if (config.detectID3) {
136
- this.setupNativeMetadata();
137
- }
138
- }
139
-
140
- /**
141
- * Setup HLS.js metadata detection
142
- */
143
- private setupHlsJsMetadata(): void {
144
- if (!this.hlsInstance || typeof Hls === 'undefined') {
145
- console.warn('HLS.js not available for metadata detection');
146
- return;
147
- }
148
-
149
- console.log('✅ Setting up HLS.js metadata listeners');
150
-
151
- // Listen for fragment parsing metadata (ID3 tags)
152
- this.hlsInstance.on(Hls.Events.FRAG_PARSING_METADATA, (event: string, data: any) => {
153
- console.log('📥 HLS metadata event:', data);
154
- this.handleHlsMetadata(data);
155
- });
156
-
157
- // Listen for level updated (manifest changes with DATERANGE)
158
- this.hlsInstance.on(Hls.Events.LEVEL_UPDATED, (event: string, data: any) => {
159
- if (data.details && data.details.dateRanges) {
160
- console.log('📅 DATERANGE tags detected:', data.details.dateRanges);
161
- this.handleDateRanges(data.details.dateRanges);
162
- }
163
- });
164
- }
165
-
166
- /**
167
- * Setup native HLS metadata detection (Safari, iOS)
168
- */
169
- private setupNativeMetadata(): void {
170
- // Wait for text tracks to be available
171
- const checkForMetadataTrack = () => {
172
- if (!this.video.textTracks) return;
173
-
174
- // Find metadata track
175
- for (let i = 0; i < this.video.textTracks.length; i++) {
176
- const track = this.video.textTracks[i];
177
- if (track.kind === 'metadata') {
178
- this.metadataTrack = track;
179
- console.log('✅ Found native metadata track');
180
-
181
- // Enable the track
182
- track.mode = 'hidden';
183
-
184
- // Listen for cue changes
185
- track.addEventListener('cuechange', () => {
186
- this.handleNativeMetadataCues(track);
187
- });
188
-
189
- break;
190
- }
191
- }
192
- };
193
-
194
- // Check now and on loadedmetadata
195
- checkForMetadataTrack();
196
- this.video.addEventListener('loadedmetadata', checkForMetadataTrack);
197
- }
198
-
199
- /**
200
- * Handle HLS.js metadata (ID3 tags)
201
- */
202
- private handleHlsMetadata(data: any): void {
203
- if (!data.samples || data.samples.length === 0) return;
204
-
205
- data.samples.forEach((sample: any) => {
206
- // Check if this is an ad-related metadata
207
- const isAdCue = this.isAdMetadata(sample);
208
-
209
- if (isAdCue) {
210
- const cueId = this.generateCueId(sample);
211
-
212
- if (!this.processedCuePoints.has(cueId)) {
213
- console.log('✅ Ad cue detected from HLS metadata:', sample);
214
- this.processedCuePoints.add(cueId);
215
-
216
- // Notify callback
217
- this.liveConfig.onLiveAdBreakDetected?.(sample);
218
-
219
- // Trigger ad break
220
- this.triggerAdBreak();
221
- }
222
- }
223
- });
224
62
  }
225
63
 
226
64
  /**
227
- * Handle #EXT-X-DATERANGE tags
228
- */
229
- private handleDateRanges(dateRanges: any): void {
230
- Object.values(dateRanges).forEach((dateRange: any) => {
231
- const className = dateRange.class || dateRange.CLASS;
232
- const adClassNames = this.liveConfig.metadataConfig?.adClassNames || [];
233
-
234
- // Check if this DATERANGE is for ads
235
- const isAdCue = adClassNames.some(name => className?.includes(name));
236
-
237
- if (isAdCue) {
238
- const cueId = dateRange.id || dateRange.ID;
239
-
240
- if (cueId && !this.processedCuePoints.has(cueId)) {
241
- console.log('✅ Ad cue detected from DATERANGE:', dateRange);
242
- this.processedCuePoints.add(cueId);
243
-
244
- // Notify callback
245
- this.liveConfig.onLiveAdBreakDetected?.(dateRange);
246
-
247
- // Trigger ad break
248
- this.triggerAdBreak();
249
- }
250
- }
251
- });
252
- }
253
-
254
- /**
255
- * Handle native metadata cues (Safari, iOS)
256
- */
257
- private handleNativeMetadataCues(track: TextTrack): void {
258
- if (!track.activeCues || track.activeCues.length === 0) return;
259
-
260
- for (let i = 0; i < track.activeCues.length; i++) {
261
- const cue: any = track.activeCues[i];
262
-
263
- // Check if this cue is ad-related
264
- const isAdCue = this.isAdMetadata(cue);
265
-
266
- if (isAdCue) {
267
- const cueId = this.generateCueId(cue);
268
-
269
- if (!this.processedCuePoints.has(cueId)) {
270
- console.log('✅ Ad cue detected from native metadata:', cue);
271
- this.processedCuePoints.add(cueId);
272
-
273
- // Notify callback
274
- this.liveConfig.onLiveAdBreakDetected?.(cue);
275
-
276
- // Trigger ad break
277
- this.triggerAdBreak();
278
- }
279
- }
280
- }
281
- }
282
-
283
- /**
284
- * Check if metadata indicates an ad break
285
- */
286
- private isAdMetadata(metadata: any): boolean {
287
- const adClassNames = this.liveConfig.metadataConfig?.adClassNames || [];
288
-
289
- // Check various metadata formats
290
- const className = metadata.class || metadata.CLASS || metadata.value?.class;
291
- const key = metadata.key;
292
- const data = metadata.data || metadata.value?.data;
293
-
294
- // Check if class name matches ad indicators
295
- if (className) {
296
- return adClassNames.some(name => className.includes(name));
297
- }
298
-
299
- // Check ID3 TXXX frame for ad indicators
300
- if (key === 'TXXX' && data) {
301
- return adClassNames.some(name => data.toLowerCase().includes(name.toLowerCase()));
302
- }
303
-
304
- return false;
305
- }
306
-
307
- /**
308
- * Generate unique ID for cue point
309
- */
310
- private generateCueId(metadata: any): string {
311
- return metadata.id ||
312
- metadata.ID ||
313
- metadata.startTime?.toString() ||
314
- JSON.stringify(metadata);
315
- }
316
-
317
- /**
318
- * ========================================================================
319
- * OPTION 3: PERIODIC AD INSERTION
320
- * ========================================================================
321
- * Shows ads every X seconds of actual playback time
65
+ * Setup periodic timer-based ad breaks
322
66
  */
323
67
  private setupPeriodicAdBreaks(): void {
324
68
  const interval = this.liveConfig.periodicAdInterval || 300;
69
+ console.log(`⏰ Setting up periodic ad breaks every ${interval}s`);
325
70
 
326
- console.log(`⏰ Setting up periodic ad breaks`);
327
- console.log(` Interval: ${interval} seconds (${interval / 60} minutes)`);
328
-
329
- // Clear existing timer
330
- if (this.periodicAdTimer) {
331
- clearInterval(this.periodicAdTimer);
332
- }
333
-
334
- // Track actual playback time (not wall-clock time)
335
- let lastCheckTime = this.video.currentTime || 0;
336
71
  let accumulatedPlaybackTime = 0;
72
+ let lastCheckTime = this.video.currentTime;
337
73
 
338
- // Check every second
339
74
  this.periodicAdTimer = setInterval(() => {
340
- // Only count time when video is actually playing
341
75
  if (!this.video.paused && !this.isPlayingAd()) {
342
76
  const currentTime = this.video.currentTime || 0;
343
77
  const timeDelta = currentTime - lastCheckTime;
344
78
 
345
- // Accumulate playback time (handle seeks)
346
- if (timeDelta > 0 && timeDelta < 5) { // Reasonable time delta
79
+ // Only accumulate if delta is reasonable (< 5 seconds)
80
+ if (timeDelta > 0 && timeDelta < 5) {
347
81
  accumulatedPlaybackTime += timeDelta;
348
82
  }
349
83
 
350
84
  lastCheckTime = currentTime;
351
85
 
352
- // Check if it's time for an ad break
353
86
  const timeSinceLastAd = accumulatedPlaybackTime - this.lastAdBreakTime;
354
87
 
355
88
  if (timeSinceLastAd >= interval) {
356
- console.log(`⏰ Periodic ad break triggered`);
357
- console.log(` Playback time: ${accumulatedPlaybackTime.toFixed(0)}s`);
358
- console.log(` Time since last ad: ${timeSinceLastAd.toFixed(0)}s`);
89
+ console.log('⏰ Periodic ad break triggered');
90
+ console.log(` Playback time: ${Math.floor(accumulatedPlaybackTime)}s`);
91
+ console.log(` Time since last ad: ${Math.floor(timeSinceLastAd)}s`);
359
92
 
360
- // Notify callback
361
93
  this.liveConfig.onAdBreakScheduled?.(accumulatedPlaybackTime);
362
-
363
- // Trigger ad break
364
94
  this.triggerAdBreak();
365
-
366
- // Update last ad break time
367
95
  this.lastAdBreakTime = accumulatedPlaybackTime;
368
96
  }
369
97
  }
370
- }, 1000);
98
+ }, 1000); // Check every second
371
99
  }
372
100
 
373
101
  /**
374
- * ========================================================================
375
- * AD BREAK TRIGGERING
376
- * ========================================================================
102
+ * Manually trigger an ad break
377
103
  */
104
+ triggerAdBreak(): void {
105
+ if (!this.adsManager) {
106
+ console.error('❌ Ads manager not initialized');
107
+ return;
108
+ }
378
109
 
379
- /**
380
- * Trigger ad break for live stream
381
- */
382
- public triggerAdBreak(): void {
383
- console.log('🎬 Triggering ad break for live stream');
384
-
385
- // Don't interrupt if ad is already playing
386
110
  if (this.isPlayingAd()) {
387
111
  console.warn('⚠️ Ad already playing, skipping trigger');
388
112
  return;
389
113
  }
390
114
 
391
- // Store current time for DVR-style resume
392
- const timeBeforeAd = this.video.currentTime;
115
+ console.log('🎬 Triggering ad break for live stream');
393
116
 
394
- console.log(`📍 Stream position before ad: ${timeBeforeAd.toFixed(2)}s`);
117
+ const currentTime = this.video.currentTime;
118
+ console.log(`📍 Stream position before ad: ${currentTime.toFixed(2)}s`);
395
119
 
396
- // Pause stream downloading during ad (saves bandwidth)
120
+ // Pause stream download if configured
397
121
  if (this.liveConfig.pauseStreamDuringAd) {
398
122
  this.pauseStreamDownload();
399
123
  }
400
124
 
401
- // Initialize ad display container
402
- try {
403
- this.initAdDisplayContainer();
404
- } catch (e) {
405
- // Already initialized
406
- }
407
-
408
- // Request ads from Google IMA
409
- this.requestAds();
125
+ // Store position for DVR-style resume
126
+ (this.video as any).__positionBeforeAd = currentTime;
410
127
 
411
- // Setup post-ad resume handler
412
- this.setupLiveEdgeResume(timeBeforeAd);
128
+ // Request ads
129
+ this.adsManager.start();
413
130
  }
414
131
 
415
132
  /**
416
- * Pause stream downloading during ad (saves bandwidth)
133
+ * Pause stream download during ad
417
134
  */
418
135
  private pauseStreamDownload(): void {
419
136
  try {
420
137
  if (this.hlsInstance && this.hlsInstance.stopLoad) {
421
138
  console.log('⏸️ Pausing stream download during ad');
422
139
  this.hlsInstance.stopLoad();
423
- } else if ((this.video as any).dashPlayer) {
424
- const dashPlayer = (this.video as any).dashPlayer;
425
- if (dashPlayer.pause) {
426
- dashPlayer.pause();
427
- }
428
140
  }
429
141
  } catch (error) {
430
142
  console.warn('Could not pause stream download:', error);
@@ -432,18 +144,13 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
432
144
  }
433
145
 
434
146
  /**
435
- * Resume stream downloading after ad
147
+ * Resume stream download after ad
436
148
  */
437
149
  private resumeStreamDownload(): void {
438
150
  try {
439
151
  if (this.hlsInstance && this.hlsInstance.startLoad) {
440
152
  console.log('▶️ Resuming stream download after ad');
441
153
  this.hlsInstance.startLoad();
442
- } else if ((this.video as any).dashPlayer) {
443
- const dashPlayer = (this.video as any).dashPlayer;
444
- if (dashPlayer.play) {
445
- dashPlayer.play();
446
- }
447
154
  }
448
155
  } catch (error) {
449
156
  console.warn('Could not resume stream download:', error);
@@ -451,38 +158,37 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
451
158
  }
452
159
 
453
160
  /**
454
- * Setup live edge resume after ad
161
+ * Override parent onAdEnd to add live stream logic
455
162
  */
456
- private setupLiveEdgeResume(timeBeforeAd: number): void {
457
- // Listen for ad end
458
- const handleAdEnd = () => {
163
+ protected setupLiveEdgeResume(): void {
164
+ const originalOnAdEnd = this.liveConfig.onAdEnd;
165
+
166
+ this.liveConfig.onAdEnd = () => {
167
+ const positionBeforeAd = (this.video as any).__positionBeforeAd;
168
+
459
169
  if (this.liveConfig.syncToLiveEdge) {
460
- // Mode 1: Jump to live edge (user skips content during ad)
170
+ // Live edge sync mode - jump to live
461
171
  console.log('📺 Ad ended, syncing to live edge (skipping content)');
172
+ this.resumeStreamDownload();
462
173
  this.syncToLiveEdge();
463
174
  } else {
464
- // Mode 2: DVR-style resume from pause point (user sees all content)
175
+ // DVR mode - resume from pause point
465
176
  console.log('📺 Ad ended, resuming from pause point (DVR-style)');
466
- this.resumeFromPausePoint(timeBeforeAd);
177
+ this.resumeFromPausePoint(positionBeforeAd);
467
178
  }
468
- };
469
179
 
470
- // Use one-time listener
471
- const originalOnAdEnd = this.config.onAdEnd;
472
- this.config.onAdEnd = () => {
473
- handleAdEnd();
180
+ // Call original callback
474
181
  originalOnAdEnd?.();
475
182
  };
476
183
  }
477
184
 
478
185
  /**
479
- * Resume from where stream was paused (DVR-style)
186
+ * Resume from paused position (DVR-style)
480
187
  */
481
188
  private resumeFromPausePoint(pausedTime: number): void {
482
189
  setTimeout(() => {
483
190
  try {
484
191
  console.log(`⏯️ Resuming from paused position: ${pausedTime.toFixed(2)}s`);
485
- console.log(` User is now behind live, but will see all content`);
486
192
 
487
193
  // Resume stream downloading first
488
194
  if (this.liveConfig.pauseStreamDuringAd) {
@@ -492,10 +198,7 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
492
198
  // Resume from exact pause point
493
199
  this.video.currentTime = pausedTime;
494
200
 
495
- // Notify callback
496
- this.liveConfig.onLiveEdgeSync?.(pausedTime);
497
-
498
- // Calculate how far behind live user is
201
+ // Calculate how far behind live
499
202
  if (this.hlsInstance && this.hlsInstance.liveSyncPosition !== undefined) {
500
203
  const liveEdge = this.hlsInstance.liveSyncPosition;
501
204
  const behindLive = liveEdge - pausedTime;
@@ -504,122 +207,49 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
504
207
  } catch (error) {
505
208
  console.error('❌ Error resuming from pause point:', error);
506
209
  }
507
- }, 100); // Small delay to ensure ad container is hidden
210
+ }, 100);
508
211
  }
509
212
 
510
213
  /**
511
- * Sync video to live edge after ad
214
+ * Sync to live edge
512
215
  */
513
216
  private syncToLiveEdge(): void {
514
217
  setTimeout(() => {
515
218
  try {
516
- // Resume stream downloading first
517
- if (this.liveConfig.pauseStreamDuringAd) {
518
- this.resumeStreamDownload();
519
- }
219
+ // Resume stream download
220
+ this.resumeStreamDownload();
520
221
 
521
- let liveEdgePosition: number | null = null;
222
+ // Get live edge position
223
+ let liveEdgePosition: number | undefined;
522
224
 
523
- // Get live edge from HLS.js
524
225
  if (this.hlsInstance && this.hlsInstance.liveSyncPosition !== undefined) {
226
+ console.log(`📍 HLS.js live edge: ${this.hlsInstance.liveSyncPosition.toFixed(2)}s`);
525
227
  liveEdgePosition = this.hlsInstance.liveSyncPosition;
526
- console.log('📍 HLS.js live edge:', liveEdgePosition);
527
- }
528
- // Get live edge from DASH.js
529
- else if ((this.video as any).dashPlayer) {
530
- const dashPlayer = (this.video as any).dashPlayer;
531
- if (dashPlayer.getDVRSeekOffset) {
532
- liveEdgePosition = dashPlayer.getDVRSeekOffset(0);
533
- }
534
- }
535
- // Fallback: estimate from duration and seekable
536
- else if (this.video.seekable && this.video.seekable.length > 0) {
537
- liveEdgePosition = this.video.seekable.end(this.video.seekable.length - 1);
538
- console.log('📍 Native seekable live edge:', liveEdgePosition);
539
228
  }
540
229
 
541
- // Apply live edge offset
542
- if (liveEdgePosition !== null) {
230
+ if (liveEdgePosition !== undefined) {
543
231
  const offset = this.liveConfig.liveEdgeOffset || 3;
544
- const targetPosition = Math.max(0, liveEdgePosition - offset);
545
-
232
+ const targetPosition = liveEdgePosition - offset;
546
233
  console.log(`⏩ Syncing to live edge: ${targetPosition.toFixed(2)}s (offset: ${offset}s)`);
547
-
548
234
  this.video.currentTime = targetPosition;
549
-
550
- // Notify callback
551
- this.liveConfig.onLiveEdgeSync?.(targetPosition);
552
235
  } else {
553
236
  console.warn('⚠️ Could not determine live edge position');
554
237
  }
555
238
  } catch (error) {
556
239
  console.error('❌ Error syncing to live edge:', error);
557
240
  }
558
- }, 100); // Small delay to ensure ad container is hidden
559
- }
560
-
561
- /**
562
- * ========================================================================
563
- * UTILITY METHODS
564
- * ========================================================================
565
- */
566
-
567
- /**
568
- * Reset periodic ad timer
569
- */
570
- public resetPeriodicTimer(): void {
571
- this.lastAdBreakTime = 0;
572
- console.log('🔄 Periodic ad timer reset');
241
+ }, 100);
573
242
  }
574
243
 
575
244
  /**
576
- * Clear processed cue points (for testing)
577
- */
578
- public clearProcessedCues(): void {
579
- this.processedCuePoints.clear();
580
- console.log('🔄 Processed cue points cleared');
581
- }
582
-
583
- /**
584
- * Update live ad configuration at runtime
585
- */
586
- public updateLiveConfig(config: Partial<LiveStreamAdsConfig>): void {
587
- Object.assign(this.liveConfig, config);
588
- console.log('🔄 Live ad config updated:', config);
589
-
590
- // Re-initialize if mode changed
591
- if (config.liveAdBreakMode && config.liveAdBreakMode !== this.liveConfig.liveAdBreakMode) {
592
- this.destroy();
593
- this.initializeLiveAds(this.hlsInstance);
594
- }
595
- }
596
-
597
- /**
598
- * Clean up live stream specific resources
245
+ * Cleanup
599
246
  */
600
247
  destroy(): void {
601
- // Clear periodic timer
602
248
  if (this.periodicAdTimer) {
603
249
  clearInterval(this.periodicAdTimer);
604
250
  this.periodicAdTimer = null;
605
251
  }
606
252
 
607
- // Clear metadata listeners
608
- if (this.hlsInstance && typeof Hls !== 'undefined') {
609
- this.hlsInstance.off(Hls.Events.FRAG_PARSING_METADATA);
610
- this.hlsInstance.off(Hls.Events.LEVEL_UPDATED);
611
- }
612
-
613
- if (this.metadataTrack) {
614
- this.metadataTrack.removeEventListener('cuechange', () => {});
615
- this.metadataTrack = null;
616
- }
617
-
618
- // Clear state
619
- this.processedCuePoints.clear();
620
- this.hlsInstance = null;
621
-
622
- // Call parent destroy
623
253
  super.destroy();
624
254
  }
625
255
  }