unified-video-framework 1.4.404 → 1.4.406

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,625 +0,0 @@
1
- /**
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
10
- */
11
-
12
- import { GoogleAdsManager, GoogleAdsConfig } from './GoogleAdsManager';
13
-
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
- 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)
37
-
38
- // Callbacks
39
- onLiveAdBreakDetected?: (metadata: any) => void;
40
- onAdBreakScheduled?: (scheduledTime: number) => void;
41
- onLiveEdgeSync?: (newPosition: number) => void;
42
- }
43
-
44
- export class LiveStreamAdsManager extends GoogleAdsManager {
45
- 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
53
- 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
-
61
- constructor(video: HTMLVideoElement, adContainer: HTMLElement, config: LiveStreamAdsConfig) {
62
- super(video, adContainer, config);
63
- this.liveConfig = {
64
- liveAdBreakMode: 'metadata',
65
- periodicAdInterval: 300, // Default: 5 minutes
66
- syncToLiveEdge: false, // Default: DVR-style (user sees all content)
67
- 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
- },
74
- ...config
75
- };
76
- }
77
-
78
- /**
79
- * Initialize live stream ads with specified mode
80
- */
81
- async initializeLiveAds(hlsInstance?: any): Promise<void> {
82
- // Initialize parent (Google IMA SDK)
83
- await this.initialize();
84
-
85
- this.isLiveStream = true;
86
- this.hlsInstance = hlsInstance;
87
- this.playbackStartTime = this.video.currentTime || 0;
88
- this.streamStartTime = Date.now();
89
-
90
- const mode = this.liveConfig.liveAdBreakMode || 'metadata';
91
-
92
- console.log(`🔴 Live Stream Ads initialized`);
93
- 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!;
128
-
129
- // Method 1: HLS.js metadata events
130
- if (this.hlsInstance && typeof Hls !== 'undefined') {
131
- this.setupHlsJsMetadata();
132
- }
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
- }
225
-
226
- /**
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
322
- */
323
- private setupPeriodicAdBreaks(): void {
324
- const interval = this.liveConfig.periodicAdInterval || 300;
325
-
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
- let accumulatedPlaybackTime = 0;
337
-
338
- // Check every second
339
- this.periodicAdTimer = setInterval(() => {
340
- // Only count time when video is actually playing
341
- if (!this.video.paused && !this.isPlayingAd()) {
342
- const currentTime = this.video.currentTime || 0;
343
- const timeDelta = currentTime - lastCheckTime;
344
-
345
- // Accumulate playback time (handle seeks)
346
- if (timeDelta > 0 && timeDelta < 5) { // Reasonable time delta
347
- accumulatedPlaybackTime += timeDelta;
348
- }
349
-
350
- lastCheckTime = currentTime;
351
-
352
- // Check if it's time for an ad break
353
- const timeSinceLastAd = accumulatedPlaybackTime - this.lastAdBreakTime;
354
-
355
- 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`);
359
-
360
- // Notify callback
361
- this.liveConfig.onAdBreakScheduled?.(accumulatedPlaybackTime);
362
-
363
- // Trigger ad break
364
- this.triggerAdBreak();
365
-
366
- // Update last ad break time
367
- this.lastAdBreakTime = accumulatedPlaybackTime;
368
- }
369
- }
370
- }, 1000);
371
- }
372
-
373
- /**
374
- * ========================================================================
375
- * AD BREAK TRIGGERING
376
- * ========================================================================
377
- */
378
-
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
- if (this.isPlayingAd()) {
387
- console.warn('⚠️ Ad already playing, skipping trigger');
388
- return;
389
- }
390
-
391
- // Store current time for DVR-style resume
392
- const timeBeforeAd = this.video.currentTime;
393
-
394
- console.log(`📍 Stream position before ad: ${timeBeforeAd.toFixed(2)}s`);
395
-
396
- // Pause stream downloading during ad (saves bandwidth)
397
- if (this.liveConfig.pauseStreamDuringAd) {
398
- this.pauseStreamDownload();
399
- }
400
-
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();
410
-
411
- // Setup post-ad resume handler
412
- this.setupLiveEdgeResume(timeBeforeAd);
413
- }
414
-
415
- /**
416
- * Pause stream downloading during ad (saves bandwidth)
417
- */
418
- private pauseStreamDownload(): void {
419
- try {
420
- if (this.hlsInstance && this.hlsInstance.stopLoad) {
421
- console.log('⏸️ Pausing stream download during ad');
422
- 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
- }
429
- } catch (error) {
430
- console.warn('Could not pause stream download:', error);
431
- }
432
- }
433
-
434
- /**
435
- * Resume stream downloading after ad
436
- */
437
- private resumeStreamDownload(): void {
438
- try {
439
- if (this.hlsInstance && this.hlsInstance.startLoad) {
440
- console.log('▶️ Resuming stream download after ad');
441
- 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
- }
448
- } catch (error) {
449
- console.warn('Could not resume stream download:', error);
450
- }
451
- }
452
-
453
- /**
454
- * Setup live edge resume after ad
455
- */
456
- private setupLiveEdgeResume(timeBeforeAd: number): void {
457
- // Listen for ad end
458
- const handleAdEnd = () => {
459
- if (this.liveConfig.syncToLiveEdge) {
460
- // Mode 1: Jump to live edge (user skips content during ad)
461
- console.log('📺 Ad ended, syncing to live edge (skipping content)');
462
- this.syncToLiveEdge();
463
- } else {
464
- // Mode 2: DVR-style resume from pause point (user sees all content)
465
- console.log('📺 Ad ended, resuming from pause point (DVR-style)');
466
- this.resumeFromPausePoint(timeBeforeAd);
467
- }
468
- };
469
-
470
- // Use one-time listener
471
- const originalOnAdEnd = this.config.onAdEnd;
472
- this.config.onAdEnd = () => {
473
- handleAdEnd();
474
- originalOnAdEnd?.();
475
- };
476
- }
477
-
478
- /**
479
- * Resume from where stream was paused (DVR-style)
480
- */
481
- private resumeFromPausePoint(pausedTime: number): void {
482
- setTimeout(() => {
483
- try {
484
- console.log(`⏯️ Resuming from paused position: ${pausedTime.toFixed(2)}s`);
485
- console.log(` User is now behind live, but will see all content`);
486
-
487
- // Resume stream downloading first
488
- if (this.liveConfig.pauseStreamDuringAd) {
489
- this.resumeStreamDownload();
490
- }
491
-
492
- // Resume from exact pause point
493
- this.video.currentTime = pausedTime;
494
-
495
- // Notify callback
496
- this.liveConfig.onLiveEdgeSync?.(pausedTime);
497
-
498
- // Calculate how far behind live user is
499
- if (this.hlsInstance && this.hlsInstance.liveSyncPosition !== undefined) {
500
- const liveEdge = this.hlsInstance.liveSyncPosition;
501
- const behindLive = liveEdge - pausedTime;
502
- console.log(` Behind live by: ${behindLive.toFixed(1)} seconds`);
503
- }
504
- } catch (error) {
505
- console.error('❌ Error resuming from pause point:', error);
506
- }
507
- }, 100); // Small delay to ensure ad container is hidden
508
- }
509
-
510
- /**
511
- * Sync video to live edge after ad
512
- */
513
- private syncToLiveEdge(): void {
514
- setTimeout(() => {
515
- try {
516
- // Resume stream downloading first
517
- if (this.liveConfig.pauseStreamDuringAd) {
518
- this.resumeStreamDownload();
519
- }
520
-
521
- let liveEdgePosition: number | null = null;
522
-
523
- // Get live edge from HLS.js
524
- if (this.hlsInstance && this.hlsInstance.liveSyncPosition !== undefined) {
525
- 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
- }
540
-
541
- // Apply live edge offset
542
- if (liveEdgePosition !== null) {
543
- const offset = this.liveConfig.liveEdgeOffset || 3;
544
- const targetPosition = Math.max(0, liveEdgePosition - offset);
545
-
546
- console.log(`⏩ Syncing to live edge: ${targetPosition.toFixed(2)}s (offset: ${offset}s)`);
547
-
548
- this.video.currentTime = targetPosition;
549
-
550
- // Notify callback
551
- this.liveConfig.onLiveEdgeSync?.(targetPosition);
552
- } else {
553
- console.warn('⚠️ Could not determine live edge position');
554
- }
555
- } catch (error) {
556
- console.error('❌ Error syncing to live edge:', error);
557
- }
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');
573
- }
574
-
575
- /**
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
599
- */
600
- destroy(): void {
601
- // Clear periodic timer
602
- if (this.periodicAdTimer) {
603
- clearInterval(this.periodicAdTimer);
604
- this.periodicAdTimer = null;
605
- }
606
-
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
- super.destroy();
624
- }
625
- }