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.
- package/package.json +1 -1
- package/packages/web/dist/ads/GoogleAdsManager.d.ts +13 -6
- package/packages/web/dist/ads/GoogleAdsManager.d.ts.map +1 -1
- package/packages/web/dist/ads/GoogleAdsManager.js +41 -89
- package/packages/web/dist/ads/GoogleAdsManager.js.map +1 -1
- package/packages/web/dist/ads/LiveStreamAdsManager.d.ts +4 -25
- package/packages/web/dist/ads/LiveStreamAdsManager.d.ts.map +1 -1
- package/packages/web/dist/ads/LiveStreamAdsManager.js +33 -224
- package/packages/web/dist/ads/LiveStreamAdsManager.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +5 -12
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +66 -156
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/src/ads/GoogleAdsManager.ts +80 -135
- package/packages/web/src/react/WebPlayerView.tsx +30 -166
- package/packages/web/src/ads/LiveStreamAdsManager.ts +0 -625
- package/packages/web/src/react/examples/live-stream-ads-example.tsx +0 -362
|
@@ -1,625 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live Stream Ads Manager
|
|
3
|
-
*
|
|
4
|
-
* Extends GoogleAdsManager to support live streaming ad insertion
|
|
5
|
-
*
|
|
6
|
-
* Supported Modes:
|
|
7
|
-
* 1. Metadata-based: Detects #EXT-X-DATERANGE or ID3 tags in HLS stream
|
|
8
|
-
* 2. Periodic: Shows ads every X seconds of playback
|
|
9
|
-
* 3. Manual: Programmatic ad break triggering
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { GoogleAdsManager, GoogleAdsConfig } from './GoogleAdsManager';
|
|
13
|
-
|
|
14
|
-
// HLS.js event types (if using HLS.js)
|
|
15
|
-
declare const Hls: any;
|
|
16
|
-
|
|
17
|
-
export type LiveAdBreakMode = 'metadata' | 'periodic' | 'manual' | 'hybrid';
|
|
18
|
-
|
|
19
|
-
export interface LiveStreamAdsConfig extends GoogleAdsConfig {
|
|
20
|
-
// Live stream specific options
|
|
21
|
-
liveAdBreakMode?: LiveAdBreakMode;
|
|
22
|
-
|
|
23
|
-
// For 'periodic' mode: Insert ads every X seconds of playback
|
|
24
|
-
periodicAdInterval?: number; // Default: 300 (5 minutes)
|
|
25
|
-
|
|
26
|
-
// For 'metadata' mode: Configure metadata detection
|
|
27
|
-
metadataConfig?: {
|
|
28
|
-
detectDateRange?: boolean; // Detect #EXT-X-DATERANGE tags (default: true)
|
|
29
|
-
detectID3?: boolean; // Detect ID3 timed metadata (default: true)
|
|
30
|
-
adClassNames?: string[]; // CLASS values to detect (default: ['com.google.ads', 'ads'])
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Advanced options
|
|
34
|
-
syncToLiveEdge?: boolean; // Jump to live edge after ad (default: false for DVR-style)
|
|
35
|
-
liveEdgeOffset?: number; // Seconds behind live edge (default: 3)
|
|
36
|
-
pauseStreamDuringAd?: boolean; // Pause stream during ad (default: true - DVR-style)
|
|
37
|
-
|
|
38
|
-
// Callbacks
|
|
39
|
-
onLiveAdBreakDetected?: (metadata: any) => void;
|
|
40
|
-
onAdBreakScheduled?: (scheduledTime: number) => void;
|
|
41
|
-
onLiveEdgeSync?: (newPosition: number) => void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class LiveStreamAdsManager extends GoogleAdsManager {
|
|
45
|
-
private liveConfig: LiveStreamAdsConfig;
|
|
46
|
-
|
|
47
|
-
// Periodic mode state
|
|
48
|
-
private periodicAdTimer: NodeJS.Timeout | null = null;
|
|
49
|
-
private lastAdBreakTime: number = 0;
|
|
50
|
-
private playbackStartTime: number = 0;
|
|
51
|
-
|
|
52
|
-
// Metadata mode state
|
|
53
|
-
private hlsInstance: any = null;
|
|
54
|
-
private processedCuePoints: Set<string> = new Set();
|
|
55
|
-
private metadataTrack: TextTrack | null = null;
|
|
56
|
-
|
|
57
|
-
// Live stream state
|
|
58
|
-
private isLiveStream: boolean = false;
|
|
59
|
-
private streamStartTime: number = 0;
|
|
60
|
-
|
|
61
|
-
constructor(video: HTMLVideoElement, adContainer: HTMLElement, config: LiveStreamAdsConfig) {
|
|
62
|
-
super(video, adContainer, config);
|
|
63
|
-
this.liveConfig = {
|
|
64
|
-
liveAdBreakMode: 'metadata',
|
|
65
|
-
periodicAdInterval: 300, // Default: 5 minutes
|
|
66
|
-
syncToLiveEdge: false, // Default: DVR-style (user sees all content)
|
|
67
|
-
liveEdgeOffset: 3,
|
|
68
|
-
pauseStreamDuringAd: true, // Default: Pause stream during ad (DVR-style)
|
|
69
|
-
metadataConfig: {
|
|
70
|
-
detectDateRange: true,
|
|
71
|
-
detectID3: true,
|
|
72
|
-
adClassNames: ['com.google.ads', 'ads', 'ad-break']
|
|
73
|
-
},
|
|
74
|
-
...config
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Initialize live stream ads with specified mode
|
|
80
|
-
*/
|
|
81
|
-
async initializeLiveAds(hlsInstance?: any): Promise<void> {
|
|
82
|
-
// Initialize parent (Google IMA SDK)
|
|
83
|
-
await this.initialize();
|
|
84
|
-
|
|
85
|
-
this.isLiveStream = true;
|
|
86
|
-
this.hlsInstance = hlsInstance;
|
|
87
|
-
this.playbackStartTime = this.video.currentTime || 0;
|
|
88
|
-
this.streamStartTime = Date.now();
|
|
89
|
-
|
|
90
|
-
const mode = this.liveConfig.liveAdBreakMode || 'metadata';
|
|
91
|
-
|
|
92
|
-
console.log(`🔴 Live Stream Ads initialized`);
|
|
93
|
-
console.log(` Mode: ${mode}`);
|
|
94
|
-
console.log(` Stream start time: ${this.playbackStartTime}s`);
|
|
95
|
-
|
|
96
|
-
// Setup based on mode
|
|
97
|
-
switch (mode) {
|
|
98
|
-
case 'metadata':
|
|
99
|
-
this.setupMetadataListener();
|
|
100
|
-
break;
|
|
101
|
-
|
|
102
|
-
case 'periodic':
|
|
103
|
-
this.setupPeriodicAdBreaks();
|
|
104
|
-
break;
|
|
105
|
-
|
|
106
|
-
case 'hybrid':
|
|
107
|
-
// Both metadata and periodic
|
|
108
|
-
this.setupMetadataListener();
|
|
109
|
-
this.setupPeriodicAdBreaks();
|
|
110
|
-
break;
|
|
111
|
-
|
|
112
|
-
case 'manual':
|
|
113
|
-
console.log('Manual mode - use triggerAdBreak() to show ads');
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* ========================================================================
|
|
120
|
-
* OPTION 2: METADATA-BASED AD INSERTION
|
|
121
|
-
* ========================================================================
|
|
122
|
-
* Detects #EXT-X-DATERANGE tags or ID3 metadata in HLS stream
|
|
123
|
-
*/
|
|
124
|
-
private setupMetadataListener(): void {
|
|
125
|
-
console.log('📡 Setting up metadata listener for live ad cues');
|
|
126
|
-
|
|
127
|
-
const config = this.liveConfig.metadataConfig!;
|
|
128
|
-
|
|
129
|
-
// Method 1: HLS.js metadata events
|
|
130
|
-
if (this.hlsInstance && typeof Hls !== 'undefined') {
|
|
131
|
-
this.setupHlsJsMetadata();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Method 2: Native HLS (Safari, iOS) - TextTrack metadata
|
|
135
|
-
if (config.detectID3) {
|
|
136
|
-
this.setupNativeMetadata();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Setup HLS.js metadata detection
|
|
142
|
-
*/
|
|
143
|
-
private setupHlsJsMetadata(): void {
|
|
144
|
-
if (!this.hlsInstance || typeof Hls === 'undefined') {
|
|
145
|
-
console.warn('HLS.js not available for metadata detection');
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
console.log('✅ Setting up HLS.js metadata listeners');
|
|
150
|
-
|
|
151
|
-
// Listen for fragment parsing metadata (ID3 tags)
|
|
152
|
-
this.hlsInstance.on(Hls.Events.FRAG_PARSING_METADATA, (event: string, data: any) => {
|
|
153
|
-
console.log('📥 HLS metadata event:', data);
|
|
154
|
-
this.handleHlsMetadata(data);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Listen for level updated (manifest changes with DATERANGE)
|
|
158
|
-
this.hlsInstance.on(Hls.Events.LEVEL_UPDATED, (event: string, data: any) => {
|
|
159
|
-
if (data.details && data.details.dateRanges) {
|
|
160
|
-
console.log('📅 DATERANGE tags detected:', data.details.dateRanges);
|
|
161
|
-
this.handleDateRanges(data.details.dateRanges);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Setup native HLS metadata detection (Safari, iOS)
|
|
168
|
-
*/
|
|
169
|
-
private setupNativeMetadata(): void {
|
|
170
|
-
// Wait for text tracks to be available
|
|
171
|
-
const checkForMetadataTrack = () => {
|
|
172
|
-
if (!this.video.textTracks) return;
|
|
173
|
-
|
|
174
|
-
// Find metadata track
|
|
175
|
-
for (let i = 0; i < this.video.textTracks.length; i++) {
|
|
176
|
-
const track = this.video.textTracks[i];
|
|
177
|
-
if (track.kind === 'metadata') {
|
|
178
|
-
this.metadataTrack = track;
|
|
179
|
-
console.log('✅ Found native metadata track');
|
|
180
|
-
|
|
181
|
-
// Enable the track
|
|
182
|
-
track.mode = 'hidden';
|
|
183
|
-
|
|
184
|
-
// Listen for cue changes
|
|
185
|
-
track.addEventListener('cuechange', () => {
|
|
186
|
-
this.handleNativeMetadataCues(track);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
// Check now and on loadedmetadata
|
|
195
|
-
checkForMetadataTrack();
|
|
196
|
-
this.video.addEventListener('loadedmetadata', checkForMetadataTrack);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Handle HLS.js metadata (ID3 tags)
|
|
201
|
-
*/
|
|
202
|
-
private handleHlsMetadata(data: any): void {
|
|
203
|
-
if (!data.samples || data.samples.length === 0) return;
|
|
204
|
-
|
|
205
|
-
data.samples.forEach((sample: any) => {
|
|
206
|
-
// Check if this is an ad-related metadata
|
|
207
|
-
const isAdCue = this.isAdMetadata(sample);
|
|
208
|
-
|
|
209
|
-
if (isAdCue) {
|
|
210
|
-
const cueId = this.generateCueId(sample);
|
|
211
|
-
|
|
212
|
-
if (!this.processedCuePoints.has(cueId)) {
|
|
213
|
-
console.log('✅ Ad cue detected from HLS metadata:', sample);
|
|
214
|
-
this.processedCuePoints.add(cueId);
|
|
215
|
-
|
|
216
|
-
// Notify callback
|
|
217
|
-
this.liveConfig.onLiveAdBreakDetected?.(sample);
|
|
218
|
-
|
|
219
|
-
// Trigger ad break
|
|
220
|
-
this.triggerAdBreak();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Handle #EXT-X-DATERANGE tags
|
|
228
|
-
*/
|
|
229
|
-
private handleDateRanges(dateRanges: any): void {
|
|
230
|
-
Object.values(dateRanges).forEach((dateRange: any) => {
|
|
231
|
-
const className = dateRange.class || dateRange.CLASS;
|
|
232
|
-
const adClassNames = this.liveConfig.metadataConfig?.adClassNames || [];
|
|
233
|
-
|
|
234
|
-
// Check if this DATERANGE is for ads
|
|
235
|
-
const isAdCue = adClassNames.some(name => className?.includes(name));
|
|
236
|
-
|
|
237
|
-
if (isAdCue) {
|
|
238
|
-
const cueId = dateRange.id || dateRange.ID;
|
|
239
|
-
|
|
240
|
-
if (cueId && !this.processedCuePoints.has(cueId)) {
|
|
241
|
-
console.log('✅ Ad cue detected from DATERANGE:', dateRange);
|
|
242
|
-
this.processedCuePoints.add(cueId);
|
|
243
|
-
|
|
244
|
-
// Notify callback
|
|
245
|
-
this.liveConfig.onLiveAdBreakDetected?.(dateRange);
|
|
246
|
-
|
|
247
|
-
// Trigger ad break
|
|
248
|
-
this.triggerAdBreak();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Handle native metadata cues (Safari, iOS)
|
|
256
|
-
*/
|
|
257
|
-
private handleNativeMetadataCues(track: TextTrack): void {
|
|
258
|
-
if (!track.activeCues || track.activeCues.length === 0) return;
|
|
259
|
-
|
|
260
|
-
for (let i = 0; i < track.activeCues.length; i++) {
|
|
261
|
-
const cue: any = track.activeCues[i];
|
|
262
|
-
|
|
263
|
-
// Check if this cue is ad-related
|
|
264
|
-
const isAdCue = this.isAdMetadata(cue);
|
|
265
|
-
|
|
266
|
-
if (isAdCue) {
|
|
267
|
-
const cueId = this.generateCueId(cue);
|
|
268
|
-
|
|
269
|
-
if (!this.processedCuePoints.has(cueId)) {
|
|
270
|
-
console.log('✅ Ad cue detected from native metadata:', cue);
|
|
271
|
-
this.processedCuePoints.add(cueId);
|
|
272
|
-
|
|
273
|
-
// Notify callback
|
|
274
|
-
this.liveConfig.onLiveAdBreakDetected?.(cue);
|
|
275
|
-
|
|
276
|
-
// Trigger ad break
|
|
277
|
-
this.triggerAdBreak();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Check if metadata indicates an ad break
|
|
285
|
-
*/
|
|
286
|
-
private isAdMetadata(metadata: any): boolean {
|
|
287
|
-
const adClassNames = this.liveConfig.metadataConfig?.adClassNames || [];
|
|
288
|
-
|
|
289
|
-
// Check various metadata formats
|
|
290
|
-
const className = metadata.class || metadata.CLASS || metadata.value?.class;
|
|
291
|
-
const key = metadata.key;
|
|
292
|
-
const data = metadata.data || metadata.value?.data;
|
|
293
|
-
|
|
294
|
-
// Check if class name matches ad indicators
|
|
295
|
-
if (className) {
|
|
296
|
-
return adClassNames.some(name => className.includes(name));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Check ID3 TXXX frame for ad indicators
|
|
300
|
-
if (key === 'TXXX' && data) {
|
|
301
|
-
return adClassNames.some(name => data.toLowerCase().includes(name.toLowerCase()));
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Generate unique ID for cue point
|
|
309
|
-
*/
|
|
310
|
-
private generateCueId(metadata: any): string {
|
|
311
|
-
return metadata.id ||
|
|
312
|
-
metadata.ID ||
|
|
313
|
-
metadata.startTime?.toString() ||
|
|
314
|
-
JSON.stringify(metadata);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* ========================================================================
|
|
319
|
-
* OPTION 3: PERIODIC AD INSERTION
|
|
320
|
-
* ========================================================================
|
|
321
|
-
* Shows ads every X seconds of actual playback time
|
|
322
|
-
*/
|
|
323
|
-
private setupPeriodicAdBreaks(): void {
|
|
324
|
-
const interval = this.liveConfig.periodicAdInterval || 300;
|
|
325
|
-
|
|
326
|
-
console.log(`⏰ Setting up periodic ad breaks`);
|
|
327
|
-
console.log(` Interval: ${interval} seconds (${interval / 60} minutes)`);
|
|
328
|
-
|
|
329
|
-
// Clear existing timer
|
|
330
|
-
if (this.periodicAdTimer) {
|
|
331
|
-
clearInterval(this.periodicAdTimer);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Track actual playback time (not wall-clock time)
|
|
335
|
-
let lastCheckTime = this.video.currentTime || 0;
|
|
336
|
-
let accumulatedPlaybackTime = 0;
|
|
337
|
-
|
|
338
|
-
// Check every second
|
|
339
|
-
this.periodicAdTimer = setInterval(() => {
|
|
340
|
-
// Only count time when video is actually playing
|
|
341
|
-
if (!this.video.paused && !this.isPlayingAd()) {
|
|
342
|
-
const currentTime = this.video.currentTime || 0;
|
|
343
|
-
const timeDelta = currentTime - lastCheckTime;
|
|
344
|
-
|
|
345
|
-
// Accumulate playback time (handle seeks)
|
|
346
|
-
if (timeDelta > 0 && timeDelta < 5) { // Reasonable time delta
|
|
347
|
-
accumulatedPlaybackTime += timeDelta;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
lastCheckTime = currentTime;
|
|
351
|
-
|
|
352
|
-
// Check if it's time for an ad break
|
|
353
|
-
const timeSinceLastAd = accumulatedPlaybackTime - this.lastAdBreakTime;
|
|
354
|
-
|
|
355
|
-
if (timeSinceLastAd >= interval) {
|
|
356
|
-
console.log(`⏰ Periodic ad break triggered`);
|
|
357
|
-
console.log(` Playback time: ${accumulatedPlaybackTime.toFixed(0)}s`);
|
|
358
|
-
console.log(` Time since last ad: ${timeSinceLastAd.toFixed(0)}s`);
|
|
359
|
-
|
|
360
|
-
// Notify callback
|
|
361
|
-
this.liveConfig.onAdBreakScheduled?.(accumulatedPlaybackTime);
|
|
362
|
-
|
|
363
|
-
// Trigger ad break
|
|
364
|
-
this.triggerAdBreak();
|
|
365
|
-
|
|
366
|
-
// Update last ad break time
|
|
367
|
-
this.lastAdBreakTime = accumulatedPlaybackTime;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}, 1000);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* ========================================================================
|
|
375
|
-
* AD BREAK TRIGGERING
|
|
376
|
-
* ========================================================================
|
|
377
|
-
*/
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Trigger ad break for live stream
|
|
381
|
-
*/
|
|
382
|
-
public triggerAdBreak(): void {
|
|
383
|
-
console.log('🎬 Triggering ad break for live stream');
|
|
384
|
-
|
|
385
|
-
// Don't interrupt if ad is already playing
|
|
386
|
-
if (this.isPlayingAd()) {
|
|
387
|
-
console.warn('⚠️ Ad already playing, skipping trigger');
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Store current time for DVR-style resume
|
|
392
|
-
const timeBeforeAd = this.video.currentTime;
|
|
393
|
-
|
|
394
|
-
console.log(`📍 Stream position before ad: ${timeBeforeAd.toFixed(2)}s`);
|
|
395
|
-
|
|
396
|
-
// Pause stream downloading during ad (saves bandwidth)
|
|
397
|
-
if (this.liveConfig.pauseStreamDuringAd) {
|
|
398
|
-
this.pauseStreamDownload();
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Initialize ad display container
|
|
402
|
-
try {
|
|
403
|
-
this.initAdDisplayContainer();
|
|
404
|
-
} catch (e) {
|
|
405
|
-
// Already initialized
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Request ads from Google IMA
|
|
409
|
-
this.requestAds();
|
|
410
|
-
|
|
411
|
-
// Setup post-ad resume handler
|
|
412
|
-
this.setupLiveEdgeResume(timeBeforeAd);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Pause stream downloading during ad (saves bandwidth)
|
|
417
|
-
*/
|
|
418
|
-
private pauseStreamDownload(): void {
|
|
419
|
-
try {
|
|
420
|
-
if (this.hlsInstance && this.hlsInstance.stopLoad) {
|
|
421
|
-
console.log('⏸️ Pausing stream download during ad');
|
|
422
|
-
this.hlsInstance.stopLoad();
|
|
423
|
-
} else if ((this.video as any).dashPlayer) {
|
|
424
|
-
const dashPlayer = (this.video as any).dashPlayer;
|
|
425
|
-
if (dashPlayer.pause) {
|
|
426
|
-
dashPlayer.pause();
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
} catch (error) {
|
|
430
|
-
console.warn('Could not pause stream download:', error);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Resume stream downloading after ad
|
|
436
|
-
*/
|
|
437
|
-
private resumeStreamDownload(): void {
|
|
438
|
-
try {
|
|
439
|
-
if (this.hlsInstance && this.hlsInstance.startLoad) {
|
|
440
|
-
console.log('▶️ Resuming stream download after ad');
|
|
441
|
-
this.hlsInstance.startLoad();
|
|
442
|
-
} else if ((this.video as any).dashPlayer) {
|
|
443
|
-
const dashPlayer = (this.video as any).dashPlayer;
|
|
444
|
-
if (dashPlayer.play) {
|
|
445
|
-
dashPlayer.play();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
} catch (error) {
|
|
449
|
-
console.warn('Could not resume stream download:', error);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Setup live edge resume after ad
|
|
455
|
-
*/
|
|
456
|
-
private setupLiveEdgeResume(timeBeforeAd: number): void {
|
|
457
|
-
// Listen for ad end
|
|
458
|
-
const handleAdEnd = () => {
|
|
459
|
-
if (this.liveConfig.syncToLiveEdge) {
|
|
460
|
-
// Mode 1: Jump to live edge (user skips content during ad)
|
|
461
|
-
console.log('📺 Ad ended, syncing to live edge (skipping content)');
|
|
462
|
-
this.syncToLiveEdge();
|
|
463
|
-
} else {
|
|
464
|
-
// Mode 2: DVR-style resume from pause point (user sees all content)
|
|
465
|
-
console.log('📺 Ad ended, resuming from pause point (DVR-style)');
|
|
466
|
-
this.resumeFromPausePoint(timeBeforeAd);
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
// Use one-time listener
|
|
471
|
-
const originalOnAdEnd = this.config.onAdEnd;
|
|
472
|
-
this.config.onAdEnd = () => {
|
|
473
|
-
handleAdEnd();
|
|
474
|
-
originalOnAdEnd?.();
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Resume from where stream was paused (DVR-style)
|
|
480
|
-
*/
|
|
481
|
-
private resumeFromPausePoint(pausedTime: number): void {
|
|
482
|
-
setTimeout(() => {
|
|
483
|
-
try {
|
|
484
|
-
console.log(`⏯️ Resuming from paused position: ${pausedTime.toFixed(2)}s`);
|
|
485
|
-
console.log(` User is now behind live, but will see all content`);
|
|
486
|
-
|
|
487
|
-
// Resume stream downloading first
|
|
488
|
-
if (this.liveConfig.pauseStreamDuringAd) {
|
|
489
|
-
this.resumeStreamDownload();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Resume from exact pause point
|
|
493
|
-
this.video.currentTime = pausedTime;
|
|
494
|
-
|
|
495
|
-
// Notify callback
|
|
496
|
-
this.liveConfig.onLiveEdgeSync?.(pausedTime);
|
|
497
|
-
|
|
498
|
-
// Calculate how far behind live user is
|
|
499
|
-
if (this.hlsInstance && this.hlsInstance.liveSyncPosition !== undefined) {
|
|
500
|
-
const liveEdge = this.hlsInstance.liveSyncPosition;
|
|
501
|
-
const behindLive = liveEdge - pausedTime;
|
|
502
|
-
console.log(` Behind live by: ${behindLive.toFixed(1)} seconds`);
|
|
503
|
-
}
|
|
504
|
-
} catch (error) {
|
|
505
|
-
console.error('❌ Error resuming from pause point:', error);
|
|
506
|
-
}
|
|
507
|
-
}, 100); // Small delay to ensure ad container is hidden
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Sync video to live edge after ad
|
|
512
|
-
*/
|
|
513
|
-
private syncToLiveEdge(): void {
|
|
514
|
-
setTimeout(() => {
|
|
515
|
-
try {
|
|
516
|
-
// Resume stream downloading first
|
|
517
|
-
if (this.liveConfig.pauseStreamDuringAd) {
|
|
518
|
-
this.resumeStreamDownload();
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
let liveEdgePosition: number | null = null;
|
|
522
|
-
|
|
523
|
-
// Get live edge from HLS.js
|
|
524
|
-
if (this.hlsInstance && this.hlsInstance.liveSyncPosition !== undefined) {
|
|
525
|
-
liveEdgePosition = this.hlsInstance.liveSyncPosition;
|
|
526
|
-
console.log('📍 HLS.js live edge:', liveEdgePosition);
|
|
527
|
-
}
|
|
528
|
-
// Get live edge from DASH.js
|
|
529
|
-
else if ((this.video as any).dashPlayer) {
|
|
530
|
-
const dashPlayer = (this.video as any).dashPlayer;
|
|
531
|
-
if (dashPlayer.getDVRSeekOffset) {
|
|
532
|
-
liveEdgePosition = dashPlayer.getDVRSeekOffset(0);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
// Fallback: estimate from duration and seekable
|
|
536
|
-
else if (this.video.seekable && this.video.seekable.length > 0) {
|
|
537
|
-
liveEdgePosition = this.video.seekable.end(this.video.seekable.length - 1);
|
|
538
|
-
console.log('📍 Native seekable live edge:', liveEdgePosition);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Apply live edge offset
|
|
542
|
-
if (liveEdgePosition !== null) {
|
|
543
|
-
const offset = this.liveConfig.liveEdgeOffset || 3;
|
|
544
|
-
const targetPosition = Math.max(0, liveEdgePosition - offset);
|
|
545
|
-
|
|
546
|
-
console.log(`⏩ Syncing to live edge: ${targetPosition.toFixed(2)}s (offset: ${offset}s)`);
|
|
547
|
-
|
|
548
|
-
this.video.currentTime = targetPosition;
|
|
549
|
-
|
|
550
|
-
// Notify callback
|
|
551
|
-
this.liveConfig.onLiveEdgeSync?.(targetPosition);
|
|
552
|
-
} else {
|
|
553
|
-
console.warn('⚠️ Could not determine live edge position');
|
|
554
|
-
}
|
|
555
|
-
} catch (error) {
|
|
556
|
-
console.error('❌ Error syncing to live edge:', error);
|
|
557
|
-
}
|
|
558
|
-
}, 100); // Small delay to ensure ad container is hidden
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* ========================================================================
|
|
563
|
-
* UTILITY METHODS
|
|
564
|
-
* ========================================================================
|
|
565
|
-
*/
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Reset periodic ad timer
|
|
569
|
-
*/
|
|
570
|
-
public resetPeriodicTimer(): void {
|
|
571
|
-
this.lastAdBreakTime = 0;
|
|
572
|
-
console.log('🔄 Periodic ad timer reset');
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Clear processed cue points (for testing)
|
|
577
|
-
*/
|
|
578
|
-
public clearProcessedCues(): void {
|
|
579
|
-
this.processedCuePoints.clear();
|
|
580
|
-
console.log('🔄 Processed cue points cleared');
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Update live ad configuration at runtime
|
|
585
|
-
*/
|
|
586
|
-
public updateLiveConfig(config: Partial<LiveStreamAdsConfig>): void {
|
|
587
|
-
Object.assign(this.liveConfig, config);
|
|
588
|
-
console.log('🔄 Live ad config updated:', config);
|
|
589
|
-
|
|
590
|
-
// Re-initialize if mode changed
|
|
591
|
-
if (config.liveAdBreakMode && config.liveAdBreakMode !== this.liveConfig.liveAdBreakMode) {
|
|
592
|
-
this.destroy();
|
|
593
|
-
this.initializeLiveAds(this.hlsInstance);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Clean up live stream specific resources
|
|
599
|
-
*/
|
|
600
|
-
destroy(): void {
|
|
601
|
-
// Clear periodic timer
|
|
602
|
-
if (this.periodicAdTimer) {
|
|
603
|
-
clearInterval(this.periodicAdTimer);
|
|
604
|
-
this.periodicAdTimer = null;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Clear metadata listeners
|
|
608
|
-
if (this.hlsInstance && typeof Hls !== 'undefined') {
|
|
609
|
-
this.hlsInstance.off(Hls.Events.FRAG_PARSING_METADATA);
|
|
610
|
-
this.hlsInstance.off(Hls.Events.LEVEL_UPDATED);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (this.metadataTrack) {
|
|
614
|
-
this.metadataTrack.removeEventListener('cuechange', () => {});
|
|
615
|
-
this.metadataTrack = null;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Clear state
|
|
619
|
-
this.processedCuePoints.clear();
|
|
620
|
-
this.hlsInstance = null;
|
|
621
|
-
|
|
622
|
-
// Call parent destroy
|
|
623
|
-
super.destroy();
|
|
624
|
-
}
|
|
625
|
-
}
|