unified-video-framework 1.4.216 → 1.4.218

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,691 +0,0 @@
1
- /**
2
- * AdsManager - Core ads orchestration for Unified Video Framework
3
- * Handles ad breaks, Google IMA SDK integration, and custom ad formats
4
- * Author: Flicknexs Team
5
- */
6
-
7
- import {
8
- AdsManagerConfig,
9
- Ad,
10
- AdBreak,
11
- AdPlacement,
12
- AdEventType,
13
- AdEvent,
14
- AdError,
15
- AdType,
16
- AdConfigRequest,
17
- AdConfigResponse,
18
- TrackEventRequest,
19
- } from './types';
20
-
21
- export class AdsManager {
22
- private config: AdsManagerConfig;
23
- private adContainer: HTMLElement;
24
- private videoElement: HTMLVideoElement;
25
- private currentAdBreak: AdBreak | null = null;
26
- private currentAd: Ad | null = null;
27
- private adBreaks: AdBreak[] = [];
28
- private sessionId: string;
29
- private contentStarted: boolean = false;
30
- private adsCompleted: Set<string> = new Set();
31
-
32
- // Google IMA SDK properties
33
- private imaSDKLoaded: boolean = false;
34
- private adsLoader: any = null;
35
- private adsManager: any = null;
36
- private adDisplayContainer: any = null;
37
-
38
- // Event listeners
39
- private eventListeners: Map<string, Function[]> = new Map();
40
-
41
- // State
42
- private isAdPlaying: boolean = false;
43
- private pauseAdShown: boolean = false;
44
- private originalVideoSrc: string = '';
45
-
46
- constructor(config: AdsManagerConfig) {
47
- this.config = {
48
- adsManagerUrl: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
49
- debug: false,
50
- maxAdsPerBreak: 3,
51
- maxTotalAds: 10,
52
- adTimeout: 5000,
53
- retryOnError: true,
54
- maxRetryAttempts: 3,
55
- ...config,
56
- };
57
-
58
- // Resolve container elements
59
- this.adContainer = this.resolveElement(this.config.adContainer);
60
- this.videoElement = this.resolveElement(this.config.videoElement) as HTMLVideoElement;
61
-
62
- // Generate session ID
63
- this.sessionId = this.generateSessionId();
64
-
65
- // Store original video source
66
- this.originalVideoSrc = this.videoElement.src || this.videoElement.currentSrc;
67
-
68
- // Initialize
69
- this.init();
70
- }
71
-
72
- // ============================================================================
73
- // INITIALIZATION
74
- // ============================================================================
75
-
76
- private async init(): Promise<void> {
77
- try {
78
- if (this.config.adsEnabled) {
79
- await this.loadIMASDK();
80
- this.setupAdContainer();
81
- this.setupVideoListeners();
82
-
83
- if (this.config.apiEndpoint) {
84
- await this.fetchAdConfiguration();
85
- }
86
-
87
- this.log('AdsManager initialized successfully');
88
- }
89
- } catch (error) {
90
- this.handleError({
91
- code: 1000,
92
- message: 'Failed to initialize AdsManager',
93
- details: error,
94
- });
95
- }
96
- }
97
-
98
- private async loadIMASDK(): Promise<void> {
99
- return new Promise((resolve, reject) => {
100
- // Check if already loaded
101
- if (typeof (window as any).google !== 'undefined' && (window as any).google.ima) {
102
- this.imaSDKLoaded = true;
103
- this.initializeIMA();
104
- resolve();
105
- return;
106
- }
107
-
108
- // Load IMA SDK
109
- const script = document.createElement('script');
110
- script.src = this.config.adsManagerUrl!;
111
- script.async = true;
112
-
113
- script.onload = () => {
114
- this.imaSDKLoaded = true;
115
- this.initializeIMA();
116
- this.log('Google IMA SDK loaded');
117
- resolve();
118
- };
119
-
120
- script.onerror = () => {
121
- reject(new Error('Failed to load Google IMA SDK'));
122
- };
123
-
124
- document.head.appendChild(script);
125
- });
126
- }
127
-
128
- private initializeIMA(): void {
129
- const ima = (window as any).google.ima;
130
-
131
- // Create ad display container
132
- this.adDisplayContainer = new ima.AdDisplayContainer(
133
- this.adContainer,
134
- this.videoElement
135
- );
136
-
137
- // Create ads loader
138
- this.adsLoader = new ima.AdsLoader(this.adDisplayContainer);
139
-
140
- // Attach event listeners
141
- this.adsLoader.addEventListener(
142
- ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
143
- this.onAdsManagerLoaded.bind(this),
144
- false
145
- );
146
-
147
- this.adsLoader.addEventListener(
148
- ima.AdErrorEvent.Type.AD_ERROR,
149
- this.onAdError.bind(this),
150
- false
151
- );
152
- }
153
-
154
- private setupAdContainer(): void {
155
- // Style ad container
156
- this.adContainer.style.position = 'absolute';
157
- this.adContainer.style.top = '0';
158
- this.adContainer.style.left = '0';
159
- this.adContainer.style.width = '100%';
160
- this.adContainer.style.height = '100%';
161
- this.adContainer.style.zIndex = '1000';
162
- this.adContainer.style.display = 'none';
163
- }
164
-
165
- private setupVideoListeners(): void {
166
- // Listen for video events to trigger ad breaks
167
- this.videoElement.addEventListener('play', this.onVideoPlay.bind(this));
168
- this.videoElement.addEventListener('pause', this.onVideoPause.bind(this));
169
- this.videoElement.addEventListener('timeupdate', this.onVideoTimeUpdate.bind(this));
170
- this.videoElement.addEventListener('ended', this.onVideoEnded.bind(this));
171
- }
172
-
173
- // ============================================================================
174
- // AD CONFIGURATION
175
- // ============================================================================
176
-
177
- private async fetchAdConfiguration(): Promise<void> {
178
- try {
179
- const request: AdConfigRequest = {
180
- contentId: this.getContentId(),
181
- sessionId: this.sessionId,
182
- placement: AdPlacement.PREROLL,
183
- width: this.videoElement.clientWidth,
184
- height: this.videoElement.clientHeight,
185
- deviceType: this.detectDeviceType(),
186
- browser: navigator.userAgent,
187
- language: navigator.language,
188
- referrer: document.referrer,
189
- gdprConsent: this.config.gdprConsent,
190
- coppa: this.config.coppa,
191
- };
192
-
193
- const response = await fetch(
194
- `${this.config.apiEndpoint}/config?${new URLSearchParams(request as any)}`,
195
- {
196
- method: 'GET',
197
- headers: {
198
- 'Content-Type': 'application/json',
199
- },
200
- }
201
- );
202
-
203
- const data: AdConfigResponse = await response.json();
204
-
205
- if (data.status === 'success' && data.data) {
206
- this.adBreaks = data.data.adBreaks;
207
- this.log('Ad configuration fetched', data);
208
-
209
- // Trigger preroll if configured
210
- if (this.config.preroll && this.adBreaks.some(ab => ab.type === AdPlacement.PREROLL)) {
211
- this.requestPrerollAds();
212
- }
213
- } else if (data.status === 'no_ads') {
214
- this.log('No ads available');
215
- } else {
216
- throw new Error(data.message || 'Failed to fetch ad configuration');
217
- }
218
- } catch (error) {
219
- this.handleError({
220
- code: 1001,
221
- message: 'Failed to fetch ad configuration',
222
- details: error,
223
- });
224
- }
225
- }
226
-
227
- // ============================================================================
228
- // AD REQUEST
229
- // ============================================================================
230
-
231
- public requestPrerollAds(): void {
232
- const prerollBreak = this.adBreaks.find(ab => ab.type === AdPlacement.PREROLL);
233
- if (prerollBreak) {
234
- this.requestAdBreak(prerollBreak);
235
- }
236
- }
237
-
238
- public requestMidrollAds(position: number): void {
239
- const midrollBreak = this.adBreaks.find(
240
- ab => ab.type === AdPlacement.MIDROLL && ab.position === position
241
- );
242
- if (midrollBreak) {
243
- this.requestAdBreak(midrollBreak);
244
- }
245
- }
246
-
247
- public requestPostrollAds(): void {
248
- const postrollBreak = this.adBreaks.find(ab => ab.type === AdPlacement.POSTROLL);
249
- if (postrollBreak) {
250
- this.requestAdBreak(postrollBreak);
251
- }
252
- }
253
-
254
- private requestAdBreak(adBreak: AdBreak): void {
255
- this.currentAdBreak = adBreak;
256
-
257
- // Use first ad's VAST URL if available
258
- const firstAd = adBreak.ads[0];
259
- if (firstAd && firstAd.vastUrl) {
260
- this.requestAdsFromVAST(firstAd.vastUrl);
261
- } else if (firstAd && firstAd.videoUrl) {
262
- // Custom ad without VAST
263
- this.playCustomAd(firstAd);
264
- }
265
-
266
- this.emit(AdEventType.AD_BREAK_READY, { adBreak });
267
- if (this.config.onAdBreakReady) {
268
- this.config.onAdBreakReady(adBreak);
269
- }
270
- }
271
-
272
- private requestAdsFromVAST(vastUrl: string): void {
273
- const ima = (window as any).google.ima;
274
- const adsRequest = new ima.AdsRequest();
275
- adsRequest.adTagUrl = vastUrl;
276
- adsRequest.linearAdSlotWidth = this.videoElement.clientWidth;
277
- adsRequest.linearAdSlotHeight = this.videoElement.clientHeight;
278
- adsRequest.nonLinearAdSlotWidth = this.videoElement.clientWidth;
279
- adsRequest.nonLinearAdSlotHeight = this.videoElement.clientHeight / 3;
280
-
281
- this.adsLoader.requestAds(adsRequest);
282
- }
283
-
284
- // ============================================================================
285
- // IMA EVENT HANDLERS
286
- // ============================================================================
287
-
288
- private onAdsManagerLoaded(adsManagerLoadedEvent: any): void {
289
- const ima = (window as any).google.ima;
290
- const adsRenderingSettings = new ima.AdsRenderingSettings();
291
- adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
292
-
293
- this.adsManager = adsManagerLoadedEvent.getAdsManager(
294
- this.videoElement,
295
- adsRenderingSettings
296
- );
297
-
298
- this.attachIMAEventListeners();
299
- this.startAds();
300
- }
301
-
302
- private attachIMAEventListeners(): void {
303
- const ima = (window as any).google.ima;
304
-
305
- this.adsManager.addEventListener(ima.AdEvent.Type.LOADED, this.onAdLoaded.bind(this));
306
- this.adsManager.addEventListener(ima.AdEvent.Type.STARTED, this.onAdStarted.bind(this));
307
- this.adsManager.addEventListener(ima.AdEvent.Type.FIRST_QUARTILE, this.onAdQuartile.bind(this));
308
- this.adsManager.addEventListener(ima.AdEvent.Type.MIDPOINT, this.onAdQuartile.bind(this));
309
- this.adsManager.addEventListener(ima.AdEvent.Type.THIRD_QUARTILE, this.onAdQuartile.bind(this));
310
- this.adsManager.addEventListener(ima.AdEvent.Type.COMPLETE, this.onAdComplete.bind(this));
311
- this.adsManager.addEventListener(ima.AdEvent.Type.SKIPPED, this.onAdSkipped.bind(this));
312
- this.adsManager.addEventListener(ima.AdEvent.Type.CLICKED, this.onAdClicked.bind(this));
313
- this.adsManager.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, this.onAdError.bind(this));
314
- }
315
-
316
- private startAds(): void {
317
- try {
318
- this.adDisplayContainer.initialize();
319
-
320
- const width = this.videoElement.clientWidth;
321
- const height = this.videoElement.clientHeight;
322
- const viewMode = 'normal';
323
-
324
- this.adsManager.init(width, height, viewMode);
325
- this.adsManager.start();
326
-
327
- this.isAdPlaying = true;
328
- this.adContainer.style.display = 'block';
329
-
330
- if (this.currentAdBreak) {
331
- this.emit(AdEventType.AD_BREAK_STARTED, { adBreak: this.currentAdBreak });
332
- if (this.config.onAdBreakStarted) {
333
- this.config.onAdBreakStarted(this.currentAdBreak);
334
- }
335
- }
336
- } catch (error) {
337
- this.handleError({
338
- code: 1002,
339
- message: 'Failed to start ads',
340
- details: error,
341
- });
342
- }
343
- }
344
-
345
- private onAdLoaded(adEvent: any): void {
346
- this.log('Ad loaded', adEvent);
347
- this.emit(AdEventType.LOADED, { ad: this.currentAd || undefined });
348
- }
349
-
350
- private onAdStarted(adEvent: any): void {
351
- const ad = adEvent.getAd();
352
- this.currentAd = {
353
- id: ad.getAdId(),
354
- type: AdType.SKIPPABLE_IN_STREAM, // Determine from ad properties
355
- metadata: {
356
- id: ad.getAdId(),
357
- title: ad.getTitle(),
358
- description: ad.getDescription(),
359
- advertiser: ad.getAdvertiserName(),
360
- duration: ad.getDuration(),
361
- skippable: ad.isSkippable(),
362
- skipOffset: ad.getSkipTimeOffset(),
363
- },
364
- };
365
-
366
- this.log('Ad started', this.currentAd);
367
- this.emit(AdEventType.STARTED, { ad: this.currentAd });
368
- this.trackEvent(AdEventType.STARTED);
369
-
370
- if (this.config.onAdStarted) {
371
- this.config.onAdStarted(this.currentAd);
372
- }
373
- }
374
-
375
- private onAdQuartile(adEvent: any): void {
376
- const eventType = adEvent.type;
377
- let quartileEvent: AdEventType;
378
-
379
- if (eventType.includes('FIRST_QUARTILE')) {
380
- quartileEvent = AdEventType.FIRST_QUARTILE;
381
- } else if (eventType.includes('MIDPOINT')) {
382
- quartileEvent = AdEventType.MIDPOINT;
383
- } else if (eventType.includes('THIRD_QUARTILE')) {
384
- quartileEvent = AdEventType.THIRD_QUARTILE;
385
- } else {
386
- return;
387
- }
388
-
389
- this.log(`Ad ${quartileEvent}`, this.currentAd);
390
- this.emit(quartileEvent, { ad: this.currentAd || undefined });
391
- this.trackEvent(quartileEvent);
392
- }
393
-
394
- private onAdComplete(adEvent: any): void {
395
- this.log('Ad completed', this.currentAd);
396
- this.emit(AdEventType.COMPLETED, { ad: this.currentAd || undefined });
397
- this.trackEvent(AdEventType.COMPLETED);
398
-
399
- if (this.config.onAdCompleted && this.currentAd) {
400
- this.config.onAdCompleted(this.currentAd);
401
- }
402
-
403
- this.endAdBreak();
404
- }
405
-
406
- private onAdSkipped(adEvent: any): void {
407
- this.log('Ad skipped', this.currentAd);
408
- this.emit(AdEventType.SKIPPED, { ad: this.currentAd || undefined });
409
- this.trackEvent(AdEventType.SKIPPED);
410
-
411
- if (this.config.onAdSkipped && this.currentAd) {
412
- this.config.onAdSkipped(this.currentAd);
413
- }
414
-
415
- this.endAdBreak();
416
- }
417
-
418
- private onAdClicked(adEvent: any): void {
419
- this.log('Ad clicked', this.currentAd);
420
- this.emit(AdEventType.CLICKED, { ad: this.currentAd || undefined });
421
- this.trackEvent(AdEventType.CLICKED);
422
- }
423
-
424
- private onAdError(adErrorEvent: any): void {
425
- const error = adErrorEvent.getError();
426
- this.handleError({
427
- code: error.getErrorCode(),
428
- message: error.getMessage(),
429
- ad: this.currentAd || undefined,
430
- adBreak: this.currentAdBreak || undefined,
431
- details: error,
432
- });
433
-
434
- // Resume content on error
435
- this.endAdBreak();
436
- }
437
-
438
- private endAdBreak(): void {
439
- this.isAdPlaying = false;
440
- this.adContainer.style.display = 'none';
441
-
442
- if (this.currentAdBreak) {
443
- this.emit(AdEventType.AD_BREAK_COMPLETED, { adBreak: this.currentAdBreak });
444
- if (this.config.onAdBreakCompleted) {
445
- this.config.onAdBreakCompleted(this.currentAdBreak);
446
- }
447
- }
448
-
449
- this.currentAd = null;
450
- this.currentAdBreak = null;
451
-
452
- // Resume content
453
- if (!this.contentStarted) {
454
- this.videoElement.play();
455
- this.contentStarted = true;
456
- }
457
- }
458
-
459
- // ============================================================================
460
- // VIDEO EVENT HANDLERS
461
- // ============================================================================
462
-
463
- private onVideoPlay(): void {
464
- if (!this.contentStarted && this.config.preroll) {
465
- this.videoElement.pause();
466
- this.requestPrerollAds();
467
- }
468
- }
469
-
470
- private onVideoPause(): void {
471
- // Trigger pause ads if configured
472
- if (this.config.pauseAds && !this.pauseAdShown && !this.isAdPlaying) {
473
- setTimeout(() => {
474
- if (this.videoElement.paused) {
475
- this.showPauseAd();
476
- }
477
- }, (this.config.pauseAds.triggers.minPauseDuration || 3) * 1000);
478
- }
479
- }
480
-
481
- private onVideoTimeUpdate(): void {
482
- // Check for midroll ad breaks
483
- const currentTime = this.videoElement.currentTime;
484
-
485
- this.adBreaks.forEach((adBreak) => {
486
- if (adBreak.type === AdPlacement.MIDROLL) {
487
- const position = typeof adBreak.position === 'number' ? adBreak.position : 0;
488
-
489
- if (
490
- !this.adsCompleted.has(adBreak.id) &&
491
- currentTime >= position &&
492
- currentTime < position + 1
493
- ) {
494
- this.videoElement.pause();
495
- this.requestAdBreak(adBreak);
496
- this.adsCompleted.add(adBreak.id);
497
- }
498
- }
499
- });
500
- }
501
-
502
- private onVideoEnded(): void {
503
- if (this.config.postroll) {
504
- this.requestPostrollAds();
505
- } else {
506
- this.emit(AdEventType.ALL_ADS_COMPLETED, {});
507
- if (this.config.onAllAdsCompleted) {
508
- this.config.onAllAdsCompleted();
509
- }
510
- }
511
- }
512
-
513
- // ============================================================================
514
- // CUSTOM ADS
515
- // ============================================================================
516
-
517
- private playCustomAd(ad: Ad): void {
518
- // Implementation for custom ads (non-VAST)
519
- // This would handle direct video URL playback
520
- this.log('Playing custom ad', ad);
521
- // TODO: Implement custom ad playback logic
522
- }
523
-
524
- private showPauseAd(): void {
525
- // Implementation for pause overlay ads
526
- this.pauseAdShown = true;
527
- this.log('Showing pause ad');
528
- // TODO: Implement pause ad UI
529
- }
530
-
531
- // ============================================================================
532
- // EVENT TRACKING
533
- // ============================================================================
534
-
535
- private async trackEvent(eventType: AdEventType): Promise<void> {
536
- if (!this.config.apiEndpoint || !this.currentAd) return;
537
-
538
- try {
539
- const request: TrackEventRequest = {
540
- eventType,
541
- adId: this.currentAd.id,
542
- sessionId: this.sessionId,
543
- contentId: this.getContentId(),
544
- timestamp: new Date().toISOString(),
545
- playerTime: this.videoElement.currentTime,
546
- };
547
-
548
- await fetch(`${this.config.apiEndpoint}/track/event`, {
549
- method: 'POST',
550
- headers: {
551
- 'Content-Type': 'application/json',
552
- },
553
- body: JSON.stringify(request),
554
- });
555
-
556
- this.log(`Tracked event: ${eventType}`);
557
- } catch (error) {
558
- this.log(`Failed to track event: ${eventType}`, error);
559
- }
560
- }
561
-
562
- // ============================================================================
563
- // EVENT EMITTER
564
- // ============================================================================
565
-
566
- public on(eventType: AdEventType | string, callback: Function): void {
567
- if (!this.eventListeners.has(eventType)) {
568
- this.eventListeners.set(eventType, []);
569
- }
570
- this.eventListeners.get(eventType)!.push(callback);
571
- }
572
-
573
- public off(eventType: AdEventType | string, callback: Function): void {
574
- const listeners = this.eventListeners.get(eventType);
575
- if (listeners) {
576
- const index = listeners.indexOf(callback);
577
- if (index > -1) {
578
- listeners.splice(index, 1);
579
- }
580
- }
581
- }
582
-
583
- private emit(eventType: AdEventType | string, data: Partial<AdEvent>): void {
584
- const listeners = this.eventListeners.get(eventType);
585
- if (listeners) {
586
- const event: AdEvent = {
587
- type: eventType as AdEventType,
588
- timestamp: new Date().toISOString(),
589
- playerTime: this.videoElement.currentTime,
590
- ...data,
591
- };
592
-
593
- listeners.forEach((callback) => callback(event));
594
- }
595
- }
596
-
597
- // ============================================================================
598
- // ERROR HANDLING
599
- // ============================================================================
600
-
601
- private handleError(error: AdError): void {
602
- this.log('Ad error', error);
603
- this.emit(AdEventType.AD_ERROR, { customData: error });
604
-
605
- if (this.config.onAdError) {
606
- this.config.onAdError(error);
607
- }
608
- }
609
-
610
- // ============================================================================
611
- // UTILITY METHODS
612
- // ============================================================================
613
-
614
- private resolveElement(element: HTMLElement | string): HTMLElement {
615
- if (typeof element === 'string') {
616
- const el = document.querySelector(element);
617
- if (!el) {
618
- throw new Error(`Element not found: ${element}`);
619
- }
620
- return el as HTMLElement;
621
- }
622
- return element;
623
- }
624
-
625
- private generateSessionId(): string {
626
- return `ads-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
627
- }
628
-
629
- private getContentId(): string {
630
- // Extract from video source or use a default
631
- return this.originalVideoSrc || 'unknown';
632
- }
633
-
634
- private detectDeviceType(): 'desktop' | 'mobile' | 'tablet' | 'tv' {
635
- const ua = navigator.userAgent;
636
- if (/mobile/i.test(ua)) return 'mobile';
637
- if (/tablet|ipad/i.test(ua)) return 'tablet';
638
- if (/tv|smarttv/i.test(ua)) return 'tv';
639
- return 'desktop';
640
- }
641
-
642
- private log(message: string, ...args: any[]): void {
643
- if (this.config.debug) {
644
- console.log(`[AdsManager] ${message}`, ...args);
645
- }
646
- }
647
-
648
- // ============================================================================
649
- // PUBLIC API
650
- // ============================================================================
651
-
652
- public destroy(): void {
653
- if (this.adsManager) {
654
- this.adsManager.destroy();
655
- }
656
-
657
- if (this.adsLoader) {
658
- this.adsLoader.destroy();
659
- }
660
-
661
- this.eventListeners.clear();
662
- this.adBreaks = [];
663
- this.adsCompleted.clear();
664
-
665
- this.log('AdsManager destroyed');
666
- }
667
-
668
- public pause(): void {
669
- if (this.adsManager && this.isAdPlaying) {
670
- this.adsManager.pause();
671
- }
672
- }
673
-
674
- public resume(): void {
675
- if (this.adsManager && this.isAdPlaying) {
676
- this.adsManager.resume();
677
- }
678
- }
679
-
680
- public skip(): void {
681
- if (this.adsManager && this.isAdPlaying) {
682
- this.adsManager.skip();
683
- }
684
- }
685
-
686
- public resize(width: number, height: number): void {
687
- if (this.adsManager) {
688
- this.adsManager.resize(width, height, 'normal');
689
- }
690
- }
691
- }