unified-video-framework 1.4.403 → 1.4.405

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.
@@ -414,27 +414,19 @@ export type WebPlayerViewProps = {
414
414
  height: number;
415
415
  }>;
416
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
- };
417
+ // Live Stream Options
418
+ liveAdBreakMode?: 'periodic' | 'manual'; // Live stream ad mode
419
+ periodicAdInterval?: number; // Seconds between ads (default: 300)
420
+ syncToLiveEdge?: boolean; // Jump to live edge after ad (default: false)
421
+ liveEdgeOffset?: number; // Seconds behind live edge (default: 3)
422
+ pauseStreamDuringAd?: boolean; // Pause stream during ad (default: true)
430
423
 
431
424
  // Callbacks
432
425
  onAdStart?: () => void; // Called when ad starts
433
426
  onAdEnd?: () => void; // Called when ad ends
434
427
  onAdError?: (error: any) => void; // Called on ad error
435
428
  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)
429
+ onAdBreakScheduled?: (scheduledTime: number) => void; // Called when ad is scheduled (periodic mode)
438
430
  };
439
431
 
440
432
  // Chapter Event Callbacks
@@ -1244,25 +1236,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1244
1236
 
1245
1237
  // Initialize Google Ads if configured
1246
1238
  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
-
1239
+ // Small delay to ensure ad container is properly mounted in DOM
1261
1240
  setTimeout(async () => {
1262
1241
  try {
1263
- // Wait for ad container to be in DOM
1264
- await waitForAdContainer();
1265
-
1266
1242
  // Ensure ad container exists
1267
1243
  if (!adContainerRef.current) {
1268
1244
  console.error('❌ Ad container ref is null - cannot initialize ads');
@@ -1316,57 +1292,38 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1316
1292
  adContainer,
1317
1293
  {
1318
1294
  adTagUrl: props.googleAds.adTagUrl,
1319
- midrollTimes: props.googleAds.midrollTimes,
1320
- companionAdSlots: props.googleAds.companionAdSlots,
1321
-
1322
- // Live stream specific config
1323
1295
  liveAdBreakMode: props.googleAds.liveAdBreakMode,
1324
1296
  periodicAdInterval: props.googleAds.periodicAdInterval,
1325
1297
  syncToLiveEdge: props.googleAds.syncToLiveEdge,
1326
1298
  liveEdgeOffset: props.googleAds.liveEdgeOffset,
1327
1299
  pauseStreamDuringAd: props.googleAds.pauseStreamDuringAd,
1328
- metadataConfig: props.googleAds.metadataConfig,
1329
-
1330
1300
  onAdStart: () => {
1331
1301
  setIsAdPlaying(true);
1332
1302
 
1333
- // Check if this is a pre-roll ad (video time near 0)
1303
+ // Check if this is a pre-roll ad
1334
1304
  if (videoElement.currentTime < 1) {
1335
- console.log('🎬 Pre-roll ad starting - blocking video playback');
1305
+ console.log('🎬 Pre-roll ad starting');
1336
1306
  isPrerollPlaying = true;
1337
1307
  hasPrerollAd = true;
1338
-
1339
- // Add playback blocker
1340
1308
  videoElement.addEventListener('play', blockVideoUntilPreroll);
1341
1309
  }
1342
1310
 
1343
- // Notify player to block keyboard controls
1344
1311
  if (typeof (player as any).setAdPlaying === 'function') {
1345
1312
  (player as any).setAdPlaying(true);
1346
1313
  }
1347
1314
  props.googleAds?.onAdStart?.();
1348
1315
  },
1349
- onLiveAdBreakDetected: props.googleAds.onLiveAdBreakDetected,
1350
1316
  onAdBreakScheduled: props.googleAds.onAdBreakScheduled,
1351
1317
  onAdEnd: () => {
1352
1318
  setIsAdPlaying(false);
1353
1319
 
1354
- // Handle pre-roll completion
1355
1320
  if (isPrerollPlaying) {
1356
- console.log('🎬 Pre-roll ad completed');
1321
+ console.log(' Pre-roll ad completed - unblocking video');
1357
1322
  isPrerollPlaying = false;
1358
-
1359
- // Remove playback blocker
1360
1323
  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
- }
1324
+ videoElement.play().catch(() => {});
1367
1325
  }
1368
1326
 
1369
- // Notify player to unblock keyboard controls
1370
1327
  if (typeof (player as any).setAdPlaying === 'function') {
1371
1328
  (player as any).setAdPlaying(false);
1372
1329
  }
@@ -1375,11 +1332,7 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1375
1332
  onAdError: (error) => {
1376
1333
  setIsAdPlaying(false);
1377
1334
  isPrerollPlaying = false;
1378
-
1379
- // Remove blocker on error
1380
1335
  videoElement.removeEventListener('play', blockVideoUntilPreroll);
1381
-
1382
- // Notify player to unblock keyboard controls
1383
1336
  if (typeof (player as any).setAdPlaying === 'function') {
1384
1337
  (player as any).setAdPlaying(false);
1385
1338
  }
@@ -1388,29 +1341,12 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1388
1341
  onAllAdsComplete: () => {
1389
1342
  setIsAdPlaying(false);
1390
1343
  isPrerollPlaying = false;
1391
-
1392
- // Remove blocker when all ads complete
1393
1344
  videoElement.removeEventListener('play', blockVideoUntilPreroll);
1394
-
1395
- // Notify player to unblock keyboard controls
1396
1345
  if (typeof (player as any).setAdPlaying === 'function') {
1397
1346
  (player as any).setAdPlaying(false);
1398
1347
  }
1399
1348
  props.googleAds?.onAllAdsComplete?.();
1400
1349
  },
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
1350
  }
1415
1351
  )
1416
1352
  : new GoogleAdsManager(
@@ -1507,7 +1443,7 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1507
1443
 
1508
1444
  // Initialize ads manager (different for live vs VOD)
1509
1445
  if (isLiveAdMode) {
1510
- // For LiveStreamAdsManager, pass HLS instance for metadata detection
1446
+ // For LiveStreamAdsManager, pass HLS instance for live edge detection
1511
1447
  const hlsInstance = (player as any).hls || (player as any).getHlsInstance?.();
1512
1448
  await (adsManager as LiveStreamAdsManager).initializeLiveAds(hlsInstance);
1513
1449
  console.log('✅ Live Stream Ads initialized successfully');
@@ -1,362 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { WebPlayerView } from '../WebPlayerView';
3
-
4
- /**
5
- * Example: Live Stream Ads with Metadata and Periodic Modes
6
- *
7
- * This example demonstrates how to integrate ads with live streaming using:
8
- * 1. Metadata-based ad insertion (detects cues in HLS stream)
9
- * 2. Periodic timer-based ad insertion (every X seconds)
10
- * 3. Hybrid mode (both metadata and periodic)
11
- */
12
-
13
- export const LiveStreamAdsExample: React.FC = () => {
14
- const [adEvents, setAdEvents] = useState<string[]>([]);
15
- const [mode, setMode] = useState<'metadata' | 'periodic' | 'hybrid'>('periodic');
16
-
17
- const logAdEvent = (event: string) => {
18
- console.log(`[Ad Event] ${event}`);
19
- setAdEvents(prev => [...prev, `${new Date().toLocaleTimeString()}: ${event}`].slice(-10)); // Keep last 10
20
- };
21
-
22
- return (
23
- <div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
24
- {/* Mode Selector */}
25
- <div style={{ padding: '20px', backgroundColor: '#f5f5f5', borderBottom: '1px solid #ddd' }}>
26
- <h2 style={{ margin: '0 0 15px 0' }}>Live Stream Ads - Example</h2>
27
-
28
- <div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
29
- <button
30
- onClick={() => setMode('metadata')}
31
- style={{
32
- padding: '10px 20px',
33
- backgroundColor: mode === 'metadata' ? '#007bff' : '#6c757d',
34
- color: 'white',
35
- border: 'none',
36
- borderRadius: '4px',
37
- cursor: 'pointer'
38
- }}
39
- >
40
- Metadata Mode
41
- </button>
42
-
43
- <button
44
- onClick={() => setMode('periodic')}
45
- style={{
46
- padding: '10px 20px',
47
- backgroundColor: mode === 'periodic' ? '#007bff' : '#6c757d',
48
- color: 'white',
49
- border: 'none',
50
- borderRadius: '4px',
51
- cursor: 'pointer'
52
- }}
53
- >
54
- Periodic Mode (Every 2 min)
55
- </button>
56
-
57
- <button
58
- onClick={() => setMode('hybrid')}
59
- style={{
60
- padding: '10px 20px',
61
- backgroundColor: mode === 'hybrid' ? '#007bff' : '#6c757d',
62
- color: 'white',
63
- border: 'none',
64
- borderRadius: '4px',
65
- cursor: 'pointer'
66
- }}
67
- >
68
- Hybrid Mode
69
- </button>
70
- </div>
71
-
72
- <div style={{ fontSize: '14px', color: '#666' }}>
73
- <strong>Current Mode:</strong> {mode}
74
- {mode === 'metadata' && ' - Detects #EXT-X-DATERANGE tags in HLS stream'}
75
- {mode === 'periodic' && ' - Shows ads every 2 minutes of playback'}
76
- {mode === 'hybrid' && ' - Uses both metadata detection AND periodic fallback'}
77
- </div>
78
- </div>
79
-
80
- {/* Video Player */}
81
- <div style={{ flex: 1, position: 'relative', backgroundColor: '#000' }}>
82
- <WebPlayerView
83
- // Live HLS stream
84
- url="https://stream.example.com/live.m3u8"
85
- type="hls"
86
-
87
- // Player config
88
- autoPlay={false}
89
- controls={true}
90
- muted={true} // Start muted for autoplay compatibility
91
-
92
- // Google Ads for Live Streaming
93
- googleAds={{
94
- // VAST ad tag URL
95
- adTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=',
96
-
97
- // ========================================
98
- // LIVE STREAM AD CONFIGURATION
99
- // ========================================
100
-
101
- // Choose ad insertion mode
102
- liveAdBreakMode: mode,
103
-
104
- // For periodic mode: ad every X seconds (default: 300 = 5 minutes)
105
- periodicAdInterval: 120, // 2 minutes for demo
106
-
107
- // Sync to live edge after ad (recommended for live streams)
108
- syncToLiveEdge: true,
109
- liveEdgeOffset: 3, // Stay 3 seconds behind live edge
110
-
111
- // Metadata detection config (for metadata/hybrid modes)
112
- metadataConfig: {
113
- detectDateRange: true, // Detect #EXT-X-DATERANGE tags
114
- detectID3: true, // Detect ID3 timed metadata
115
- adClassNames: ['com.google.ads', 'ads', 'ad-break']
116
- },
117
-
118
- // ========================================
119
- // AD EVENT CALLBACKS
120
- // ========================================
121
-
122
- onAdStart: () => {
123
- logAdEvent(`🎬 Ad Started (Mode: ${mode})`);
124
- },
125
-
126
- onAdEnd: () => {
127
- logAdEvent('✅ Ad Ended - Resuming Live Stream');
128
- },
129
-
130
- onAdError: (error: any) => {
131
- logAdEvent(`❌ Ad Error: ${error?.getMessage?.() || error}`);
132
- },
133
-
134
- onAllAdsComplete: () => {
135
- logAdEvent('✅ All Ads in Pod Completed');
136
- },
137
-
138
- // Live-specific callbacks
139
- onLiveAdBreakDetected: (metadata) => {
140
- logAdEvent(`📍 Live Ad Cue Detected: ${JSON.stringify(metadata).slice(0, 50)}...`);
141
- },
142
-
143
- onAdBreakScheduled: (scheduledTime) => {
144
- logAdEvent(`⏰ Ad Break Scheduled at ${scheduledTime.toFixed(0)}s playback time`);
145
- },
146
- }}
147
-
148
- // Player callbacks
149
- onReady={(player) => {
150
- console.log('Player ready with live stream ads');
151
- logAdEvent('🎥 Player Ready');
152
- }}
153
-
154
- onError={(error) => {
155
- console.error('Player error:', error);
156
- logAdEvent(`❌ Player Error: ${error instanceof Error ? error.message : String(error)}`);
157
- }}
158
-
159
- onTimeUpdate={(time) => {
160
- // Optional: Track playback time
161
- }}
162
- />
163
- </div>
164
-
165
- {/* Ad Event Log */}
166
- <div style={{
167
- padding: '20px',
168
- backgroundColor: '#f9f9f9',
169
- borderTop: '1px solid #ddd',
170
- maxHeight: '200px',
171
- overflowY: 'auto',
172
- }}>
173
- <h3 style={{ margin: '0 0 10px 0', fontSize: '16px' }}>Live Ad Events Log:</h3>
174
- {adEvents.length === 0 ? (
175
- <p style={{ color: '#999', margin: 0 }}>No ad events yet. Play the video to see ads.</p>
176
- ) : (
177
- <ul style={{ margin: 0, padding: '0 0 0 20px' }}>
178
- {adEvents.map((event, index) => (
179
- <li key={index} style={{ fontSize: '14px', marginBottom: '5px' }}>
180
- {event}
181
- </li>
182
- ))}
183
- </ul>
184
- )}
185
- </div>
186
-
187
- {/* Implementation Notes */}
188
- <div style={{
189
- padding: '20px',
190
- backgroundColor: '#e7f3ff',
191
- borderTop: '1px solid #b3d9ff'
192
- }}>
193
- <h3 style={{ margin: '0 0 10px 0', fontSize: '16px' }}>💡 Implementation Notes:</h3>
194
- <ul style={{ margin: 0, paddingLeft: '20px', fontSize: '14px' }}>
195
- <li>
196
- <strong>Metadata Mode:</strong> Requires HLS stream with #EXT-X-DATERANGE tags or ID3 metadata.
197
- Your encoder must embed ad cues in the manifest.
198
- </li>
199
- <li>
200
- <strong>Periodic Mode:</strong> Works with any live stream. Ads trigger every X seconds of actual playback.
201
- Simple but users see ads at different times (not synchronized).
202
- </li>
203
- <li>
204
- <strong>Hybrid Mode:</strong> Uses metadata when available, falls back to periodic timer.
205
- Best of both worlds!
206
- </li>
207
- <li>
208
- <strong>Live Edge Sync:</strong> After ads, player jumps to live edge (catches up to "now").
209
- Disable if you want DVR-style behavior (continue from where paused).
210
- </li>
211
- </ul>
212
- </div>
213
- </div>
214
- );
215
- };
216
-
217
- /**
218
- * ========================================================================
219
- * USAGE EXAMPLE 1: METADATA-BASED LIVE ADS
220
- * ========================================================================
221
- */
222
- export const MetadataLiveAdsExample: React.FC = () => {
223
- return (
224
- <WebPlayerView
225
- url="https://stream.example.com/live.m3u8"
226
- type="hls"
227
-
228
- googleAds={{
229
- adTagUrl: 'https://pubads.g.doubleclick.net/...',
230
-
231
- // Use metadata mode
232
- liveAdBreakMode: 'metadata',
233
-
234
- // Metadata detection config
235
- metadataConfig: {
236
- detectDateRange: true,
237
- detectID3: true,
238
- adClassNames: ['com.google.ads', 'ads']
239
- },
240
-
241
- // Sync to live edge after ads
242
- syncToLiveEdge: true,
243
- liveEdgeOffset: 3,
244
-
245
- onLiveAdBreakDetected: (metadata) => {
246
- console.log('Ad cue detected:', metadata);
247
- }
248
- }}
249
- />
250
- );
251
- };
252
-
253
- /**
254
- * ========================================================================
255
- * USAGE EXAMPLE 2: PERIODIC TIMER-BASED LIVE ADS
256
- * ========================================================================
257
- */
258
- export const PeriodicLiveAdsExample: React.FC = () => {
259
- return (
260
- <WebPlayerView
261
- url="https://stream.example.com/live.m3u8"
262
- type="hls"
263
-
264
- googleAds={{
265
- adTagUrl: 'https://pubads.g.doubleclick.net/...',
266
-
267
- // Use periodic mode
268
- liveAdBreakMode: 'periodic',
269
-
270
- // Ad every 5 minutes (300 seconds)
271
- periodicAdInterval: 300,
272
-
273
- // Sync to live edge after ads
274
- syncToLiveEdge: true,
275
-
276
- onAdBreakScheduled: (scheduledTime) => {
277
- console.log(`Next ad at ${scheduledTime}s playback time`);
278
- }
279
- }}
280
- />
281
- );
282
- };
283
-
284
- /**
285
- * ========================================================================
286
- * USAGE EXAMPLE 3: HYBRID MODE (METADATA + PERIODIC FALLBACK)
287
- * ========================================================================
288
- */
289
- export const HybridLiveAdsExample: React.FC = () => {
290
- return (
291
- <WebPlayerView
292
- url="https://stream.example.com/live.m3u8"
293
- type="hls"
294
-
295
- googleAds={{
296
- adTagUrl: 'https://pubads.g.doubleclick.net/...',
297
-
298
- // Use hybrid mode
299
- liveAdBreakMode: 'hybrid',
300
-
301
- // Metadata detection (primary)
302
- metadataConfig: {
303
- detectDateRange: true,
304
- detectID3: true
305
- },
306
-
307
- // Periodic fallback (if no metadata detected)
308
- periodicAdInterval: 300,
309
-
310
- // Sync to live edge
311
- syncToLiveEdge: true,
312
-
313
- // Callbacks for both modes
314
- onLiveAdBreakDetected: (metadata) => {
315
- console.log('✅ Metadata ad cue detected');
316
- },
317
-
318
- onAdBreakScheduled: (scheduledTime) => {
319
- console.log('⏰ Fallback periodic ad scheduled');
320
- }
321
- }}
322
- />
323
- );
324
- };
325
-
326
- /**
327
- * ========================================================================
328
- * BACKEND-CONTROLLED EXAMPLE
329
- * ========================================================================
330
- * Fetch ad schedule from backend and configure player accordingly
331
- */
332
- export const BackendControlledLiveAdsExample: React.FC = () => {
333
- const [adConfig, setAdConfig] = useState<any>(null);
334
-
335
- // Fetch ad configuration from backend
336
- React.useEffect(() => {
337
- fetch('/api/live-stream/ad-config')
338
- .then(res => res.json())
339
- .then(config => {
340
- setAdConfig(config);
341
- });
342
- }, []);
343
-
344
- if (!adConfig) return <div>Loading...</div>;
345
-
346
- return (
347
- <WebPlayerView
348
- url={adConfig.streamUrl}
349
- type="hls"
350
-
351
- googleAds={{
352
- adTagUrl: adConfig.adTagUrl,
353
- liveAdBreakMode: adConfig.mode, // Backend decides: metadata | periodic | hybrid
354
- periodicAdInterval: adConfig.periodicInterval,
355
- metadataConfig: adConfig.metadataConfig,
356
- syncToLiveEdge: true
357
- }}
358
- />
359
- );
360
- };
361
-
362
- export default LiveStreamAdsExample;