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.
@@ -18,6 +18,13 @@ export interface GoogleAdsConfig {
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
+ // Periodic ad breaks (for live streams or automatic scheduling)
22
+ liveAdBreakMode?: 'periodic' | 'manual'; // 'periodic' = auto-schedule at intervals, 'manual' = use midrollTimes
23
+ periodicAdInterval?: number; // Interval in seconds between ads (e.g., 30 = ad every 30 seconds)
24
+ syncToLiveEdge?: boolean; // For live streams: sync back to live edge after ad
25
+ pauseStreamDuringAd?: boolean; // Pause underlying stream during ad playback
26
+ liveEdgeOffset?: number; // Seconds behind live edge to resume at (default: 3)
27
+
21
28
  // Companion ad containers
22
29
  companionAdSlots?: Array<{
23
30
  containerId: string; // HTML element ID
@@ -34,16 +41,20 @@ export interface GoogleAdsConfig {
34
41
  }
35
42
 
36
43
  export class GoogleAdsManager {
37
- protected video: HTMLVideoElement;
38
- protected adContainer: HTMLElement;
39
- protected config: GoogleAdsConfig;
44
+ private video: HTMLVideoElement;
45
+ private adContainer: HTMLElement;
46
+ private config: GoogleAdsConfig;
40
47
  private adsManager: any = null;
41
48
  private adsLoader: any = null;
42
49
  private adDisplayContainer: any = null;
43
50
  private isAdPlaying = false;
44
51
  private isMuted = true;
45
52
  private unmuteButton: HTMLElement | null = null;
46
- private adSafetyTimeout: any = null;
53
+
54
+ // Periodic ad scheduling
55
+ private lastAdTime = 0;
56
+ private periodicAdCheckInterval: any = null;
57
+ private pendingAdRequest = false;
47
58
 
48
59
  constructor(video: HTMLVideoElement, adContainer: HTMLElement, config: GoogleAdsConfig) {
49
60
  this.video = video;
@@ -146,6 +157,59 @@ export class GoogleAdsManager {
146
157
  });
147
158
  }
148
159
 
160
+ /**
161
+ * Setup periodic ad breaks (for live streams or auto-scheduling)
162
+ */
163
+ setupPeriodicAds(): void {
164
+ // Only setup if periodic mode is enabled
165
+ if (this.config.liveAdBreakMode !== 'periodic' || !this.config.periodicAdInterval) {
166
+ return;
167
+ }
168
+
169
+ console.log(`📺 Setting up periodic ads: interval=${this.config.periodicAdInterval}s`);
170
+
171
+ // Check every second if we should trigger an ad
172
+ this.periodicAdCheckInterval = setInterval(() => {
173
+ // Skip if ad is already playing or pending
174
+ if (this.isAdPlaying || this.pendingAdRequest) {
175
+ return;
176
+ }
177
+
178
+ const currentTime = this.video.currentTime;
179
+ const interval = this.config.periodicAdInterval || 30;
180
+
181
+ // Check if enough time has passed since last ad
182
+ if (currentTime - this.lastAdTime >= interval && currentTime > 0) {
183
+ console.log(`⏰ Periodic ad triggered at ${currentTime.toFixed(1)}s (interval: ${interval}s)`);
184
+ this.triggerPeriodicAd();
185
+ }
186
+ }, 1000); // Check every second
187
+
188
+ console.log('✅ Periodic ad scheduling enabled');
189
+ }
190
+
191
+ /**
192
+ * Trigger a periodic ad break
193
+ */
194
+ private triggerPeriodicAd(): void {
195
+ if (this.pendingAdRequest || this.isAdPlaying) {
196
+ return;
197
+ }
198
+
199
+ this.pendingAdRequest = true;
200
+ this.lastAdTime = this.video.currentTime;
201
+
202
+ console.log('🎬 Triggering periodic ad break...');
203
+
204
+ // Pause video if configured
205
+ if (this.config.pauseStreamDuringAd && !this.video.paused) {
206
+ this.video.pause();
207
+ }
208
+
209
+ // Request a new ad
210
+ this.requestAds();
211
+ }
212
+
149
213
  /**
150
214
  * Request ads
151
215
  */
@@ -245,6 +309,9 @@ export class GoogleAdsManager {
245
309
 
246
310
  // Start ads
247
311
  this.adsManager.start();
312
+
313
+ // Setup periodic ad scheduling if enabled
314
+ this.setupPeriodicAds();
248
315
  } catch (error) {
249
316
  console.error('Error starting ads:', error);
250
317
  this.video.play().catch(() => { });
@@ -263,6 +330,7 @@ export class GoogleAdsManager {
263
330
  () => {
264
331
  console.log('Ad: Content paused');
265
332
  this.isAdPlaying = true;
333
+ this.pendingAdRequest = false; // Reset pending flag
266
334
  this.video.pause();
267
335
 
268
336
  // Force ad container visibility and z-index
@@ -289,14 +357,6 @@ export class GoogleAdsManager {
289
357
  // Store cleanup function to remove listener later
290
358
  (this.video as any).__adPlayBlocker = preventPlayDuringAd;
291
359
 
292
- // ✅ Safety timeout: Force hide ad container after 120 seconds if ad gets stuck
293
- this.adSafetyTimeout = setTimeout(() => {
294
- if (this.isAdPlaying) {
295
- console.warn('⚠️ Ad safety timeout triggered - forcing ad container to hide');
296
- this.forceHideAdContainer();
297
- }
298
- }, 120000); // 120 seconds = 2 minutes max ad length
299
-
300
360
  this.config.onAdStart?.();
301
361
  }
302
362
  );
@@ -308,12 +368,6 @@ export class GoogleAdsManager {
308
368
  console.log('Ad: Content resume');
309
369
  this.isAdPlaying = false;
310
370
 
311
- // ✅ Clear safety timeout
312
- if (this.adSafetyTimeout) {
313
- clearTimeout(this.adSafetyTimeout);
314
- this.adSafetyTimeout = null;
315
- }
316
-
317
371
  // Remove play blocker
318
372
  const preventPlayDuringAd = (this.video as any).__adPlayBlocker;
319
373
  if (preventPlayDuringAd) {
@@ -321,18 +375,6 @@ export class GoogleAdsManager {
321
375
  delete (this.video as any).__adPlayBlocker;
322
376
  }
323
377
 
324
- // ✅ Hide ad container to show video again
325
- if (this.adContainer) {
326
- this.adContainer.style.visibility = 'hidden';
327
- this.adContainer.style.opacity = '0';
328
- this.adContainer.style.pointerEvents = 'none';
329
- this.adContainer.style.zIndex = '1';
330
- console.log('✅ Ad container hidden - video visible again');
331
- }
332
-
333
- // ✅ Ensure video element is visible and playing
334
- this.restoreVideoVisibility();
335
-
336
378
  this.config.onAdEnd?.();
337
379
  this.video.play().catch(() => { });
338
380
  }
@@ -420,29 +462,11 @@ export class GoogleAdsManager {
420
462
 
421
463
  this.config.onAdError?.(error);
422
464
 
423
- // ✅ Clear safety timeout
424
- if (this.adSafetyTimeout) {
425
- clearTimeout(this.adSafetyTimeout);
426
- this.adSafetyTimeout = null;
427
- }
428
-
429
465
  // Destroy ads manager on error
430
466
  if (this.adsManager) {
431
467
  this.adsManager.destroy();
432
468
  }
433
469
 
434
- // ✅ Hide ad container on error
435
- if (this.adContainer) {
436
- this.adContainer.style.visibility = 'hidden';
437
- this.adContainer.style.opacity = '0';
438
- this.adContainer.style.pointerEvents = 'none';
439
- this.adContainer.style.zIndex = '1';
440
- console.log('✅ Ad container hidden after error - video visible again');
441
- }
442
-
443
- // ✅ Restore video visibility
444
- this.restoreVideoVisibility();
445
-
446
470
  // Resume content playback
447
471
  this.isAdPlaying = false;
448
472
  this.video.play().catch(() => { });
@@ -692,99 +716,19 @@ export class GoogleAdsManager {
692
716
  }
693
717
  }
694
718
 
695
- /**
696
- * Restore video element visibility
697
- */
698
- private restoreVideoVisibility(): void {
699
- console.log('🔍 Restoring video visibility...');
700
-
701
- // Check current video state
702
- console.log('Video element state:', {
703
- paused: this.video.paused,
704
- currentTime: this.video.currentTime,
705
- readyState: this.video.readyState,
706
- networkState: this.video.networkState,
707
- videoWidth: this.video.videoWidth,
708
- videoHeight: this.video.videoHeight,
709
- display: getComputedStyle(this.video).display,
710
- visibility: getComputedStyle(this.video).visibility,
711
- opacity: getComputedStyle(this.video).opacity
712
- });
713
-
714
- // Force video element to be visible
715
- this.video.style.display = 'block';
716
- this.video.style.visibility = 'visible';
717
- this.video.style.opacity = '1';
718
- this.video.style.zIndex = '1';
719
-
720
- // Ensure video is not hidden by any parent
721
- let parent = this.video.parentElement;
722
- while (parent && parent !== document.body) {
723
- const styles = getComputedStyle(parent);
724
- if (styles.display === 'none' || styles.visibility === 'hidden' || styles.opacity === '0') {
725
- console.warn('⚠️ Parent element is hidden:', parent.className);
726
- }
727
- parent = parent.parentElement;
728
- }
729
-
730
- console.log('✅ Video element visibility restored');
731
- }
732
-
733
- /**
734
- * Force hide ad container (safety mechanism)
735
- */
736
- private forceHideAdContainer(): void {
737
- console.warn('🚨 Force hiding ad container and resuming video');
738
-
739
- this.isAdPlaying = false;
740
-
741
- // Clear safety timeout
742
- if (this.adSafetyTimeout) {
743
- clearTimeout(this.adSafetyTimeout);
744
- this.adSafetyTimeout = null;
745
- }
746
-
747
- // Remove play blocker
748
- const preventPlayDuringAd = (this.video as any).__adPlayBlocker;
749
- if (preventPlayDuringAd) {
750
- this.video.removeEventListener('play', preventPlayDuringAd);
751
- delete (this.video as any).__adPlayBlocker;
752
- }
753
-
754
- // Hide ad container
755
- if (this.adContainer) {
756
- this.adContainer.style.visibility = 'hidden';
757
- this.adContainer.style.opacity = '0';
758
- this.adContainer.style.pointerEvents = 'none';
759
- this.adContainer.style.zIndex = '1';
760
- console.log('✅ Ad container force-hidden');
761
- }
762
-
763
- // Destroy stuck ads manager
764
- if (this.adsManager) {
765
- try {
766
- this.adsManager.destroy();
767
- } catch (e) {
768
- console.warn('Failed to destroy ads manager:', e);
769
- }
770
- }
771
-
772
- // ✅ Restore video visibility
773
- this.restoreVideoVisibility();
774
-
775
- // Resume video
776
- this.video.play().catch(() => { });
777
-
778
- // Notify callback
779
- this.config.onAdEnd?.();
780
- }
781
-
782
719
  /**
783
720
  * Cleanup
784
721
  */
785
722
  destroy(): void {
786
723
  this.hideUnmuteButton();
787
724
 
725
+ // Clear periodic ad interval
726
+ if (this.periodicAdCheckInterval) {
727
+ clearInterval(this.periodicAdCheckInterval);
728
+ this.periodicAdCheckInterval = null;
729
+ console.log('✅ Periodic ad scheduling stopped');
730
+ }
731
+
788
732
  if (this.adsManager) {
789
733
  this.adsManager.destroy();
790
734
  this.adsManager = null;
@@ -796,5 +740,6 @@ export class GoogleAdsManager {
796
740
  }
797
741
 
798
742
  this.isAdPlaying = false;
743
+ this.pendingAdRequest = false;
799
744
  }
800
745
  }
@@ -4,7 +4,6 @@ import type { CSSProperties } from 'react';
4
4
  import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from '../../core/dist';
5
5
  import { WebPlayer } from '../WebPlayer';
6
6
  import { GoogleAdsManager } from '../ads/GoogleAdsManager';
7
- import { LiveStreamAdsManager } from '../ads/LiveStreamAdsManager';
8
7
  // EPG imports - conditionally loaded
9
8
  import type { EPGData, EPGConfig, EPGProgram, EPGProgramRow } from './types/EPGTypes';
10
9
  import type { VCManifest, VCProduct, VCEvent } from './types/VideoCommerceTypes';
@@ -407,34 +406,24 @@ export type WebPlayerViewProps = {
407
406
  // Google Ads Configuration
408
407
  googleAds?: {
409
408
  adTagUrl: string; // VAST/VMAP ad tag URL
410
- midrollTimes?: number[]; // Mid-roll ad times in seconds [30, 60, 120] (VOD only)
409
+ midrollTimes?: number[]; // Mid-roll ad times in seconds [30, 60, 120]
410
+
411
+ // Periodic ad breaks (for live streams or automatic scheduling)
412
+ liveAdBreakMode?: 'periodic' | 'manual'; // 'periodic' = auto-schedule at intervals, 'manual' = use midrollTimes
413
+ periodicAdInterval?: number; // Interval in seconds between ads (e.g., 30 = ad every 30 seconds)
414
+ syncToLiveEdge?: boolean; // For live streams: sync back to live edge after ad
415
+ pauseStreamDuringAd?: boolean; // Pause underlying stream during ad playback
416
+ liveEdgeOffset?: number; // Seconds behind live edge to resume at (default: 3)
417
+
411
418
  companionAdSlots?: Array<{ // Companion ad containers
412
419
  containerId: string;
413
420
  width: number;
414
421
  height: number;
415
422
  }>;
416
-
417
- // Live Stream Ad Options (NEW)
418
- liveAdBreakMode?: 'metadata' | 'periodic' | 'manual' | 'hybrid'; // Live ad insertion mode
419
- periodicAdInterval?: number; // For periodic mode: ad every X seconds (default: 300)
420
- syncToLiveEdge?: boolean; // Jump to live edge after ad (default: false - DVR style)
421
- liveEdgeOffset?: number; // Seconds behind live edge (default: 3)
422
- pauseStreamDuringAd?: boolean; // Pause stream download during ad (default: true)
423
-
424
- // Metadata Detection Config (for 'metadata' mode)
425
- metadataConfig?: {
426
- detectDateRange?: boolean; // Detect #EXT-X-DATERANGE tags (default: true)
427
- detectID3?: boolean; // Detect ID3 timed metadata (default: true)
428
- adClassNames?: string[]; // CLASS values to detect (default: ['com.google.ads', 'ads'])
429
- };
430
-
431
- // Callbacks
432
423
  onAdStart?: () => void; // Called when ad starts
433
424
  onAdEnd?: () => void; // Called when ad ends
434
425
  onAdError?: (error: any) => void; // Called on ad error
435
426
  onAllAdsComplete?: () => void; // Called when all ads complete
436
- onLiveAdBreakDetected?: (metadata: any) => void; // Called when live ad cue detected (metadata mode)
437
- onAdBreakScheduled?: (scheduledTime: number) => void; // Called when ad scheduled (periodic mode)
438
427
  };
439
428
 
440
429
  // Chapter Event Callbacks
@@ -1244,25 +1233,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1244
1233
 
1245
1234
  // Initialize Google Ads if configured
1246
1235
  if (props.googleAds) {
1247
- // Wait for ad container to be properly mounted in DOM
1248
- const waitForAdContainer = () => {
1249
- return new Promise<void>((resolve) => {
1250
- const checkContainer = () => {
1251
- if (adContainerRef.current && document.body.contains(adContainerRef.current)) {
1252
- resolve();
1253
- } else {
1254
- setTimeout(checkContainer, 50);
1255
- }
1256
- };
1257
- checkContainer();
1258
- });
1259
- };
1260
-
1236
+ // Small delay to ensure ad container is properly mounted in DOM
1261
1237
  setTimeout(async () => {
1262
1238
  try {
1263
- // Wait for ad container to be in DOM
1264
- await waitForAdContainer();
1265
-
1266
1239
  // Ensure ad container exists
1267
1240
  if (!adContainerRef.current) {
1268
1241
  console.error('❌ Ad container ref is null - cannot initialize ads');
@@ -1305,122 +1278,22 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1305
1278
  return true;
1306
1279
  };
1307
1280
 
1308
- // Determine if this is a live stream with live ad modes
1309
- const isLiveAdMode = props.googleAds.liveAdBreakMode &&
1310
- props.googleAds.liveAdBreakMode !== 'manual';
1311
-
1312
- // Use LiveStreamAdsManager for live ad modes, otherwise use GoogleAdsManager
1313
- const adsManager = isLiveAdMode
1314
- ? new LiveStreamAdsManager(
1315
- videoElement,
1316
- adContainer,
1317
- {
1318
- adTagUrl: props.googleAds.adTagUrl,
1319
- midrollTimes: props.googleAds.midrollTimes,
1320
- companionAdSlots: props.googleAds.companionAdSlots,
1321
-
1322
- // Live stream specific config
1323
- liveAdBreakMode: props.googleAds.liveAdBreakMode,
1324
- periodicAdInterval: props.googleAds.periodicAdInterval,
1325
- syncToLiveEdge: props.googleAds.syncToLiveEdge,
1326
- liveEdgeOffset: props.googleAds.liveEdgeOffset,
1327
- pauseStreamDuringAd: props.googleAds.pauseStreamDuringAd,
1328
- metadataConfig: props.googleAds.metadataConfig,
1329
-
1330
- onAdStart: () => {
1331
- setIsAdPlaying(true);
1332
-
1333
- // Check if this is a pre-roll ad (video time near 0)
1334
- if (videoElement.currentTime < 1) {
1335
- console.log('🎬 Pre-roll ad starting - blocking video playback');
1336
- isPrerollPlaying = true;
1337
- hasPrerollAd = true;
1338
-
1339
- // Add playback blocker
1340
- videoElement.addEventListener('play', blockVideoUntilPreroll);
1341
- }
1342
-
1343
- // Notify player to block keyboard controls
1344
- if (typeof (player as any).setAdPlaying === 'function') {
1345
- (player as any).setAdPlaying(true);
1346
- }
1347
- props.googleAds?.onAdStart?.();
1348
- },
1349
- onLiveAdBreakDetected: props.googleAds.onLiveAdBreakDetected,
1350
- onAdBreakScheduled: props.googleAds.onAdBreakScheduled,
1351
- onAdEnd: () => {
1352
- setIsAdPlaying(false);
1353
-
1354
- // Handle pre-roll completion
1355
- if (isPrerollPlaying) {
1356
- console.log('🎬 Pre-roll ad completed');
1357
- isPrerollPlaying = false;
1358
-
1359
- // Remove playback blocker
1360
- videoElement.removeEventListener('play', blockVideoUntilPreroll);
1361
-
1362
- // Reset video to beginning if it leaked any playback
1363
- if (videoElement.currentTime > 0 && videoElement.currentTime < 10) {
1364
- console.log(`⏮️ Resetting video to 0:00 (was at ${videoElement.currentTime.toFixed(2)}s)`);
1365
- videoElement.currentTime = 0;
1366
- }
1367
- }
1368
-
1369
- // Notify player to unblock keyboard controls
1370
- if (typeof (player as any).setAdPlaying === 'function') {
1371
- (player as any).setAdPlaying(false);
1372
- }
1373
- props.googleAds?.onAdEnd?.();
1374
- },
1375
- onAdError: (error) => {
1376
- setIsAdPlaying(false);
1377
- isPrerollPlaying = false;
1378
-
1379
- // Remove blocker on error
1380
- videoElement.removeEventListener('play', blockVideoUntilPreroll);
1381
-
1382
- // Notify player to unblock keyboard controls
1383
- if (typeof (player as any).setAdPlaying === 'function') {
1384
- (player as any).setAdPlaying(false);
1385
- }
1386
- props.googleAds?.onAdError?.(error);
1387
- },
1388
- onAllAdsComplete: () => {
1389
- setIsAdPlaying(false);
1390
- isPrerollPlaying = false;
1391
-
1392
- // Remove blocker when all ads complete
1393
- videoElement.removeEventListener('play', blockVideoUntilPreroll);
1394
-
1395
- // Notify player to unblock keyboard controls
1396
- if (typeof (player as any).setAdPlaying === 'function') {
1397
- (player as any).setAdPlaying(false);
1398
- }
1399
- props.googleAds?.onAllAdsComplete?.();
1400
- },
1401
- onAdCuePoints: (cuePoints: number[]) => {
1402
- // Check if there's a pre-roll (cue point at 0)
1403
- if (cuePoints.includes(0)) {
1404
- console.log('✅ Pre-roll ad detected in cue points');
1405
- hasPrerollAd = true;
1406
- }
1407
-
1408
- // Inject markers from VMAP cue points (if midrollTimes not provided)
1409
- if (!props.googleAds?.midrollTimes || props.googleAds.midrollTimes.length === 0) {
1410
- console.log('🔵 Using VMAP cue points for ad markers:', cuePoints);
1411
- injectAdMarkersFromTimes(cuePoints);
1412
- }
1413
- },
1414
- }
1415
- )
1416
- : new GoogleAdsManager(
1417
- videoElement,
1418
- adContainer,
1419
- {
1420
- adTagUrl: props.googleAds.adTagUrl,
1421
- midrollTimes: props.googleAds.midrollTimes,
1422
- companionAdSlots: props.googleAds.companionAdSlots,
1423
- onAdStart: () => {
1281
+ const adsManager = new GoogleAdsManager(
1282
+ videoElement,
1283
+ adContainer,
1284
+ {
1285
+ adTagUrl: props.googleAds.adTagUrl,
1286
+ midrollTimes: props.googleAds.midrollTimes,
1287
+ companionAdSlots: props.googleAds.companionAdSlots,
1288
+
1289
+ // Periodic ad configuration
1290
+ liveAdBreakMode: props.googleAds.liveAdBreakMode,
1291
+ periodicAdInterval: props.googleAds.periodicAdInterval,
1292
+ syncToLiveEdge: props.googleAds.syncToLiveEdge,
1293
+ pauseStreamDuringAd: props.googleAds.pauseStreamDuringAd,
1294
+ liveEdgeOffset: props.googleAds.liveEdgeOffset,
1295
+
1296
+ onAdStart: () => {
1424
1297
  setIsAdPlaying(true);
1425
1298
 
1426
1299
  // Check if this is a pre-roll ad (video time near 0)
@@ -1504,21 +1377,12 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1504
1377
  },
1505
1378
  }
1506
1379
  );
1507
-
1508
- // Initialize ads manager (different for live vs VOD)
1509
- if (isLiveAdMode) {
1510
- // For LiveStreamAdsManager, pass HLS instance for metadata detection
1511
- const hlsInstance = (player as any).hls || (player as any).getHlsInstance?.();
1512
- await (adsManager as LiveStreamAdsManager).initializeLiveAds(hlsInstance);
1513
- console.log('✅ Live Stream Ads initialized successfully');
1514
- } else {
1515
- // For regular GoogleAdsManager
1516
- await adsManager.initialize();
1517
- console.log('✅ Google Ads initialized successfully');
1518
- }
1519
-
1380
+
1381
+ await adsManager.initialize();
1520
1382
  adsManagerRef.current = adsManager;
1521
1383
 
1384
+ console.log('✅ Google Ads initialized successfully');
1385
+
1522
1386
  // Move ad container into player wrapper for fullscreen support
1523
1387
  const playerWrapper = containerRef.current?.querySelector('.uvf-player-wrapper');
1524
1388
  if (playerWrapper && adContainerRef.current && adContainerRef.current.parentElement) {