unified-video-framework 1.4.376 → 1.4.378

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/interfaces/IDRMProtection.d.ts +90 -0
  3. package/packages/core/dist/interfaces/IDRMProtection.d.ts.map +1 -0
  4. package/packages/core/dist/interfaces/IDRMProtection.js +15 -0
  5. package/packages/core/dist/interfaces/IDRMProtection.js.map +1 -0
  6. package/packages/core/dist/interfaces.d.ts +1 -0
  7. package/packages/core/dist/interfaces.d.ts.map +1 -1
  8. package/packages/core/dist/interfaces.js +1 -0
  9. package/packages/core/dist/interfaces.js.map +1 -1
  10. package/packages/core/src/interfaces/IDRMProtection.ts +285 -0
  11. package/packages/core/src/interfaces.ts +2 -0
  12. package/packages/react-native/dist/drm/AndroidDRMProtection.d.ts +21 -0
  13. package/packages/react-native/dist/drm/AndroidDRMProtection.d.ts.map +1 -0
  14. package/packages/react-native/dist/drm/AndroidDRMProtection.js +184 -0
  15. package/packages/react-native/dist/drm/AndroidDRMProtection.js.map +1 -0
  16. package/packages/react-native/dist/drm/iOSDRMProtection.d.ts +21 -0
  17. package/packages/react-native/dist/drm/iOSDRMProtection.d.ts.map +1 -0
  18. package/packages/react-native/dist/drm/iOSDRMProtection.js +172 -0
  19. package/packages/react-native/dist/drm/iOSDRMProtection.js.map +1 -0
  20. package/packages/react-native/src/drm/AndroidDRMProtection.ts +419 -0
  21. package/packages/react-native/src/drm/iOSDRMProtection.ts +415 -0
  22. package/packages/web/dist/drm/WebDRMProtection.d.ts +37 -0
  23. package/packages/web/dist/drm/WebDRMProtection.d.ts.map +1 -0
  24. package/packages/web/dist/drm/WebDRMProtection.js +378 -0
  25. package/packages/web/dist/drm/WebDRMProtection.js.map +1 -0
  26. package/packages/web/dist/index.d.ts +1 -0
  27. package/packages/web/dist/index.d.ts.map +1 -1
  28. package/packages/web/dist/index.js +1 -0
  29. package/packages/web/dist/index.js.map +1 -1
  30. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  31. package/packages/web/dist/react/WebPlayerView.js +17 -42
  32. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  33. package/packages/web/src/drm/WebDRMProtection.ts +596 -0
  34. package/packages/web/src/index.ts +3 -0
  35. package/packages/web/src/react/WebPlayerView.tsx +27 -52
