unified-video-framework 1.4.372 → 1.4.374

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.
@@ -13,18 +13,18 @@
13
13
  export interface GoogleAdsConfig {
14
14
  // Ad tag URL (VAST/VMAP)
15
15
  adTagUrl: string;
16
-
16
+
17
17
  // Optional: Specific ad break times (for mid-rolls)
18
18
  // If not provided, uses VMAP schedule from ad server
19
19
  midrollTimes?: number[]; // e.g., [30, 60, 120] = ads at 30s, 60s, 120s
20
-
20
+
21
21
  // Companion ad containers
22
22
  companionAdSlots?: Array<{
23
23
  containerId: string; // HTML element ID
24
24
  width: number;
25
25
  height: number;
26
26
  }>;
27
-
27
+
28
28
  // Callbacks
29
29
  onAdStart?: () => void;
30
30
  onAdEnd?: () => void;
@@ -48,11 +48,11 @@ export class GoogleAdsManager {
48
48
  this.video = video;
49
49
  this.adContainer = adContainer;
50
50
  this.config = config;
51
-
51
+
52
52
  // Add focus handler to resume ads after click-through
53
53
  this.setupFocusHandler();
54
54
  }
55
-
55
+
56
56
  /**
57
57
  * Setup focus handler to resume ads when window regains focus
58
58
  */
@@ -66,7 +66,7 @@ export class GoogleAdsManager {
66
66
  setTimeout(() => this.resumeAdPlayback(), 200);
67
67
  }
68
68
  });
69
-
69
+
70
70
  // Also check visibility change (tab switch)
