unified-video-framework 1.4.399 → 1.4.401
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/package.json +1 -1
- package/packages/web/dist/ads/GoogleAdsManager.d.ts +3 -3
- package/packages/web/dist/ads/GoogleAdsManager.d.ts.map +1 -1
- package/packages/web/dist/ads/LiveStreamAdsManager.d.ts +50 -0
- package/packages/web/dist/ads/LiveStreamAdsManager.d.ts.map +1 -0
- package/packages/web/dist/ads/LiveStreamAdsManager.js +359 -0
- package/packages/web/dist/ads/LiveStreamAdsManager.js.map +1 -0
- package/packages/web/dist/react/WebPlayerView.d.ts +12 -0
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +156 -61
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/dist/react/examples/live-stream-ads-example.d.ts +8 -0
- package/packages/web/dist/react/examples/live-stream-ads-example.d.ts.map +1 -0
- package/packages/web/dist/react/examples/live-stream-ads-example.js +177 -0
- package/packages/web/dist/react/examples/live-stream-ads-example.js.map +1 -0
- package/packages/web/src/ads/GoogleAdsManager.ts +3 -3
- package/packages/web/src/ads/LiveStreamAdsManager.ts +625 -0
- package/packages/web/src/react/WebPlayerView.tsx +166 -14
- package/packages/web/src/react/examples/live-stream-ads-example.tsx +362 -0
|
@@ -4,6 +4,7 @@ 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';
|
|
7
8
|
// EPG imports - conditionally loaded
|
|
8
9
|
import type { EPGData, EPGConfig, EPGProgram, EPGProgramRow } from './types/EPGTypes';
|
|
9
10
|
import type { VCManifest, VCProduct, VCEvent } from './types/VideoCommerceTypes';
|
|
@@ -406,16 +407,34 @@ export type WebPlayerViewProps = {
|
|
|
406
407
|
// Google Ads Configuration
|
|
407
408
|
googleAds?: {
|
|
408
409
|
adTagUrl: string; // VAST/VMAP ad tag URL
|
|
409
|
-
midrollTimes?: number[]; // Mid-roll ad times in seconds [30, 60, 120]
|
|
410
|
+
midrollTimes?: number[]; // Mid-roll ad times in seconds [30, 60, 120] (VOD only)
|
|
410
411
|
companionAdSlots?: Array<{ // Companion ad containers
|
|
411
412
|
containerId: string;
|
|
412
413
|
width: number;
|
|
413
414
|
height: number;
|
|
414
415
|
}>;
|
|
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
|
|
415
432
|
onAdStart?: () => void; // Called when ad starts
|
|
416
433
|
onAdEnd?: () => void; // Called when ad ends
|
|
417
434
|
onAdError?: (error: any) => void; // Called on ad error
|
|
418
435
|
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)
|
|
419
438
|
};
|
|
420
439
|
|
|
421
440
|
// Chapter Event Callbacks
|
|
@@ -1225,9 +1244,25 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1225
1244
|
|
|
1226
1245
|
// Initialize Google Ads if configured
|
|
1227
1246
|
if (props.googleAds) {
|
|
1228
|
-
//
|
|
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
|
+
|
|
1229
1261
|
setTimeout(async () => {
|
|
1230
1262
|
try {
|
|
1263
|
+
// Wait for ad container to be in DOM
|
|
1264
|
+
await waitForAdContainer();
|
|
1265
|
+
|
|
1231
1266
|
// Ensure ad container exists
|
|
1232
1267
|
if (!adContainerRef.current) {
|
|
1233
1268
|
console.error('❌ Ad container ref is null - cannot initialize ads');
|
|
@@ -1270,14 +1305,122 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1270
1305
|
return true;
|
|
1271
1306
|
};
|
|
1272
1307
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
1424
|
setIsAdPlaying(true);
|
|
1282
1425
|
|
|
1283
1426
|
// Check if this is a pre-roll ad (video time near 0)
|
|
@@ -1361,11 +1504,20 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1361
1504
|
},
|
|
1362
1505
|
}
|
|
1363
1506
|
);
|
|
1364
|
-
|
|
1365
|
-
await adsManager.initialize();
|
|
1366
|
-
adsManagerRef.current = adsManager;
|
|
1367
1507
|
|
|
1368
|
-
|
|
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
|
+
|
|
1520
|
+
adsManagerRef.current = adsManager;
|
|
1369
1521
|
|
|
1370
1522
|
// Move ad container into player wrapper for fullscreen support
|
|
1371
1523
|
const playerWrapper = containerRef.current?.querySelector('.uvf-player-wrapper');
|
|
@@ -0,0 +1,362 @@
|
|
|
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;
|