@@ -0,0 +1,596 @@
1
+ /**
2
+ * Web DRM Protection Implementation
3
+ *
4
+ * Implements Netflix-like content protection for web browsers using:
5
+ * - Encrypted Media Extensions (EME)
6
+ * - Widevine / PlayReady DRM
7
+ * - Screen recording detection
8
+ * - Tab capture blocking
9
+ * - Chromecast support
10
+ */
11
+
12
+ import {
13
+ IDRMProtection,
14
+ IDRMProtectionConfig,
15
+ IDRMProtectionStatus,
16
+ CastDevice,
17
+ CastDeviceType,
18
+ DRMError,
19
+ DRMErrorCode,
20
+ DRMKeySystemConfig,
21
+ } from '@unified-video/core';
22
+
23
+ export class WebDRMProtection implements IDRMProtection {
24
+ private config: IDRMProtectionConfig;
25
+ private videoElement: HTMLVideoElement;
26
+ private mediaKeys: MediaKeys | null = null;
27
+ private status: IDRMProtectionStatus;
28
+ private screenRecordingCheckInterval: number | null = null;
29
+ private castSession: any = null;
30
+ private castContext: any = null;
31
+
32
+ // Key Systems in preference order
33
+ private readonly KEY_SYSTEMS = {
34
+ widevine: 'com.widevine.alpha',
35
+ playready: 'com.microsoft.playready',
36
+ clearkey: 'org.w3.clearkey', // Fallback for testing
37
+ };
38
+
39
+ constructor(videoElement: HTMLVideoElement) {
40
+ this.videoElement = videoElement;
41
+ this.config = { enabled: false };
42
+ this.status = {
43
+ isProtected: false,
44
+ drmSystem: 'none',
45
+ isScreenRecordingBlocked: false,
46
+ isAudioCaptureBlocked: false,
47
+ isScreenshotBlocked: false,
48
+ isCasting: false,
49
+ screenRecordingDetected: false,
50
+ mirroringDetected: false,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Initialize DRM protection
56
+ */
57
+ async initialize(config: IDRMProtectionConfig): Promise<void> {
58
+ this.config = {
59
+ blockScreenRecording: true,
60
+ blockAudioCapture: true,
61
+ blockScreenshots: true,
62
+ allowCasting: true,
63
+ blockMirroring: true,
64
+ widevineSecurityLevel: 'L1',
65
+ ...config,
66
+ };
67
+
68
+ if (!this.config.enabled) {
69
+ console.log('[DRM] Protection disabled by configuration');
70
+ return;
71
+ }
72
+
73
+ console.log('[DRM] Initializing web DRM protection...', this.config);
74
+
75
+ try {
76
+ // Check if EME is supported
77
+ if (!this.isEMESupported()) {
78
+ throw this.createError(
79
+ DRMErrorCode.DEVICE_NOT_SUPPORTED,
80
+ 'Encrypted Media Extensions (EME) not supported'
81
+ );
82
+ }
83
+
84
+ // Initialize DRM key system
85
+ if (this.config.licenseServerUrl) {
86
+ await this.initializeDRMKeySystem();
87
+ }
88
+
89
+ // Start screen recording detection
90
+ if (this.config.blockScreenRecording) {
91
+ this.startScreenRecordingDetection();
92
+ }
93
+
94
+ // Initialize casting support
95
+ if (this.config.allowCasting) {
96
+ await this.initializeCastSupport();
97
+ }
98
+
99
+ // Block screenshot capture
100
+ if (this.config.blockScreenshots) {
101
+ this.blockScreenshots();
102
+ }
103
+
104
+ // Prevent tab capture
105
+ if (this.config.blockMirroring) {
106
+ this.preventTabCapture();
107
+ }
108
+
109
+ this.status.isProtected = true;
110
+ console.log('[DRM] Protection initialized successfully');
111
+ } catch (error) {
112
+ console.error('[DRM] Initialization failed:', error);
113
+ this.config.onDRMError?.(error as DRMError);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Check if EME is supported
120
+ */
121
+ private isEMESupported(): boolean {
122
+ return !!(
123
+ window.navigator &&
124
+ (window.navigator as any).requestMediaKeySystemAccess &&
125
+ window.MediaKeys
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Initialize DRM Key System (Widevine/PlayReady)
131
+ */
132
+ private async initializeDRMKeySystem(): Promise<void> {
133
+ console.log('[DRM] Initializing key system...');
134
+
135
+ // Try Widevine first, then PlayReady
136
+ const keySystemConfigs: DRMKeySystemConfig[] = [
137
+ {
138
+ keySystem: this.KEY_SYSTEMS.widevine,
139
+ licenseServerUrl: this.config.licenseServerUrl!,
140
+ certificateUrl: this.config.certificateUrl,
141
+ headers: this.config.licenseHeaders,
142
+ // Widevine robustness levels
143
+ videoRobustness: this.config.widevineSecurityLevel === 'L1' ? 'HW_SECURE_ALL' : 'SW_SECURE_CRYPTO',
144
+ audioRobustness: this.config.widevineSecurityLevel === 'L1' ? 'HW_SECURE_ALL' : 'SW_SECURE_CRYPTO',
145
+ persistentState: 'optional',
146
+ distinctiveIdentifier: 'optional',
147
+ },
148
+ {
149
+ keySystem: this.KEY_SYSTEMS.playready,
150
+ licenseServerUrl: this.config.licenseServerUrl!,
151
+ headers: this.config.licenseHeaders,
152
+ videoRobustness: '3000', // PlayReady SL3000
153
+ audioRobustness: '3000',
154
+ persistentState: 'optional',
155
+ distinctiveIdentifier: 'optional',
156
+ },
157
+ ];
158
+
159
+ for (const config of keySystemConfigs) {
160
+ try {
161
+ await this.requestMediaKeySystemAccess(config);
162
+ console.log(`[DRM] Successfully initialized ${config.keySystem}`);
163
+ return;
164
+ } catch (error) {
165
+ console.warn(`[DRM] ${config.keySystem} not available:`, error);
166
+ }
167
+ }
168
+
169
+ throw this.createError(
170
+ DRMErrorCode.WIDEVINE_NOT_AVAILABLE,
171
+ 'No supported DRM system available (tried Widevine and PlayReady)'
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Request Media Key System Access
177
+ */
178
+ private async requestMediaKeySystemAccess(config: DRMKeySystemConfig): Promise<void> {
179
+ const keySystemConfig = [
180
+ {
181
+ initDataTypes: ['cenc', 'keyids', 'webm'],
182
+ audioCapabilities: [
183
+ {
184
+ contentType: 'audio/mp4; codecs="mp4a.40.2"',
185
+ robustness: config.audioRobustness || '',
186
+ },
187
+ ],
188
+ videoCapabilities: [
189
+ {
190
+ contentType: 'video/mp4; codecs="avc1.42E01E"',
191
+ robustness: config.videoRobustness || '',
192
+ },
193
+ {
194
+ contentType: 'video/webm; codecs="vp9"',
195
+ robustness: config.videoRobustness || '',
196
+ },
197
+ ],
198
+ distinctiveIdentifier: config.distinctiveIdentifier || 'optional',
199
+ persistentState: config.persistentState || 'optional',
200
+ },
201
+ ];
202
+
203
+ const keySystemAccess = await navigator.requestMediaKeySystemAccess(
204
+ config.keySystem,
205
+ keySystemConfig
206
+ );
207
+
208
+ this.mediaKeys = await keySystemAccess.createMediaKeys();
209
+ await this.videoElement.setMediaKeys(this.mediaKeys);
210
+
211
+ // Set up license acquisition
212
+ this.setupLicenseAcquisition(config);
213
+
214
+ // Update status
215
+ if (config.keySystem === this.KEY_SYSTEMS.widevine) {
216
+ this.status.drmSystem = 'widevine';
217
+ this.status.securityLevel = config.videoRobustness === 'HW_SECURE_ALL' ? 'L1' : 'L3';
218
+ } else if (config.keySystem === this.KEY_SYSTEMS.playready) {
219
+ this.status.drmSystem = 'playready';
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Setup license acquisition from license server
225
+ */
226
+ private setupLicenseAcquisition(config: DRMKeySystemConfig): void {
227
+ this.videoElement.addEventListener('encrypted', async (event: any) => {
228
+ console.log('[DRM] Encrypted event received, requesting license...');
229
+
230
+ try {
231
+ if (!this.mediaKeys) {
232
+ throw new Error('MediaKeys not initialized');
233
+ }
234
+
235
+ const session = this.mediaKeys.createSession();
236
+
237
+ // Listen for license messages
238
+ session.addEventListener('message', async (messageEvent: any) => {
239
+ console.log('[DRM] License request message received');
240
+
241
+ try {
242
+ const response = await fetch(config.licenseServerUrl, {
243
+ method: 'POST',
244
+ headers: {
245
+ 'Content-Type': 'application/octet-stream',
246
+ ...config.headers,
247
+ },
248
+ body: messageEvent.message,
249
+ });
250
+
251
+ if (!response.ok) {
252
+ throw this.createError(
253
+ DRMErrorCode.LICENSE_REQUEST_FAILED,
254
+ `License server returned ${response.status}`
255
+ );
256
+ }
257
+
258
+ const license = await response.arrayBuffer();
259
+ await session.update(license);
260
+
261
+ console.log('[DRM] License acquired successfully');
262
+ this.config.onLicenseAcquired?.();
263
+ } catch (error) {
264
+ console.error('[DRM] License acquisition failed:', error);
265
+ this.config.onDRMError?.(error as DRMError);
266
+ }
267
+ });
268
+
269
+ // Generate license request
270
+ await session.generateRequest(event.initDataType, event.initData);
271
+ } catch (error) {
272
+ console.error('[DRM] Failed to generate license request:', error);
273
+ this.config.onDRMError?.(error as DRMError);
274
+ }
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Start screen recording detection
280
+ * Uses multiple detection methods
281
+ */
282
+ private startScreenRecordingDetection(): void {
283
+ console.log('[DRM] Starting screen recording detection...');
284
+
285
+ // Method 1: getDisplayMedia API detection
286
+ this.detectGetDisplayMedia();
287
+
288
+ // Method 2: Canvas fingerprinting detection
289
+ this.detectCanvasCapture();
290
+
291
+ // Method 3: Tab capture detection (Chrome)
292
+ this.detectTabCapture();
293
+
294
+ // Method 4: Periodic frame analysis
295
+ this.screenRecordingCheckInterval = window.setInterval(() => {
296
+ this.analyzeScreenRecordingSignals();
297
+ }, 1000);
298
+
299
+ this.status.isScreenRecordingBlocked = true;
300
+ }
301
+
302
+ /**
303
+ * Detect getDisplayMedia screen capture
304
+ */
305
+ private detectGetDisplayMedia(): void {
306
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
307
+ return;
308
+ }
309
+
310
+ // Override getDisplayMedia to detect screen capture attempts
311
+ const originalGetDisplayMedia = navigator.mediaDevices.getDisplayMedia.bind(
312
+ navigator.mediaDevices
313
+ );
314
+
315
+ navigator.mediaDevices.getDisplayMedia = async function (constraints?: any) {
316
+ console.warn('[DRM] Screen capture attempt detected via getDisplayMedia');
317
+ // Block the request or notify
318
+ throw new DOMException('Screen capture blocked by DRM protection', 'NotAllowedError');
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Detect canvas-based capture
324
+ */
325
+ private detectCanvasCapture(): void {
326
+ // Monitor canvas operations on the video
327
+ const observer = new MutationObserver((mutations) => {
328
+ mutations.forEach((mutation) => {
329
+ if (mutation.type === 'childList') {
330
+ mutation.addedNodes.forEach((node) => {
331
+ if (node.nodeName === 'CANVAS') {
332
+ console.warn('[DRM] Canvas element detected - potential screen capture');
333
+ this.handleScreenRecordingDetected();
334
+ }
335
+ });
336
+ }
337
+ });
338
+ });
339
+
340
+ observer.observe(document.body, {
341
+ childList: true,
342
+ subtree: true,
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Detect tab capture (Chrome-specific)
348
+ */
349
+ private detectTabCapture(): void {
350
+ // Chrome exposes tab capture status
351
+ if ('mediaSession' in navigator) {
352
+ document.addEventListener('visibilitychange', () => {
353
+ if (document.hidden) {
354
+ console.log('[DRM] Tab hidden - checking for capture...');
355
+ }
356
+ });
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Analyze various signals for screen recording
362
+ */
363
+ private analyzeScreenRecordingSignals(): void {
364
+ // Check if video is playing in background tab
365
+ if (document.hidden && !this.videoElement.paused) {
366
+ console.warn('[DRM] Video playing in hidden tab - possible screen recording');
367
+ }
368
+
369
+ // Check for unusual video element states
370
+ if ((this.videoElement as any).captureStream) {
371
+ console.warn('[DRM] captureStream API available - monitoring...');
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Handle screen recording detection
377
+ */
378
+ private handleScreenRecordingDetected(): void {
379
+ console.error('[DRM] ⚠️ SCREEN RECORDING DETECTED!');
380
+ this.status.screenRecordingDetected = true;
381
+
382
+ // Black out video
383
+ this.videoElement.style.filter = 'brightness(0)';
384
+ this.videoElement.pause();
385
+
386
+ // Notify callback
387
+ this.config.onScreenRecordingDetected?.();
388
+
389
+ // Show warning overlay
390
+ this.showProtectionWarning('Screen recording detected. Playback blocked.');
391
+ }
392
+
393
+ /**
394
+ * Block screenshots
395
+ */
396
+ private blockScreenshots(): void {
397
+ // CSS-based screenshot prevention
398
+ this.videoElement.style.webkitUserSelect = 'none';
399
+ this.videoElement.style.userSelect = 'none';
400
+ this.videoElement.setAttribute('oncontextmenu', 'return false');
401
+
402
+ // Prevent drag-and-drop
403
+ this.videoElement.addEventListener('dragstart', (e) => e.preventDefault());
404
+
405
+ // Detect PrintScreen key
406
+ document.addEventListener('keyup', (e) => {
407
+ if (e.key === 'PrintScreen') {
408
+ console.warn('[DRM] Screenshot attempt detected (PrintScreen key)');
409
+ this.config.onScreenshotAttempted?.();
410
+ }
411
+ });
412
+
413
+ this.status.isScreenshotBlocked = true;
414
+ console.log('[DRM] Screenshot blocking enabled');
415
+ }
416
+
417
+ /**
418
+ * Prevent tab capture and mirroring
419
+ */
420
+ private preventTabCapture(): void {
421
+ // Detect mirroring via multiple screens
422
+ if (window.screen && (window.screen as any).isExtended) {
423
+ console.warn('[DRM] Extended display detected');
424
+ this.status.mirroringDetected = true;
425
+ this.config.onMirroringDetected?.();
426
+ }
427
+
428
+ // Monitor presentation API
429
+ if ('presentation' in navigator) {
430
+ console.log('[DRM] Monitoring presentation API for mirroring...');
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Initialize Chromecast support
436
+ */
437
+ private async initializeCastSupport(): Promise<void> {
438
+ if (!(window as any).chrome || !(window as any).chrome.cast) {
439
+ console.warn('[DRM] Google Cast API not available');
440
+ return;
441
+ }
442
+
443
+ console.log('[DRM] Initializing Chromecast support...');
444
+
445
+ // Load Cast SDK
446
+ const script = document.createElement('script');
447
+ script.src = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
448
+ document.head.appendChild(script);
449
+
450
+ await new Promise((resolve) => {
451
+ script.onload = resolve;
452
+ });
453
+
454
+ // Initialize Cast API
455
+ (window as any).__onGCastApiAvailable = (isAvailable: boolean) => {
456
+ if (isAvailable) {
457
+ this.castContext = (window as any).cast.framework.CastContext.getInstance();
458
+ this.castContext.setOptions({
459
+ receiverApplicationId: (window as any).chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
460
+ autoJoinPolicy: (window as any).chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
461
+ });
462
+
463
+ console.log('[DRM] Chromecast initialized');
464
+ }
465
+ };
466
+ }
467
+
468
+ /**
469
+ * Get current protection status
470
+ */
471
+ getStatus(): IDRMProtectionStatus {
472
+ return { ...this.status };
473
+ }
474
+
475
+ /**
476
+ * Enable/disable DRM protection
477
+ */
478
+ setEnabled(enabled: boolean): void {
479
+ this.config.enabled = enabled;
480
+ if (!enabled) {
481
+ this.dispose();
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Enable/disable specific feature
487
+ */
488
+ setFeature(feature: keyof IDRMProtectionConfig, enabled: boolean): void {
489
+ (this.config as any)[feature] = enabled;
490
+
491
+ // Re-apply feature if changing during playback
492
+ switch (feature) {
493
+ case 'blockScreenRecording':
494
+ if (enabled) {
495
+ this.startScreenRecordingDetection();
496
+ } else if (this.screenRecordingCheckInterval) {
497
+ clearInterval(this.screenRecordingCheckInterval);
498
+ }
499
+ break;
500
+ case 'blockScreenshots':
501
+ this.status.isScreenshotBlocked = enabled;
502
+ break;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Start casting
508
+ */
509
+ async startCasting(deviceId: string): Promise<void> {
510
+ if (!this.castContext) {
511
+ throw new Error('Cast not initialized');
512
+ }
513
+
514
+ // Cast implementation
515
+ console.log('[DRM] Starting cast to device:', deviceId);
516
+ this.status.isCasting = true;
517
+ }
518
+
519
+ /**
520
+ * Stop casting
521
+ */
522
+ async stopCasting(): Promise<void> {
523
+ if (this.castSession) {
524
+ this.castSession.endSession(true);
525
+ this.castSession = null;
526
+ }
527
+ this.status.isCasting = false;
528
+ }
529
+
530
+ /**
531
+ * Get available cast devices
532
+ */
533
+ async getAvailableCastDevices(): Promise<CastDevice[]> {
534
+ // Return available Chromecast devices
535
+ return [];
536
+ }
537
+
538
+ /**
539
+ * Renew DRM license
540
+ */
541
+ async renewLicense(): Promise<void> {
542
+ console.log('[DRM] Renewing license...');
543
+ // Re-trigger license acquisition
544
+ }
545
+
546
+ /**
547
+ * Show protection warning overlay
548
+ */
549
+ private showProtectionWarning(message: string): void {
550
+ const overlay = document.createElement('div');
551
+ overlay.style.cssText = `
552
+ position: absolute;
553
+ top: 0;
554
+ left: 0;
555
+ width: 100%;
556
+ height: 100%;
557
+ background: #000;
558
+ color: #fff;
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ font-size: 18px;
563
+ z-index: 9999999;
564
+ `;
565
+ overlay.textContent = message;
566
+ this.videoElement.parentElement?.appendChild(overlay);
567
+ }
568
+
569
+ /**
570
+ * Create DRM error
571
+ */
572
+ private createError(code: DRMErrorCode, message: string): DRMError {
573
+ return {
574
+ code,
575
+ message,
576
+ platform: 'web',
577
+ recoverable: false,
578
+ };
579
+ }
580
+
581
+ /**
582
+ * Cleanup resources
583
+ */
584
+ dispose(): void {
585
+ if (this.screenRecordingCheckInterval) {
586
+ clearInterval(this.screenRecordingCheckInterval);
587
+ }
588
+
589
+ if (this.castSession) {
590
+ this.stopCasting();
591
+ }
592
+
593
+ this.status.isProtected = false;
594
+ console.log('[DRM] Protection disposed');
595
+ }
596
+ }
@@ -11,6 +11,9 @@ export { WebPlayer } from './WebPlayer';
11
11
  export { WebPlayerView } from './react/WebPlayerView';
12
12
  export { SecureVideoPlayer } from './SecureVideoPlayer';
13
13
 
14
+ // Export DRM Protection
15
+ export { WebDRMProtection } from './drm/WebDRMProtection';
16
+
14
17
  // Export EPG (Electronic Program Guide) components
15
18
  export * from './react/EPG';
16
19
 
@@ -534,45 +534,6 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
534
534
  const adContainerRef = useRef<HTMLDivElement | null>(null);
535
535
  const [isAdPlaying, setIsAdPlaying] = useState(false);
536
536
 
537
- // Create ad container element programmatically
538
- useEffect(() => {
539
- if (!props.googleAds || adContainerRef.current) return;
540
-
541
- const adContainer = document.createElement('div');
542
- adContainer.className = 'uvf-ad-container';
543
- adContainer.style.cssText = `
544
- position: absolute;
545
- top: 0;
546
- left: 0;
547
- right: 0;
548
- bottom: 0;
549
- z-index: 999999999;
550
- pointer-events: none;
551
- visibility: hidden;
552
- opacity: 0;
553
- transition: opacity 0.2s ease, visibility 0.2s ease;
554
- `;
555
-
556
- adContainerRef.current = adContainer;
557
- console.log('✅ Ad container element created (will be injected into player wrapper)');
558
-
559
- return () => {
560
- // Cleanup: remove ad container on unmount
561
- if (adContainer.parentElement) {
562
- adContainer.parentElement.removeChild(adContainer);
563
- }
564
- };
565
- }, [props.googleAds]);
566
-
567
- // Update ad container visibility when isAdPlaying changes
568
- useEffect(() => {
569
- if (!adContainerRef.current) return;
570
-
571
- adContainerRef.current.style.pointerEvents = isAdPlaying ? 'auto' : 'none';
572
- adContainerRef.current.style.visibility = isAdPlaying ? 'visible' : 'hidden';
573
- adContainerRef.current.style.opacity = isAdPlaying ? '1' : '0';
574
- }, [isAdPlaying]);
575
-
576
537
  /**
577
538
  * Generate ad chapter segments from googleAds cue points
578
539
  * Handles pre-roll (0), mid-rolls, and post-roll (-1)
@@ -1287,21 +1248,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1287
1248
  return;
1288
1249
  }
1289
1250
 
1290
- // First, inject ad container into player wrapper before initializing Google Ads
1291
- const playerWrapper = containerRef.current?.querySelector('.uvf-player-wrapper');
1292
- if (playerWrapper) {
1293
- // Add ad container to player wrapper
1294
- playerWrapper.appendChild(adContainer);
1295
- console.log('✅ Ad container injected into player wrapper before IMA SDK initialization');
1296
- } else {
1297
- console.error('❌ Player wrapper not found - cannot inject ad container');
1298
- return;
1299
- }
1300
-
1301
1251
  console.log('✅ Initializing Google Ads...', {
1302
1252
  adContainer: adContainer.className,
1303
1253
  videoElement: videoElement.tagName,
1304
- adContainerParent: adContainer.parentElement?.className,
1305
1254
  adContainerInDOM: document.body.contains(adContainer)
1306
1255
  });
1307
1256
 
@@ -1359,6 +1308,14 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1359
1308
 
1360
1309
  console.log('✅ Google Ads initialized successfully');
1361
1310
 
1311
+ // Move ad container into player wrapper for fullscreen support
1312
+ const playerWrapper = containerRef.current?.querySelector('.uvf-player-wrapper');
1313
+ if (playerWrapper && adContainerRef.current && adContainerRef.current.parentElement) {
1314
+ // Move ad container from player-container to player-wrapper
1315
+ playerWrapper.appendChild(adContainerRef.current);
1316
+ console.log('✅ Ad container moved into player wrapper for fullscreen support');
1317
+ }
1318
+
1362
1319
  // Initialize ad display container on first user interaction
1363
1320
  // Chrome requires this to be called on a user gesture
1364
1321
  let adContainerInitialized = false;
@@ -1731,7 +1688,25 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1731
1688
  style={responsiveStyle}
1732
1689
  />
1733
1690
 
1734
- {/* Ad container will be programmatically injected into player wrapper */}
1691
+ {/* Google Ads Container - positioned over the video player */}
1692
+ {props.googleAds && (
1693
+ <div
1694
+ ref={adContainerRef}
1695
+ className="uvf-ad-container"
1696
+ style={{
1697
+ position: 'absolute',
1698
+ top: 0,
1699
+ left: 0,
1700
+ right: 0,
1701
+ bottom: 0,
1702
+ zIndex: 999999999,
1703
+ pointerEvents: isAdPlaying ? 'auto' : 'none',
1704
+ visibility: isAdPlaying ? 'visible' : 'hidden',
1705
+ opacity: isAdPlaying ? 1 : 0,
1706
+ transition: 'opacity 0.2s ease, visibility 0.2s ease',
1707
+ }}
1708
+ />
1709
+ )}
1735
1710
 
1736
1711
 
1737
1712
  {/* EPG Overlay - Full-screen glassmorphic overlay */}