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.
- package/package.json +1 -1
- package/packages/web/dist/ads/GoogleAdsManager.d.ts +3 -2
- package/packages/web/dist/ads/GoogleAdsManager.d.ts.map +1 -1
- package/packages/web/dist/ads/GoogleAdsManager.js +13 -3
- 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 +1 -7
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +3 -34
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/src/ads/GoogleAdsManager.ts +34 -11
- package/packages/web/src/ads/LiveStreamAdsManager.ts +71 -441
- package/packages/web/src/react/WebPlayerView.tsx +13 -77
- package/packages/web/src/react/examples/live-stream-ads-example.tsx +0 -362
|
@@ -1,82 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
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
|
|
3
|
+
* Extends GoogleAdsManager with periodic ad break support for live streams
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
6
|
import { GoogleAdsManager, GoogleAdsConfig } from './GoogleAdsManager';
|
|
13
7
|
|
|
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
8
|
export interface LiveStreamAdsConfig extends GoogleAdsConfig {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
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)
|
|
9
|
+
liveAdBreakMode?: 'periodic' | 'manual';
|
|
10
|
+
periodicAdInterval?: number; // Seconds between ads (default: 300)
|
|
11
|
+
syncToLiveEdge?: boolean; // Jump to live edge after ad (default: false)
|
|
12
|
+
liveEdgeOffset?: number; // Seconds behind live edge (default: 3)
|
|
13
|
+
pauseStreamDuringAd?: boolean; // Pause stream download during ads (default: true)
|
|
37
14
|
|
|
38
15
|
// Callbacks
|
|
39
|
-
onLiveAdBreakDetected?: (metadata: any) => void;
|
|
40
16
|
onAdBreakScheduled?: (scheduledTime: number) => void;
|
|
41
|
-
onLiveEdgeSync?: (newPosition: number) => void;
|
|
42
17
|
}
|
|
43
18
|
|
|
44
19
|
export class LiveStreamAdsManager extends GoogleAdsManager {
|
|
45
20
|
private liveConfig: LiveStreamAdsConfig;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
private
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
// Metadata mode state
|
|
21
|
+
private isLiveStream = false;
|
|
22
|
+
private periodicAdTimer: any = null;
|
|
23
|
+
private lastAdBreakTime = 0;
|
|
24
|
+
private playbackStartTime = 0;
|
|
25
|
+
private streamStartTime = 0;
|
|
53
26
|
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
27
|
|
|
61
28
|
constructor(video: HTMLVideoElement, adContainer: HTMLElement, config: LiveStreamAdsConfig) {
|
|
62
29
|
super(video, adContainer, config);
|
|
63
30
|
this.liveConfig = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
syncToLiveEdge: false, // Default: DVR-style (user sees all content)
|
|
31
|
+
periodicAdInterval: 300,
|
|
32
|
+
syncToLiveEdge: false,
|
|
67
33
|
liveEdgeOffset: 3,
|
|
68
|
-
pauseStreamDuringAd: true,
|
|
69
|
-
metadataConfig: {
|
|
70
|
-
detectDateRange: true,
|
|
71
|
-
detectID3: true,
|
|
72
|
-
adClassNames: ['com.google.ads', 'ads', 'ad-break']
|
|
73
|
-
},
|
|
34
|
+
pauseStreamDuringAd: true,
|
|
74
35
|
...config
|
|
75
36
|
};
|
|
76
37
|
}
|
|
77
38
|
|
|
78
39
|
/**
|
|
79
|
-
* Initialize live stream ads with
|
|
40
|
+
* Initialize live stream ads with periodic mode
|
|
80
41
|
*/
|
|
81
42
|
async initializeLiveAds(hlsInstance?: any): Promise<void> {
|
|
82
43
|
// Initialize parent (Google IMA SDK)
|
|
@@ -87,344 +48,95 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
|
|
|
87
48
|
this.playbackStartTime = this.video.currentTime || 0;
|
|
88
49
|
this.streamStartTime = Date.now();
|
|
89
50
|
|
|
90
|
-
const mode = this.liveConfig.liveAdBreakMode || '
|
|
51
|
+
const mode = this.liveConfig.liveAdBreakMode || 'periodic';
|
|
91
52
|
|
|
92
53
|
console.log(`🔴 Live Stream Ads initialized`);
|
|
93
54
|
console.log(` Mode: ${mode}`);
|
|
94
|
-
console.log(`
|
|
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!;
|
|
55
|
+
console.log(` Periodic interval: ${this.liveConfig.periodicAdInterval}s`);
|
|
128
56
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
57
|
+
if (mode === 'periodic') {
|
|
58
|
+
this.setupPeriodicAdBreaks();
|
|
59
|
+
} else {
|
|
60
|
+
console.log('Manual mode - call triggerAdBreak() to show ads');
|
|
132
61
|
}
|
|
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
62
|
}
|
|
225
63
|
|
|
226
64
|
/**
|
|
227
|
-
*
|
|
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
|
|
65
|
+
* Setup periodic timer-based ad breaks
|
|
322
66
|
*/
|
|
323
67
|
private setupPeriodicAdBreaks(): void {
|
|
324
68
|
const interval = this.liveConfig.periodicAdInterval || 300;
|
|
69
|
+
console.log(`⏰ Setting up periodic ad breaks every ${interval}s`);
|
|
325
70
|
|
|
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
71
|
let accumulatedPlaybackTime = 0;
|
|
72
|
+
let lastCheckTime = this.video.currentTime;
|
|
337
73
|
|
|
338
|
-
// Check every second
|
|
339
74
|
this.periodicAdTimer = setInterval(() => {
|
|
340
|
-
// Only count time when video is actually playing
|
|
341
75
|
if (!this.video.paused && !this.isPlayingAd()) {
|
|
342
76
|
const currentTime = this.video.currentTime || 0;
|
|
343
77
|
const timeDelta = currentTime - lastCheckTime;
|
|
344
78
|
|
|
345
|
-
//
|
|
346
|
-
if (timeDelta > 0 && timeDelta < 5) {
|
|
79
|
+
// Only accumulate if delta is reasonable (< 5 seconds)
|
|
80
|
+
if (timeDelta > 0 && timeDelta < 5) {
|
|
347
81
|
accumulatedPlaybackTime += timeDelta;
|
|
348
82
|
}
|
|
349
83
|
|
|
350
84
|
lastCheckTime = currentTime;
|
|
351
85
|
|
|
352
|
-
// Check if it's time for an ad break
|
|
353
86
|
const timeSinceLastAd = accumulatedPlaybackTime - this.lastAdBreakTime;
|
|
354
87
|
|
|
355
88
|
if (timeSinceLastAd >= interval) {
|
|
356
|
-
console.log(
|
|
357
|
-
console.log(` Playback time: ${
|
|
358
|
-
console.log(` Time since last ad: ${
|
|
89
|
+
console.log('⏰ Periodic ad break triggered');
|
|
90
|
+
console.log(` Playback time: ${Math.floor(accumulatedPlaybackTime)}s`);
|
|
91
|
+
console.log(` Time since last ad: ${Math.floor(timeSinceLastAd)}s`);
|
|
359
92
|
|
|
360
|
-
// Notify callback
|
|
361
93
|
this.liveConfig.onAdBreakScheduled?.(accumulatedPlaybackTime);
|
|
362
|
-
|
|
363
|
-
// Trigger ad break
|
|
364
94
|
this.triggerAdBreak();
|
|
365
|
-
|
|
366
|
-
// Update last ad break time
|
|
367
95
|
this.lastAdBreakTime = accumulatedPlaybackTime;
|
|
368
96
|
}
|
|
369
97
|
}
|
|
370
|
-
}, 1000);
|
|
98
|
+
}, 1000); // Check every second
|
|
371
99
|
}
|
|
372
100
|
|
|
373
101
|
/**
|
|
374
|
-
*
|
|
375
|
-
* AD BREAK TRIGGERING
|
|
376
|
-
* ========================================================================
|
|
102
|
+
* Manually trigger an ad break
|
|
377
103
|
*/
|
|
104
|
+
triggerAdBreak(): void {
|
|
105
|
+
if (!this.adsManager) {
|
|
106
|
+
console.error('❌ Ads manager not initialized');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
378
109
|
|
|
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
110
|
if (this.isPlayingAd()) {
|
|
387
111
|
console.warn('⚠️ Ad already playing, skipping trigger');
|
|
388
112
|
return;
|
|
389
113
|
}
|
|
390
114
|
|
|
391
|
-
|
|
392
|
-
const timeBeforeAd = this.video.currentTime;
|
|
115
|
+
console.log('🎬 Triggering ad break for live stream');
|
|
393
116
|
|
|
394
|
-
|
|
117
|
+
const currentTime = this.video.currentTime;
|
|
118
|
+
console.log(`📍 Stream position before ad: ${currentTime.toFixed(2)}s`);
|
|
395
119
|
|
|
396
|
-
// Pause stream
|
|
120
|
+
// Pause stream download if configured
|
|
397
121
|
if (this.liveConfig.pauseStreamDuringAd) {
|
|
398
122
|
this.pauseStreamDownload();
|
|
399
123
|
}
|
|
400
124
|
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
this.initAdDisplayContainer();
|
|
404
|
-
} catch (e) {
|
|
405
|
-
// Already initialized
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Request ads from Google IMA
|
|
409
|
-
this.requestAds();
|
|
125
|
+
// Store position for DVR-style resume
|
|
126
|
+
(this.video as any).__positionBeforeAd = currentTime;
|
|
410
127
|
|
|
411
|
-
//
|
|
412
|
-
this.
|
|
128
|
+
// Request ads
|
|
129
|
+
this.adsManager.start();
|
|
413
130
|
}
|
|
414
131
|
|
|
415
132
|
/**
|
|
416
|
-
* Pause stream
|
|
133
|
+
* Pause stream download during ad
|
|
417
134
|
*/
|
|
418
135
|
private pauseStreamDownload(): void {
|
|
419
136
|
try {
|
|
420
137
|
if (this.hlsInstance && this.hlsInstance.stopLoad) {
|
|
421
138
|
console.log('⏸️ Pausing stream download during ad');
|
|
422
139
|
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
140
|
}
|
|
429
141
|
} catch (error) {
|
|
430
142
|
console.warn('Could not pause stream download:', error);
|
|
@@ -432,18 +144,13 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
|
|
|
432
144
|
}
|
|
433
145
|
|
|
434
146
|
/**
|
|
435
|
-
* Resume stream
|
|
147
|
+
* Resume stream download after ad
|
|
436
148
|
*/
|
|
437
149
|
private resumeStreamDownload(): void {
|
|
438
150
|
try {
|
|
439
151
|
if (this.hlsInstance && this.hlsInstance.startLoad) {
|
|
440
152
|
console.log('▶️ Resuming stream download after ad');
|
|
441
153
|
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
154
|
}
|
|
448
155
|
} catch (error) {
|
|
449
156
|
console.warn('Could not resume stream download:', error);
|
|
@@ -451,38 +158,37 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
|
|
|
451
158
|
}
|
|
452
159
|
|
|
453
160
|
/**
|
|
454
|
-
*
|
|
161
|
+
* Override parent onAdEnd to add live stream logic
|
|
455
162
|
*/
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
163
|
+
protected setupLiveEdgeResume(): void {
|
|
164
|
+
const originalOnAdEnd = this.liveConfig.onAdEnd;
|
|
165
|
+
|
|
166
|
+
this.liveConfig.onAdEnd = () => {
|
|
167
|
+
const positionBeforeAd = (this.video as any).__positionBeforeAd;
|
|
168
|
+
|
|
459
169
|
if (this.liveConfig.syncToLiveEdge) {
|
|
460
|
-
//
|
|
170
|
+
// Live edge sync mode - jump to live
|
|
461
171
|
console.log('📺 Ad ended, syncing to live edge (skipping content)');
|
|
172
|
+
this.resumeStreamDownload();
|
|
462
173
|
this.syncToLiveEdge();
|
|
463
174
|
} else {
|
|
464
|
-
//
|
|
175
|
+
// DVR mode - resume from pause point
|
|
465
176
|
console.log('📺 Ad ended, resuming from pause point (DVR-style)');
|
|
466
|
-
this.resumeFromPausePoint(
|
|
177
|
+
this.resumeFromPausePoint(positionBeforeAd);
|
|
467
178
|
}
|
|
468
|
-
};
|
|
469
179
|
|
|
470
|
-
|
|
471
|
-
const originalOnAdEnd = this.config.onAdEnd;
|
|
472
|
-
this.config.onAdEnd = () => {
|
|
473
|
-
handleAdEnd();
|
|
180
|
+
// Call original callback
|
|
474
181
|
originalOnAdEnd?.();
|
|
475
182
|
};
|
|
476
183
|
}
|
|
477
184
|
|
|
478
185
|
/**
|
|
479
|
-
* Resume from
|
|
186
|
+
* Resume from paused position (DVR-style)
|
|
480
187
|
*/
|
|
481
188
|
private resumeFromPausePoint(pausedTime: number): void {
|
|
482
189
|
setTimeout(() => {
|
|
483
190
|
try {
|
|
484
191
|
console.log(`⏯️ Resuming from paused position: ${pausedTime.toFixed(2)}s`);
|
|
485
|
-
console.log(` User is now behind live, but will see all content`);
|
|
486
192
|
|
|
487
193
|
// Resume stream downloading first
|
|
488
194
|
if (this.liveConfig.pauseStreamDuringAd) {
|
|
@@ -492,10 +198,7 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
|
|
|
492
198
|
// Resume from exact pause point
|
|
493
199
|
this.video.currentTime = pausedTime;
|
|
494
200
|
|
|
495
|
-
//
|
|
496
|
-
this.liveConfig.onLiveEdgeSync?.(pausedTime);
|
|
497
|
-
|
|
498
|
-
// Calculate how far behind live user is
|
|
201
|
+
// Calculate how far behind live
|
|
499
202
|
if (this.hlsInstance && this.hlsInstance.liveSyncPosition !== undefined) {
|
|
500
203
|
const liveEdge = this.hlsInstance.liveSyncPosition;
|
|
501
204
|
const behindLive = liveEdge - pausedTime;
|
|
@@ -504,122 +207,49 @@ export class LiveStreamAdsManager extends GoogleAdsManager {
|
|
|
504
207
|
} catch (error) {
|
|
505
208
|
console.error('❌ Error resuming from pause point:', error);
|
|
506
209
|
}
|
|
507
|
-
}, 100);
|
|
210
|
+
}, 100);
|
|
508
211
|
}
|
|
509
212
|
|
|
510
213
|
/**
|
|
511
|
-
* Sync
|
|
214
|
+
* Sync to live edge
|
|
512
215
|
*/
|
|
513
216
|
private syncToLiveEdge(): void {
|
|
514
217
|
setTimeout(() => {
|
|
515
218
|
try {
|
|
516
|
-
// Resume stream
|
|
517
|
-
|
|
518
|
-
this.resumeStreamDownload();
|
|
519
|
-
}
|
|
219
|
+
// Resume stream download
|
|
220
|
+
this.resumeStreamDownload();
|
|
520
221
|
|
|
521
|
-
|
|
222
|
+
// Get live edge position
|
|
223
|
+
let liveEdgePosition: number | undefined;
|
|
522
224
|
|
|
523
|
-
// Get live edge from HLS.js
|
|
524
225
|
if (this.hlsInstance && this.hlsInstance.liveSyncPosition !== undefined) {
|
|
226
|
+
console.log(`📍 HLS.js live edge: ${this.hlsInstance.liveSyncPosition.toFixed(2)}s`);
|
|
525
227
|
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
228
|
}
|
|
540
229
|
|
|
541
|
-
|
|
542
|
-
if (liveEdgePosition !== null) {
|
|
230
|
+
if (liveEdgePosition !== undefined) {
|
|
543
231
|
const offset = this.liveConfig.liveEdgeOffset || 3;
|
|
544
|
-
const targetPosition =
|
|
545
|
-
|
|
232
|
+
const targetPosition = liveEdgePosition - offset;
|
|
546
233
|
console.log(`⏩ Syncing to live edge: ${targetPosition.toFixed(2)}s (offset: ${offset}s)`);
|
|
547
|
-
|
|
548
234
|
this.video.currentTime = targetPosition;
|
|
549
|
-
|
|
550
|
-
// Notify callback
|
|
551
|
-
this.liveConfig.onLiveEdgeSync?.(targetPosition);
|
|
552
235
|
} else {
|
|
553
236
|
console.warn('⚠️ Could not determine live edge position');
|
|
554
237
|
}
|
|
555
238
|
} catch (error) {
|
|
556
239
|
console.error('❌ Error syncing to live edge:', error);
|
|
557
240
|
}
|
|
558
|
-
}, 100);
|
|
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');
|
|
241
|
+
}, 100);
|
|
573
242
|
}
|
|
574
243
|
|
|
575
244
|
/**
|
|
576
|
-
*
|
|
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
|
|
245
|
+
* Cleanup
|
|
599
246
|
*/
|
|
600
247
|
destroy(): void {
|
|
601
|
-
// Clear periodic timer
|
|
602
248
|
if (this.periodicAdTimer) {
|
|
603
249
|
clearInterval(this.periodicAdTimer);
|
|
604
250
|
this.periodicAdTimer = null;
|
|
605
251
|
}
|
|
606
252
|
|
|
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
253
|
super.destroy();
|
|
624
254
|
}
|
|
625
255
|
}
|