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.
- package/CHANGELOG.md +0 -94
- package/package.json +1 -1
- package/packages/core/dist/interfaces.d.ts +2 -34
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces.ts +3 -51
- package/packages/web/dist/ads/GoogleAdsManager.d.ts +39 -0
- package/packages/web/dist/ads/GoogleAdsManager.d.ts.map +1 -0
- package/packages/web/dist/ads/GoogleAdsManager.js +180 -0
- package/packages/web/dist/ads/GoogleAdsManager.js.map +1 -0
- package/packages/web/dist/index.d.ts +0 -1
- package/packages/web/dist/index.d.ts.map +1 -1
- package/packages/web/dist/index.js +0 -1
- package/packages/web/dist/index.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +90 -0
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +162 -1
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/src/ads/GoogleAdsManager.ts +358 -0
- package/packages/web/src/index.ts +0 -3
- package/packages/web/src/react/WebPlayerView.tsx +304 -2
- package/packages/web/src/ads/AdsManager.ts +0 -691
- package/packages/web/src/ads/README.md +0 -403
- package/packages/web/src/ads/index.ts +0 -17
- package/packages/web/src/ads/types.ts +0 -442
|
@@ -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
|
-
}
|