71
71
  document.addEventListener('visibilitychange', () => {
72
72
  if (!document.hidden && this.isAdPlaying) {
@@ -113,30 +113,30 @@ export class GoogleAdsManager {
113
113
  */
114
114
  private setupAdsLoader(): void {
115
115
  const google = (window as any).google;
116
-
116
+
117
117
  // Create ad display container
118
118
  this.adDisplayContainer = new google.ima.AdDisplayContainer(
119
119
  this.adContainer,
120
120
  this.video
121
121
  );
122
-
122
+
123
123
  // Create ads loader
124
124
  this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer);
125
-
125
+
126
126
  // Register for ads loaded event
127
127
  this.adsLoader.addEventListener(
128
128
  google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
129
129
  (event: any) => this.onAdsManagerLoaded(event),
130
130
  false
131
131
  );
132
-
132
+
133
133
  // Register for error event
134
134
  this.adsLoader.addEventListener(
135
135
  google.ima.AdErrorEvent.Type.AD_ERROR,
136
136
  (event: any) => this.onAdError(event),
137
137
  false
138
138
  );
139
-
139
+
140
140
  // Signal when video content completes (for post-rolls)
141
141
  this.video.addEventListener('ended', () => {
142
142
  if (!this.isAdPlaying) {
@@ -150,26 +150,29 @@ export class GoogleAdsManager {
150
150
  */
151
151
  requestAds(): void {
152
152
  const google = (window as any).google;
153
-
153
+
154
154
  try {
155
155
  const adsRequest = new google.ima.AdsRequest();
156
156
  adsRequest.adTagUrl = this.config.adTagUrl;
157
-
157
+
158
158
  // Set video dimensions
159
159
  adsRequest.linearAdSlotWidth = this.video.clientWidth;
160
160
  adsRequest.linearAdSlotHeight = this.video.clientHeight;
161
161
  adsRequest.nonLinearAdSlotWidth = this.video.clientWidth;
162
162
  adsRequest.nonLinearAdSlotHeight = Math.floor(this.video.clientHeight / 3);
163
-
163
+
164
164
  // Set companion ad slots if provided
165
165
  if (this.config.companionAdSlots && this.config.companionAdSlots.length > 0) {
166
166
  const companionAdSlots = this.config.companionAdSlots.map(slot => {
167
167
  return new google.ima.CompanionAdSelectionSettings();
168
168
  });
169
- adsRequest.setAdWillAutoPlay(true);
170
- adsRequest.setAdWillPlayMuted(false);
171
169
  }
172
-
170
+
171
+ // Chrome autoplay policy: ads must start muted for autoplay to work
172
+ // User can unmute after ad starts
173
+ adsRequest.setAdWillAutoPlay(true);
174
+ adsRequest.setAdWillPlayMuted(true); // Start muted for Chrome compatibility
175
+
173
176
  // Request ads
174
177
  this.adsLoader.requestAds(adsRequest);
175
178
  } catch (error) {
@@ -194,16 +197,17 @@ export class GoogleAdsManager {
194
197
  */
195
198
  private onAdsManagerLoaded(event: any): void {
196
199
  const google = (window as any).google;
197
-
200
+
198
201
  // Setup ads rendering settings
199
202
  const adsRenderingSettings = new google.ima.AdsRenderingSettings();
200
203
  adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
201
204
  adsRenderingSettings.enablePreloading = true;
202
- adsRenderingSettings.mute = false; // Enable audio for ads
203
-
205
+ // Start muted for Chrome autoplay compatibility - user can unmute via button
206
+ adsRenderingSettings.mute = this.video.muted;
207
+
204
208
  // Get the ads manager
205
209
  this.adsManager = event.getAdsManager(this.video, adsRenderingSettings);
206
-
210
+
207
211
  // Extract cue points (ad break times) from VMAP/ad server
208
212
  try {
209
213
  const cuePoints = this.adsManager.getCuePoints();
@@ -215,9 +219,9 @@ export class GoogleAdsManager {
215
219
  if (time === -1) return -1; // Post-roll (will be converted to video duration later)
216
220
  return time; // Mid-roll at specific time
217
221
  });
218
-
222
+
219
223
  console.log('📍 Ad cue points detected (pre/mid/post):', allCuePoints);
220
-
224
+
221
225
  // Notify callback with all cue points
222
226
  if (this.config.onAdCuePoints) {
223
227
  this.config.onAdCuePoints(allCuePoints);
@@ -226,10 +230,10 @@ export class GoogleAdsManager {
226
230
  } catch (error) {
227
231
  console.warn('Could not extract ad cue points:', error);
228
232
  }
229
-
233
+
230
234
  // Setup ads manager event listeners
231
235
  this.setupAdsManagerListeners();
232
-
236
+
233
237
  try {
234
238
  // Initialize ads manager
235
239
  this.adsManager.init(
@@ -237,12 +241,12 @@ export class GoogleAdsManager {
237
241
  this.video.clientHeight,
238
242
  google.ima.ViewMode.NORMAL
239
243
  );
240
-
244
+
241
245
  // Start ads
242
246
  this.adsManager.start();
243
247
  } catch (error) {
244
248
  console.error('Error starting ads:', error);
245
- this.video.play().catch(() => {});
249
+ this.video.play().catch(() => { });
246
250
  }
247
251
  }
248
252
 
@@ -251,7 +255,7 @@ export class GoogleAdsManager {
251
255
  */
252
256
  private setupAdsManagerListeners(): void {
253
257
  const google = (window as any).google;
254
-
258
+
255
259
  // Content pause - ad is about to play
256
260
  this.adsManager.addEventListener(
257
261
  google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
@@ -259,21 +263,54 @@ export class GoogleAdsManager {
259
263
  console.log('Ad: Content paused');
260
264
  this.isAdPlaying = true;
261
265
  this.video.pause();
266
+
267
+ // Force ad container visibility and z-index
268
+ if (this.adContainer) {
269
+ this.adContainer.style.visibility = 'visible';
270
+ this.adContainer.style.opacity = '1';
271
+ this.adContainer.style.pointerEvents = 'auto';
272
+ this.adContainer.style.zIndex = '2147483647';
273
+ console.log('✅ Ad container visibility enforced');
274
+ }
275
+
276
+ // Strict enforcement: Prevent video from playing during ads
277
+ const preventPlayDuringAd = (e: Event) => {
278
+ if (this.isAdPlaying) {
279
+ e.preventDefault();
280
+ this.video.pause();
281
+ console.warn('Blocked video play attempt during ad');
282
+ }
283
+ };
284
+
285
+ // Add play event listener to block any play attempts
286
+ this.video.addEventListener('play', preventPlayDuringAd);
287
+
288
+ // Store cleanup function to remove listener later
289
+ (this.video as any).__adPlayBlocker = preventPlayDuringAd;
290
+
262
291
  this.config.onAdStart?.();
263
292
  }
264
293
  );
265
-
294
+
266
295
  // Content resume - ad finished
267
296
  this.adsManager.addEventListener(
268
297
  google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
269
298
  () => {
270
299
  console.log('Ad: Content resume');
271
300
  this.isAdPlaying = false;
301
+
302
+ // Remove play blocker
303
+ const preventPlayDuringAd = (this.video as any).__adPlayBlocker;
304
+ if (preventPlayDuringAd) {
305
+ this.video.removeEventListener('play', preventPlayDuringAd);
306
+ delete (this.video as any).__adPlayBlocker;
307
+ }
308
+
272
309
  this.config.onAdEnd?.();
273
- this.video.play().catch(() => {});
310
+ this.video.play().catch(() => { });
274
311
  }
275
312
  );
276
-
313
+
277
314
  // Ad started
278
315
  this.adsManager.addEventListener(
279
316
  google.ima.AdEvent.Type.STARTED,
@@ -285,19 +322,19 @@ export class GoogleAdsManager {
285
322
  skippable: ad.getSkipTimeOffset() !== -1,
286
323
  title: ad.getTitle(),
287
324
  });
288
-
325
+
289
326
  // Sync ads mute state with video element
290
327
  this.isMuted = this.video.muted;
291
328
  console.log(`Ad started - video.muted=${this.video.muted}, isMuted=${this.isMuted}`);
292
-
329
+
293
330
  // Check if video is actually muted and show unmute button only then
294
331
  if (this.isMuted) {
295
332
  this.showUnmuteButton();
296
333
  }
297
-
334
+
298
335
  }
299
336
  );
300
-
337
+
301
338
  // Ad completed
302
339
  this.adsManager.addEventListener(
303
340
  google.ima.AdEvent.Type.COMPLETE,
@@ -305,7 +342,7 @@ export class GoogleAdsManager {
305
342
  console.log('Ad completed');
306
343
  }
307
344
  );
308
-
345
+
309
346
  // All ads completed
310
347
  this.adsManager.addEventListener(
311
348
  google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
@@ -314,13 +351,13 @@ export class GoogleAdsManager {
314
351
  this.config.onAllAdsComplete?.();
315
352
  }
316
353
  );
317
-
354
+
318
355
  // Ad error
319
356
  this.adsManager.addEventListener(
320
357
  google.ima.AdErrorEvent.Type.AD_ERROR,
321
358
  (event: any) => this.onAdError(event)
322
359
  );
323
-
360
+
324
361
  // Ad skipped
325
362
  this.adsManager.addEventListener(
326
363
  google.ima.AdEvent.Type.SKIPPED,
@@ -328,7 +365,7 @@ export class GoogleAdsManager {
328
365
  console.log('Ad skipped by user');
329
366
  }
330
367
  );
331
-
368
+
332
369
  // Ad paused (by click-through)
333
370
  this.adsManager.addEventListener(
334
371
  google.ima.AdEvent.Type.PAUSED,
@@ -337,7 +374,7 @@ export class GoogleAdsManager {
337
374
  // Don't mark as not playing - keep isAdPlaying true so we can resume
338
375
  }
339
376
  );
340
-
377
+
341
378
  // Ad playing (resume after pause)
342
379
  this.adsManager.addEventListener(
343
380
  google.ima.AdEvent.Type.PLAYING,
@@ -353,17 +390,17 @@ export class GoogleAdsManager {
353
390
  private onAdError(event: any): void {
354
391
  const error = event.getError?.();
355
392
  console.error('Ad error:', error?.getMessage?.() || error);
356
-
393
+
357
394
  this.config.onAdError?.(error);
358
-
395
+
359
396
  // Destroy ads manager on error
360
397
  if (this.adsManager) {
361
398
  this.adsManager.destroy();
362
399
  }
363
-
400
+
364
401
  // Resume content playback
365
402
  this.isAdPlaying = false;
366
- this.video.play().catch(() => {});
403
+ this.video.play().catch(() => { });
367
404
  }
368
405
 
369
406
  /**
@@ -394,12 +431,27 @@ export class GoogleAdsManager {
394
431
  }
395
432
 
396
433
  /**
397
- * Resize ads
434
+ * Resize ads - with enhanced fullscreen support
398
435
  */
399
- resize(width: number, height: number): void {
436
+ resize(width: number, height: number, viewMode?: any): void {
400
437
  const google = (window as any).google;
401
- if (this.adsManager) {
402
- this.adsManager.resize(width, height, google.ima.ViewMode.NORMAL);
438
+ if (this.adsManager && google && google.ima) {
439
+ const mode = viewMode || google.ima.ViewMode.NORMAL;
440
+
441
+ console.log(`📐 Resizing ads: ${width}x${height}, ViewMode: ${mode === google.ima.ViewMode.FULLSCREEN ? 'FULLSCREEN' : 'NORMAL'}`);
442
+
443
+ // Force ad container dimensions in fullscreen
444
+ if (this.adContainer && mode === google.ima.ViewMode.FULLSCREEN) {
445
+ this.adContainer.style.position = 'fixed';
446
+ this.adContainer.style.top = '0';
447
+ this.adContainer.style.left = '0';
448
+ this.adContainer.style.width = `${width}px`;
449
+ this.adContainer.style.height = `${height}px`;
450
+ this.adContainer.style.zIndex = '2147483647';
451
+ console.log('✅ Ad container forced to fullscreen dimensions');
452
+ }
453
+
454
+ this.adsManager.resize(width, height, mode);
403
455
  }
404
456
  }
405
457
 
@@ -427,7 +479,7 @@ export class GoogleAdsManager {
427
479
  if (this.unmuteButton) {
428
480
  this.unmuteButton.remove();
429
481
  }
430
-
482
+
431
483
  // Create unmute button (matching WebPlayer.ts style)
432
484
  this.unmuteButton = document.createElement('button');
433
485
  this.unmuteButton.id = 'ad-unmute-btn';
@@ -439,13 +491,13 @@ export class GoogleAdsManager {
439
491
  </svg>
440
492
  <span class="uvf-unmute-text">Tap to unmute</span>
441
493
  `;
442
-
494
+
443
495
  // Click handler to unmute
444
496
  this.unmuteButton.addEventListener('click', (e) => {
445
497
  e.stopPropagation();
446
498
  this.toggleAdMute();
447
499
  });
448
-
500
+
449
501
  // Add styles if not already added
450
502
  if (!document.getElementById('uvf-unmute-styles')) {
451
503
  const style = document.createElement('style');
@@ -517,33 +569,33 @@ export class GoogleAdsManager {
517
569
  `;
518
570
  document.head.appendChild(style);
519
571
  }
520
-
572
+
521
573
  // Add to ad container
522
574
  this.adContainer.appendChild(this.unmuteButton);
523
575
  console.log('Unmute button displayed (matching player style)');
524
576
  }
525
-
577
+
526
578
  /**
527
579
  * Toggle ad mute state
528
580
  */
529
581
  private toggleAdMute(): void {
530
582
  this.isMuted = !this.isMuted;
531
-
583
+
532
584
  if (this.adsManager) {
533
585
  this.adsManager.setVolume(this.isMuted ? 0 : 1);
534
586
  console.log(`Ad ${this.isMuted ? 'muted' : 'unmuted'}`);
535
587
  }
536
-
588
+
537
589
  // Sync mute state with video element
538
590
  this.video.muted = this.isMuted;
539
-
591
+
540
592
  // Hide button if unmuted
541
593
  if (!this.isMuted && this.unmuteButton) {
542
594
  this.unmuteButton.remove();
543
595
  this.unmuteButton = null;
544
596
  }
545
597
  }
546
-
598
+
547
599
  /**
548
600
  * Resume ad playback after click-through
549
601
  */
@@ -553,9 +605,9 @@ export class GoogleAdsManager {
553
605
  if (!this.adsManager || !this.isAdPlaying) {
554
606
  return;
555
607
  }
556
-
608
+
557
609
  console.log('Attempting to resume ad playback...');
558
-
610
+
559
611
  // Try multiple resume methods
560
612
  try {
561
613
  this.adsManager.resume();
@@ -563,7 +615,7 @@ export class GoogleAdsManager {
563
615
  } catch (e) {
564
616
  console.warn('resume() failed:', e);
565
617
  }
566
-
618
+
567
619
  // Also try playing the video element directly
568
620
  if (this.video) {
569
621
  try {
@@ -584,7 +636,7 @@ export class GoogleAdsManager {
584
636
  console.error('Error resuming ad playback:', error);
585
637
  }
586
638
  }
587
-
639
+
588
640
  /**
589
641
  * Hide unmute button
590
642
  */
@@ -600,17 +652,17 @@ export class GoogleAdsManager {
600
652
  */
601
653
  destroy(): void {
602
654
  this.hideUnmuteButton();
603
-
655
+
604
656
  if (this.adsManager) {
605
657
  this.adsManager.destroy();
606
658
  this.adsManager = null;
607
659
  }
608
-
660
+
609
661
  if (this.adsLoader) {
610
662
  this.adsLoader.destroy();
611
663
  this.adsLoader = null;
612
664
  }
613
-
665
+
614
666
  this.isAdPlaying = false;
615
667
  }
616
668
  }
@@ -417,7 +417,7 @@ export class CreditsButtonController {
417
417
  backgroundColor: 'rgba(255, 255, 255, 0.9)',
418
418
  width: '0%',
419
419
  transition: 'width 1s linear',
420
- borderRadius: '0 0 6px 6px'
420
+ borderRadius: '6px'
421
421
  });
422
422
 
423
423
  return progressBar;
@@ -531,9 +531,41 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
531
531
 
532
532
  // Google Ads state
533
533
  const adsManagerRef = useRef<GoogleAdsManager | null>(null);
534
- const adContainerRef = useRef<HTMLDivElement>(null);
534
+ const adContainerRef = useRef<HTMLDivElement | null>(null);
535
535
  const [isAdPlaying, setIsAdPlaying] = useState(false);
536
-
536
+
537
+ // Create ad container element programmatically
538
+ useEffect(() => {
539
+ if (!props.googleAds || adContainerRef.current) return;
540
+
541
+ const adContainer = document.createElement('div');
542
+ adContainer.className = 'uvf-ad-container';
543
+ adContainer.style.cssText = `
544
+ position: absolute;
545
+ top: 0;
546
+ left: 0;
547
+ right: 0;
548
+ bottom: 0;
549
+ z-index: 999999999;
550
+ pointer-events: none;
551
+ visibility: hidden;
552
+ opacity: 0;
553
+ transition: opacity 0.2s ease, visibility 0.2s ease;
554
+ `;
555
+
556
+ adContainerRef.current = adContainer;
557
+ console.log('✅ Ad container element created');
558
+ }, [props.googleAds]);
559
+
560
+ // Update ad container visibility when isAdPlaying changes
561
+ useEffect(() => {
562
+ if (!adContainerRef.current) return;
563
+
564
+ adContainerRef.current.style.pointerEvents = isAdPlaying ? 'auto' : 'none';
565
+ adContainerRef.current.style.visibility = isAdPlaying ? 'visible' : 'hidden';
566
+ adContainerRef.current.style.opacity = isAdPlaying ? '1' : '0';
567
+ }, [isAdPlaying]);
568
+
537
569
  /**
538
570
  * Generate ad chapter segments from googleAds cue points
539
571
  * Handles pre-roll (0), mid-rolls, and post-roll (-1)
@@ -1165,8 +1197,59 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1165
1197
  if (props.onBuffering && typeof (player as any).on === 'function') {
1166
1198
  (player as any).on('onBuffering', props.onBuffering);
1167
1199
  }
1168
- if (props.onFullscreenChange && typeof (player as any).on === 'function') {
1169
- (player as any).on('onFullscreenChanged', props.onFullscreenChange);
1200
+ if (typeof (player as any).on === 'function') {
1201
+ (player as any).on('onFullscreenChanged', (isFullscreen: boolean) => {
1202
+ console.log(`🔄 Fullscreen changed: ${isFullscreen}`);
1203
+
1204
+ // Force ad container positioning for fullscreen
1205
+ if (adContainerRef.current) {
1206
+ const adContainer = adContainerRef.current;
1207
+
1208
+ if (isFullscreen) {
1209
+ // Fullscreen: use fixed positioning
1210
+ adContainer.style.position = 'fixed';
1211
+ adContainer.style.top = '0';
1212
+ adContainer.style.left = '0';
1213
+ adContainer.style.width = '100vw';
1214
+ adContainer.style.height = '100vh';
1215
+ adContainer.style.zIndex = '2147483647';
1216
+ console.log('✅ Ad container: fullscreen positioning applied');
1217
+ } else {
1218
+ // Normal: use absolute positioning
1219
+ adContainer.style.position = 'absolute';
1220
+ adContainer.style.top = '0';
1221
+ adContainer.style.left = '0';
1222
+ adContainer.style.right = '0';
1223
+ adContainer.style.bottom = '0';
1224
+ adContainer.style.width = '';
1225
+ adContainer.style.height = '';
1226
+ adContainer.style.zIndex = '999999999';
1227
+ console.log('✅ Ad container: normal positioning restored');
1228
+ }
1229
+ }
1230
+
1231
+ // Resize Google Ads if active
1232
+ if (adsManagerRef.current) {
1233
+ const google = (window as any).google;
1234
+ if (google && google.ima) {
1235
+ const width = isFullscreen
1236
+ ? window.screen.width
1237
+ : (containerRef.current?.clientWidth || window.innerWidth);
1238
+ const height = isFullscreen
1239
+ ? window.screen.height
1240
+ : (containerRef.current?.clientHeight || window.innerHeight);
1241
+ const viewMode = isFullscreen
1242
+ ? google.ima.ViewMode.FULLSCREEN
1243
+ : google.ima.ViewMode.NORMAL;
1244
+
1245
+ adsManagerRef.current.resize(width, height, viewMode);
1246
+ console.log(`✅ Ads resized: ${width}x${height}, ViewMode: ${viewMode === google.ima.ViewMode.FULLSCREEN ? 'FULLSCREEN' : 'NORMAL'}`);
1247
+ }
1248
+ }
1249
+
1250
+ // Call user's fullscreen callback
1251
+ props.onFullscreenChange?.(isFullscreen);
1252
+ });
1170
1253
  }
1171
1254
  if (props.onPictureInPictureChange && typeof (player as any).on === 'function') {
1172
1255
  (player as any).on('onPictureInPicturechange', props.onPictureInPictureChange);
@@ -1249,18 +1332,47 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1249
1332
 
1250
1333
  await adsManager.initialize();
1251
1334
  adsManagerRef.current = adsManager;
1252
-
1335
+
1253
1336
  console.log('Google Ads initialized successfully');
1254
-
1255
- // Initialize ad display container on first play
1337
+
1338
+ // Inject ad container into player wrapper (not as sibling)
1339
+ const playerWrapper = containerRef.current?.querySelector('.uvf-player-wrapper');
1340
+ if (playerWrapper && adContainerRef.current) {
1341
+ // Move ad container INSIDE player wrapper so it inherits fullscreen context
1342
+ playerWrapper.appendChild(adContainerRef.current);
1343
+ console.log('✅ Ad container injected into player wrapper');
1344
+ }
1345
+
1346
+ // Initialize ad display container on first user interaction
1347
+ // Chrome requires this to be called on a user gesture
1348
+ let adContainerInitialized = false;
1349
+
1350
+ const initAdsOnUserGesture = () => {
1351
+ if (!adContainerInitialized && adsManagerRef.current) {
1352
+ console.log('Initializing ad container on user gesture');
1353
+ try {
1354
+ adsManagerRef.current.initAdDisplayContainer();
1355
+ adContainerInitialized = true;
1356
+ } catch (err) {
1357
+ console.warn('Ad container init error:', err);
1358
+ }
1359
+ }
1360
+ };
1361
+
1256
1362
  const handleFirstPlay = () => {
1257
- if (adsManagerRef.current) {
1258
- adsManagerRef.current.initAdDisplayContainer();
1363
+ initAdsOnUserGesture();
1364
+ if (adsManagerRef.current && adContainerInitialized) {
1259
1365
  adsManagerRef.current.requestAds();
1260
1366
  }
1261
1367
  videoElement.removeEventListener('play', handleFirstPlay);
1368
+ videoElement.removeEventListener('click', initAdsOnUserGesture);
1369
+ adContainerRef.current?.removeEventListener('click', initAdsOnUserGesture);
1262
1370
  };
1371
+
1372
+ // Listen for both play and click events to ensure we catch user gesture
1263
1373
  videoElement.addEventListener('play', handleFirstPlay, { once: true });
1374
+ videoElement.addEventListener('click', initAdsOnUserGesture, { once: true });
1375
+ adContainerRef.current?.addEventListener('click', initAdsOnUserGesture, { once: true });
1264
1376
  } catch (adsError) {
1265
1377
  console.error('Failed to initialize Google Ads:', adsError);
1266
1378
  props.googleAds?.onAdError?.(adsError);
@@ -1597,29 +1709,13 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1597
1709
  }}
1598
1710
  >
1599
1711
  {/* Video Player */}
1600
- <div
1601
- ref={containerRef}
1712
+ <div
1713
+ ref={containerRef}
1602
1714
  className={`uvf-responsive-container ${props.className || ''}`}
1603
1715
  style={responsiveStyle}
1604
1716
  />
1605
-
1606
- {/* Google Ads Container - positioned over the video player and controls */}
1607
- {props.googleAds && (
1608
- <div
1609
- ref={adContainerRef}
1610
- className="uvf-ad-container"
1611
- style={{
1612
- position: 'fixed',
1613
- top: 0,
1614
- left: 0,
1615
- right: 0,
1616
- bottom: 0,
1617
- zIndex: 2147483647, // Maximum z-index - ads must be on top of everything
1618
- pointerEvents: isAdPlaying ? 'auto' : 'none', // Allow interaction only when ad is playing
1619
- display: isAdPlaying ? 'block' : 'none', // Hide container when no ad
1620
- }}
1621
- />
1622
- )}
1717
+
1718
+ {/* Ad container will be programmatically injected into player wrapper */}
1623
1719
 
1624
1720
 
1625
1721
  {/* EPG Overlay - Full-screen glassmorphic overlay */}