unified-video-framework 1.4.157 → 1.4.159

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 (67) hide show
  1. package/package.json +12 -2
  2. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.d.ts +18 -0
  3. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.d.ts.map +1 -0
  4. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.js +117 -0
  5. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.js.map +1 -0
  6. package/packages/core/dist/analytics/core/AnalyticsProvider.d.ts +18 -0
  7. package/packages/core/dist/analytics/core/AnalyticsProvider.d.ts.map +1 -0
  8. package/packages/core/dist/analytics/core/AnalyticsProvider.js +99 -0
  9. package/packages/core/dist/analytics/core/AnalyticsProvider.js.map +1 -0
  10. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.d.ts +20 -0
  11. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.d.ts.map +1 -0
  12. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.js +161 -0
  13. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.js.map +1 -0
  14. package/packages/core/dist/analytics/core/EventBatcher.d.ts +32 -0
  15. package/packages/core/dist/analytics/core/EventBatcher.d.ts.map +1 -0
  16. package/packages/core/dist/analytics/core/EventBatcher.js +98 -0
  17. package/packages/core/dist/analytics/core/EventBatcher.js.map +1 -0
  18. package/packages/core/dist/analytics/core/PlayerAnalytics.d.ts +19 -0
  19. package/packages/core/dist/analytics/core/PlayerAnalytics.d.ts.map +1 -0
  20. package/packages/core/dist/analytics/core/PlayerAnalytics.js +80 -0
  21. package/packages/core/dist/analytics/core/PlayerAnalytics.js.map +1 -0
  22. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.d.ts +32 -0
  23. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.d.ts.map +1 -0
  24. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.js +220 -0
  25. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.js.map +1 -0
  26. package/packages/core/dist/analytics/index.d.ts +13 -0
  27. package/packages/core/dist/analytics/index.d.ts.map +1 -0
  28. package/packages/core/dist/analytics/index.js +13 -0
  29. package/packages/core/dist/analytics/index.js.map +1 -0
  30. package/packages/core/dist/analytics/types/AnalyticsTypes.d.ts +239 -0
  31. package/packages/core/dist/analytics/types/AnalyticsTypes.d.ts.map +1 -0
  32. package/packages/core/dist/analytics/types/AnalyticsTypes.js +8 -0
  33. package/packages/core/dist/analytics/types/AnalyticsTypes.js.map +1 -0
  34. package/packages/core/dist/analytics/utils/DeviceDetection.d.ts +27 -0
  35. package/packages/core/dist/analytics/utils/DeviceDetection.d.ts.map +1 -0
  36. package/packages/core/dist/analytics/utils/DeviceDetection.js +184 -0
  37. package/packages/core/dist/analytics/utils/DeviceDetection.js.map +1 -0
  38. package/packages/core/dist/chapter-manager.d.ts +39 -0
  39. package/packages/core/dist/index.d.ts +1 -0
  40. package/packages/core/dist/index.d.ts.map +1 -1
  41. package/packages/core/dist/index.js +1 -0
  42. package/packages/core/dist/index.js.map +1 -1
  43. package/packages/core/src/analytics/README.md +902 -0
  44. package/packages/core/src/analytics/adapters/PlayerAnalyticsAdapter.ts +156 -0
  45. package/packages/core/src/analytics/core/AnalyticsProvider.ts +169 -0
  46. package/packages/core/src/analytics/core/DynamicAnalyticsManager.ts +199 -0
  47. package/packages/core/src/analytics/core/EventBatcher.ts +160 -0
  48. package/packages/core/src/analytics/core/PlayerAnalytics.ts +147 -0
  49. package/packages/core/src/analytics/index.ts +51 -0
  50. package/packages/core/src/analytics/types/AnalyticsTypes.ts +315 -0
  51. package/packages/core/src/analytics/utils/DeviceDetection.ts +220 -0
  52. package/packages/core/src/index.ts +3 -0
  53. package/packages/ios/README.md +84 -0
  54. package/packages/web/dist/WebPlayer.d.ts +6 -0
  55. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  56. package/packages/web/dist/WebPlayer.js +273 -67
  57. package/packages/web/dist/WebPlayer.js.map +1 -1
  58. package/packages/web/dist/epg/EPGController.d.ts +78 -0
  59. package/packages/web/dist/epg/EPGController.d.ts.map +1 -0
  60. package/packages/web/dist/epg/EPGController.js +476 -0
  61. package/packages/web/dist/epg/EPGController.js.map +1 -0
  62. package/packages/web/src/WebPlayer.ts +336 -85
  63. package/src/analytics/README.md +902 -0
  64. package/src/analytics/adapters/PlayerAnalyticsAdapter.ts +572 -0
  65. package/src/analytics/core/DynamicAnalyticsManager.ts +526 -0
  66. package/src/analytics/examples/DynamicAnalyticsExample.ts +324 -0
  67. package/src/analytics/index.ts +60 -0
@@ -215,8 +215,8 @@ export class WebPlayer extends BasePlayer {
215
215
  this.video.controls = false; // We'll use custom controls
216
216
  // Don't set autoplay attribute - we'll handle it programmatically with intelligent detection
217
217
  this.video.autoplay = false;
218
- // Start muted if autoplay is enabled (browser policy), but we'll try unmuted if supported
219
- this.video.muted = this.config.autoPlay ? true : (this.config.muted ?? false);
218
+ // Respect user's muted preference, intelligent autoplay will handle browser policies
219
+ this.video.muted = this.config.muted ?? false;
220
220
  this.video.loop = this.config.loop ?? false;
221
221
  this.video.playsInline = this.config.playsInline ?? true;
222
222
  this.video.preload = this.config.preload ?? 'metadata';
@@ -862,6 +862,25 @@ export class WebPlayer extends BasePlayer {
862
862
  }
863
863
  }
864
864
 
865
+ /**
866
+ * Check if page has user activation (from navigation or interaction)
867
+ */
868
+ private hasUserActivation(): boolean {
869
+ // Check if browser supports userActivation API
870
+ if (typeof navigator !== 'undefined' && (navigator as any).userActivation) {
871
+ const hasActivation = (navigator as any).userActivation.hasBeenActive;
872
+ this.debugLog(`🎯 User activation detected: ${hasActivation}`);
873
+ return hasActivation;
874
+ }
875
+
876
+ // Fallback: Check if user has interacted with the page
877
+ const hasInteracted = this.lastUserInteraction > 0 &&
878
+ (Date.now() - this.lastUserInteraction) < 5000;
879
+
880
+ this.debugLog(`🎯 Recent user interaction: ${hasInteracted}`);
881
+ return hasInteracted;
882
+ }
883
+
865
884
  /**
866
885
  * Attempt intelligent autoplay based on detected capabilities
867
886
  */
@@ -871,11 +890,19 @@ export class WebPlayer extends BasePlayer {
871
890
  // Detect capabilities first
872
891
  await this.detectAutoplayCapabilities();
873
892
 
874
- // Try unmuted autoplay if supported and not explicitly muted
875
- if (this.autoplayCapabilities.canAutoplayUnmuted && !this.config.muted) {
893
+ // Check if user has activated the page (navigation counts as activation)
894
+ const hasActivation = this.hasUserActivation();
895
+
896
+ // Try unmuted autoplay if:
897
+ // 1. Browser supports unmuted autoplay OR user has activated the page
898
+ // 2. User hasn't explicitly set muted=true
899
+ const shouldTryUnmuted = (this.autoplayCapabilities.canAutoplayUnmuted || hasActivation)
900
+ && this.config.muted !== true;
901
+
902
+ if (shouldTryUnmuted) {
876
903
  this.video.muted = false;
877
904
  this.video.volume = this.config.volume ?? 1.0;
878
- this.debugLog('🔊 Attempting unmuted autoplay');
905
+ this.debugLog(`🔊 Attempting unmuted autoplay (activation: ${hasActivation})`);
879
906
 
880
907
  try {
881
908
  await this.play();
@@ -887,7 +914,7 @@ export class WebPlayer extends BasePlayer {
887
914
  }
888
915
 
889
916
  // Fall back to muted autoplay
890
- if (this.autoplayCapabilities.canAutoplayMuted) {
917
+ if (this.autoplayCapabilities.canAutoplayMuted || hasActivation) {
891
918
  this.video.muted = true;
892
919
  this.debugLog('🔇 Attempting muted autoplay');
893
920
 
@@ -1314,20 +1341,41 @@ export class WebPlayer extends BasePlayer {
1314
1341
  if (!this.playerWrapper) return;
1315
1342
 
1316
1343
  try {
1317
- // Check if fullscreen is supported
1318
- if (!document.fullscreenEnabled &&
1319
- !(document as any).webkitFullscreenEnabled &&
1320
- !(document as any).mozFullScreenEnabled &&
1321
- !(document as any).msFullscreenEnabled) {
1344
+ // iOS Safari special handling - use video element fullscreen
1345
+ if (this.isIOSDevice() && this.video) {
1346
+ this.debugLog('iOS device detected - using video element fullscreen');
1347
+
1348
+ try {
1349
+ // iOS Safari supports video fullscreen but not element fullscreen
1350
+ if ((this.video as any).webkitEnterFullscreen) {
1351
+ await (this.video as any).webkitEnterFullscreen();
1352
+ this.playerWrapper.classList.add('uvf-fullscreen');
1353
+ this.emit('onFullscreenChanged', true);
1354
+ return;
1355
+ } else if ((this.video as any).webkitRequestFullscreen) {
1356
+ await (this.video as any).webkitRequestFullscreen();
1357
+ this.playerWrapper.classList.add('uvf-fullscreen');
1358
+ this.emit('onFullscreenChanged', true);
1359
+ return;
1360
+ }
1361
+ } catch (iosError) {
1362
+ this.debugWarn('iOS video fullscreen failed:', (iosError as Error).message);
1363
+ // Fall through to try standard fullscreen
1364
+ }
1365
+ }
1366
+
1367
+ // Check if fullscreen is supported for non-iOS devices
1368
+ if (!this.isFullscreenSupported()) {
1322
1369
  this.debugWarn('Fullscreen not supported by browser');
1370
+ // On mobile devices that don't support fullscreen, show a helpful message
1371
+ if (this.isMobileDevice()) {
1372
+ this.showShortcutIndicator('Rotate device for fullscreen experience');
1373
+ }
1323
1374
  return;
1324
1375
  }
1325
1376
 
1326
1377
  // Check if already in fullscreen
1327
- if (document.fullscreenElement ||
1328
- (document as any).webkitFullscreenElement ||
1329
- (document as any).mozFullScreenElement ||
1330
- (document as any).msFullscreenElement) {
1378
+ if (this.isFullscreen()) {
1331
1379
  this.debugLog('Already in fullscreen mode');
1332
1380
  return;
1333
1381
  }
@@ -1335,31 +1383,63 @@ export class WebPlayer extends BasePlayer {
1335
1383
  // Target the player wrapper to maintain custom controls
1336
1384
  const element = this.playerWrapper;
1337
1385
 
1386
+ // Try different fullscreen APIs with better error handling
1387
+ let fullscreenSuccess = false;
1388
+
1338
1389
  if (element.requestFullscreen) {
1339
- await element.requestFullscreen().catch(err => {
1340
- this.debugWarn('Fullscreen request failed:', err.message);
1341
- // Don't throw, just log the error
1342
- });
1390
+ try {
1391
+ await element.requestFullscreen();
1392
+ fullscreenSuccess = true;
1393
+ } catch (err) {
1394
+ this.debugWarn('Standard fullscreen request failed:', (err as Error).message);
1395
+ }
1343
1396
  } else if ((element as any).webkitRequestFullscreen) {
1344
- await (element as any).webkitRequestFullscreen().catch((err: any) => {
1345
- this.debugWarn('WebKit fullscreen request failed:', err.message);
1346
- });
1397
+ try {
1398
+ await (element as any).webkitRequestFullscreen();
1399
+ fullscreenSuccess = true;
1400
+ } catch (err) {
1401
+ this.debugWarn('WebKit fullscreen request failed:', (err as Error).message);
1402
+ }
1347
1403
  } else if ((element as any).mozRequestFullScreen) {
1348
- await (element as any).mozRequestFullScreen().catch((err: any) => {
1349
- this.debugWarn('Mozilla fullscreen request failed:', err.message);
1350
- });
1404
+ try {
1405
+ await (element as any).mozRequestFullScreen();
1406
+ fullscreenSuccess = true;
1407
+ } catch (err) {
1408
+ this.debugWarn('Mozilla fullscreen request failed:', (err as Error).message);
1409
+ }
1351
1410
  } else if ((element as any).msRequestFullscreen) {
1352
- await (element as any).msRequestFullscreen().catch((err: any) => {
1353
- this.debugWarn('MS fullscreen request failed:', err.message);
1354
- });
1411
+ try {
1412
+ await (element as any).msRequestFullscreen();
1413
+ fullscreenSuccess = true;
1414
+ } catch (err) {
1415
+ this.debugWarn('MS fullscreen request failed:', (err as Error).message);
1416
+ }
1417
+ }
1418
+
1419
+ if (fullscreenSuccess) {
1420
+ // Add fullscreen class for styling
1421
+ this.playerWrapper.classList.add('uvf-fullscreen');
1422
+ this.emit('onFullscreenChanged', true);
1423
+
1424
+ // On Android, suggest orientation for better experience
1425
+ if (this.isAndroidDevice()) {
1426
+ setTimeout(() => {
1427
+ this.showShortcutIndicator('Rotate device to landscape for best experience');
1428
+ }, 1000);
1429
+ }
1355
1430
  } else {
1356
- this.debugWarn('Fullscreen API not supported by this browser');
1357
- return;
1431
+ this.debugWarn('All fullscreen methods failed');
1432
+
1433
+ // Provide helpful feedback based on device
1434
+ if (this.isIOSDevice()) {
1435
+ this.showShortcutIndicator('Fullscreen not available - use device controls');
1436
+ } else if (this.isAndroidDevice()) {
1437
+ this.showShortcutIndicator('Try rotating device to landscape');
1438
+ } else {
1439
+ this.showShortcutIndicator('Fullscreen not supported in this browser');
1440
+ }
1358
1441
  }
1359
1442
 
1360
- // Add fullscreen class for styling
1361
- this.playerWrapper.classList.add('uvf-fullscreen');
1362
- this.emit('onFullscreenChanged', true);
1363
1443
  } catch (error) {
1364
1444
  this.debugWarn('Failed to enter fullscreen:', (error as Error).message);
1365
1445
  // Don't re-throw the error to prevent breaking the user experience
@@ -1368,38 +1448,76 @@ export class WebPlayer extends BasePlayer {
1368
1448
 
1369
1449
  async exitFullscreen(): Promise<void> {
1370
1450
  try {
1451
+ // iOS Safari special handling
1452
+ if (this.isIOSDevice() && this.video) {
1453
+ try {
1454
+ if ((this.video as any).webkitExitFullscreen) {
1455
+ await (this.video as any).webkitExitFullscreen();
1456
+ if (this.playerWrapper) {
1457
+ this.playerWrapper.classList.remove('uvf-fullscreen');
1458
+ }
1459
+ this.emit('onFullscreenChanged', false);
1460
+ return;
1461
+ }
1462
+ } catch (iosError) {
1463
+ this.debugWarn('iOS video exit fullscreen failed:', (iosError as Error).message);
1464
+ // Fall through to try standard methods
1465
+ }
1466
+ }
1467
+
1371
1468
  // Check if we're actually in fullscreen
1372
- if (!document.fullscreenElement &&
1373
- !(document as any).webkitFullscreenElement &&
1374
- !(document as any).mozFullScreenElement &&
1375
- !(document as any).msFullscreenElement) {
1469
+ if (!this.isFullscreen()) {
1376
1470
  this.debugLog('Not in fullscreen mode');
1377
1471
  return;
1378
1472
  }
1379
1473
 
1474
+ // Try different exit fullscreen methods
1475
+ let exitSuccess = false;
1476
+
1380
1477
  if (document.exitFullscreen) {
1381
- await document.exitFullscreen().catch(err => {
1382
- this.debugWarn('Exit fullscreen failed:', err.message);
1383
- });
1478
+ try {
1479
+ await document.exitFullscreen();
1480
+ exitSuccess = true;
1481
+ } catch (err) {
1482
+ this.debugWarn('Standard exit fullscreen failed:', (err as Error).message);
1483
+ }
1384
1484
  } else if ((document as any).webkitExitFullscreen) {
1385
- await (document as any).webkitExitFullscreen().catch((err: any) => {
1386
- this.debugWarn('WebKit exit fullscreen failed:', err.message);
1387
- });
1485
+ try {
1486
+ await (document as any).webkitExitFullscreen();
1487
+ exitSuccess = true;
1488
+ } catch (err) {
1489
+ this.debugWarn('WebKit exit fullscreen failed:', (err as Error).message);
1490
+ }
1388
1491
  } else if ((document as any).mozCancelFullScreen) {
1389
- await (document as any).mozCancelFullScreen().catch((err: any) => {
1390
- this.debugWarn('Mozilla exit fullscreen failed:', err.message);
1391
- });
1492
+ try {
1493
+ await (document as any).mozCancelFullScreen();
1494
+ exitSuccess = true;
1495
+ } catch (err) {
1496
+ this.debugWarn('Mozilla exit fullscreen failed:', (err as Error).message);
1497
+ }
1392
1498
  } else if ((document as any).msExitFullscreen) {
1393
- await (document as any).msExitFullscreen().catch((err: any) => {
1394
- this.debugWarn('MS exit fullscreen failed:', err.message);
1395
- });
1499
+ try {
1500
+ await (document as any).msExitFullscreen();
1501
+ exitSuccess = true;
1502
+ } catch (err) {
1503
+ this.debugWarn('MS exit fullscreen failed:', (err as Error).message);
1504
+ }
1396
1505
  }
1397
1506
 
1398
- // Remove fullscreen class
1399
- if (this.playerWrapper) {
1400
- this.playerWrapper.classList.remove('uvf-fullscreen');
1507
+ if (exitSuccess || !this.isFullscreen()) {
1508
+ // Remove fullscreen class
1509
+ if (this.playerWrapper) {
1510
+ this.playerWrapper.classList.remove('uvf-fullscreen');
1511
+ }
1512
+ this.emit('onFullscreenChanged', false);
1513
+ } else {
1514
+ this.debugWarn('All exit fullscreen methods failed');
1515
+ // Still remove the class to keep UI consistent
1516
+ if (this.playerWrapper) {
1517
+ this.playerWrapper.classList.remove('uvf-fullscreen');
1518
+ }
1401
1519
  }
1402
- this.emit('onFullscreenChanged', false);
1520
+
1403
1521
  } catch (error) {
1404
1522
  this.debugWarn('Failed to exit fullscreen:', (error as Error).message);
1405
1523
  // Don't re-throw the error to prevent breaking the user experience
@@ -4133,7 +4251,7 @@ export class WebPlayer extends BasePlayer {
4133
4251
  }
4134
4252
  }
4135
4253
 
4136
- /* iOS Safari specific fixes - address bar handling */
4254
+ /* iOS Safari specific fixes - address bar handling and control positioning */
4137
4255
  @supports (-webkit-appearance: none) {
4138
4256
  .uvf-player-wrapper.uvf-fullscreen,
4139
4257
  .uvf-video-container.uvf-fullscreen {
@@ -4151,6 +4269,31 @@ export class WebPlayer extends BasePlayer {
4151
4269
  .uvf-player-wrapper {
4152
4270
  height: -webkit-fill-available;
4153
4271
  min-height: 100vh;
4272
+ /* Fix for iOS Safari control overlay positioning */
4273
+ position: relative;
4274
+ overflow: hidden;
4275
+ }
4276
+
4277
+ /* iOS Safari specific fixes for control positioning */
4278
+ .uvf-controls-bar {
4279
+ position: absolute !important;
4280
+ bottom: 0 !important;
4281
+ left: 0 !important;
4282
+ right: 0 !important;
4283
+ /* Ensure hardware acceleration */
4284
+ -webkit-transform: translate3d(0,0,0);
4285
+ transform: translate3d(0,0,0);
4286
+ /* Prevent any webkit transforms that could cause positioning issues */
4287
+ -webkit-perspective: 1000;
4288
+ perspective: 1000;
4289
+ }
4290
+
4291
+ /* Ensure all control elements use hardware acceleration */
4292
+ .uvf-control-btn,
4293
+ .uvf-progress-bar,
4294
+ .uvf-progress-section {
4295
+ -webkit-transform: translateZ(0);
4296
+ transform: translateZ(0);
4154
4297
  }
4155
4298
  }
4156
4299
  }
@@ -4211,14 +4354,45 @@ export class WebPlayer extends BasePlayer {
4211
4354
 
4212
4355
  /* Fix for controls being cut off by virtual keyboard */
4213
4356
  .uvf-controls-bar {
4214
- position: fixed !important;
4215
- bottom: var(--uvf-safe-area-bottom, 0) !important;
4357
+ position: absolute !important;
4358
+ bottom: 0 !important;
4359
+ left: 0 !important;
4360
+ right: 0 !important;
4361
+ /* Remove fixed positioning that causes issues on iOS Safari */
4362
+ z-index: 1000 !important;
4363
+ transform: translateZ(0); /* Force hardware acceleration */
4216
4364
  }
4217
4365
 
4218
4366
  /* Ensure controls stay above virtual keyboards */
4219
4367
  @supports (bottom: env(keyboard-inset-height)) {
4220
4368
  .uvf-controls-bar {
4221
- bottom: max(var(--uvf-safe-area-bottom, 0), env(keyboard-inset-height, 0)) !important;
4369
+ bottom: max(0px, env(keyboard-inset-height, 0)) !important;
4370
+ padding-bottom: calc(16px + max(var(--uvf-safe-area-bottom, 0), env(keyboard-inset-height, 0))) !important;
4371
+ }
4372
+ }
4373
+
4374
+ /* Hide PiP button on mobile - not supported on most mobile browsers */
4375
+ #uvf-pip-btn {
4376
+ display: none !important;
4377
+ }
4378
+
4379
+ /* Mobile fullscreen enhancements */
4380
+ .uvf-player-wrapper.uvf-fullscreen {
4381
+ /* Ensure fullscreen covers entire viewport on mobile */
4382
+ position: fixed !important;
4383
+ top: 0 !important;
4384
+ left: 0 !important;
4385
+ width: 100vw !important;
4386
+ height: 100vh !important;
4387
+ z-index: 2147483647 !important;
4388
+ background: #000 !important;
4389
+ }
4390
+
4391
+ /* iOS Safari specific fullscreen fixes */
4392
+ @supports (-webkit-appearance: none) {
4393
+ .uvf-player-wrapper.uvf-fullscreen {
4394
+ /* Use viewport units that work better with iOS Safari */
4395
+ height: -webkit-fill-available !important;
4222
4396
  }
4223
4397
  }
4224
4398
  }
@@ -4260,19 +4434,25 @@ export class WebPlayer extends BasePlayer {
4260
4434
  min-height: inherit;
4261
4435
  }
4262
4436
 
4263
- /* Enhanced mobile controls bar with safe area padding */
4437
+ /* Enhanced mobile controls bar with safe area padding - iOS Safari specific fixes */
4264
4438
  .uvf-controls-bar {
4265
- position: absolute;
4266
- bottom: 0;
4267
- left: 0;
4268
- right: 0;
4439
+ position: absolute !important;
4440
+ bottom: 0 !important;
4441
+ left: 0 !important;
4442
+ right: 0 !important;
4269
4443
  padding: 16px 12px;
4270
- padding-bottom: calc(16px + var(--uvf-safe-area-bottom));
4271
- padding-left: calc(12px + var(--uvf-safe-area-left));
4272
- padding-right: calc(12px + var(--uvf-safe-area-right));
4444
+ padding-bottom: calc(16px + var(--uvf-safe-area-bottom, 0px));
4445
+ padding-left: calc(12px + var(--uvf-safe-area-left, 0px));
4446
+ padding-right: calc(12px + var(--uvf-safe-area-right, 0px));
4273
4447
  background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 80%, var(--uvf-overlay-transparent) 100%);
4274
4448
  box-sizing: border-box;
4275
- z-index: 1000;
4449
+ z-index: 1000 !important;
4450
+ /* iOS Safari specific fixes */
4451
+ transform: translateZ(0);
4452
+ -webkit-transform: translateZ(0);
4453
+ will-change: transform;
4454
+ /* Ensure proper stacking */
4455
+ isolation: isolate;
4276
4456
  }
4277
4457
 
4278
4458
  .uvf-progress-section {
@@ -5775,12 +5955,18 @@ export class WebPlayer extends BasePlayer {
5775
5955
  epgBtn.style.display = 'none'; // Initially hidden, will be shown when EPG data is available
5776
5956
  rightControls.appendChild(epgBtn);
5777
5957
 
5778
- // PiP button
5958
+ // PiP button - only show on desktop/supported browsers
5779
5959
  const pipBtn = document.createElement('button');
5780
5960
  pipBtn.className = 'uvf-control-btn';
5781
5961
  pipBtn.id = 'uvf-pip-btn';
5782
5962
  pipBtn.title = 'Picture-in-Picture';
5783
5963
  pipBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
5964
+
5965
+ // Hide PiP button on mobile devices and browsers that don't support it
5966
+ if (this.isMobileDevice() || !this.isPipSupported()) {
5967
+ pipBtn.style.display = 'none';
5968
+ }
5969
+
5784
5970
  rightControls.appendChild(pipBtn);
5785
5971
 
5786
5972
  // Fullscreen button
@@ -6080,19 +6266,26 @@ export class WebPlayer extends BasePlayer {
6080
6266
  }
6081
6267
  });
6082
6268
 
6083
- // Fullscreen button with enhanced Brave support
6269
+ // Fullscreen button with enhanced cross-platform support
6084
6270
  fullscreenBtn?.addEventListener('click', (event) => {
6085
- // Enhanced debugging for Brave browser
6271
+ // Enhanced debugging for all platforms
6086
6272
  const isBrave = this.isBraveBrowser();
6087
6273
  const isPrivate = this.isPrivateWindow();
6274
+ const isIOS = this.isIOSDevice();
6275
+ const isAndroid = this.isAndroidDevice();
6276
+ const isMobile = this.isMobileDevice();
6088
6277
 
6089
6278
  this.debugLog('Fullscreen button clicked:', {
6090
6279
  isBrave,
6091
6280
  isPrivate,
6281
+ isIOS,
6282
+ isAndroid,
6283
+ isMobile,
6092
6284
  isFullscreen: this.isFullscreen(),
6093
6285
  eventTrusted: event.isTrusted,
6094
6286
  eventType: event.type,
6095
- timestamp: Date.now()
6287
+ timestamp: Date.now(),
6288
+ fullscreenSupported: this.isFullscreenSupported()
6096
6289
  });
6097
6290
 
6098
6291
  // Update user interaction timestamp
@@ -6109,22 +6302,28 @@ export class WebPlayer extends BasePlayer {
6109
6302
  } else {
6110
6303
  this.debugLog('Entering fullscreen via button');
6111
6304
 
6112
- // Special handling for Brave browser
6113
- if (isBrave && !isPrivate) {
6114
- // For Brave normal tabs, try direct API call first
6115
- this.enterFullscreenWithBraveSupport().catch(err => {
6116
- this.debugWarn('Brave fullscreen button failed:', err.message);
6117
- this.showTemporaryMessage('Brave Browser: Please allow fullscreen in site settings');
6118
- });
6119
- } else {
6120
- this.enterFullscreen().catch(err => {
6121
- this.debugWarn('Fullscreen button failed:', err.message);
6122
-
6123
- if (isBrave) {
6124
- this.showTemporaryMessage('Try refreshing the page or check Brave shields settings');
6125
- }
6126
- });
6305
+ // iOS Safari special message
6306
+ if (isIOS) {
6307
+ this.showShortcutIndicator('Using iOS video fullscreen');
6308
+ } else if (isAndroid) {
6309
+ this.showShortcutIndicator('Entering fullscreen - rotate to landscape');
6127
6310
  }
6311
+
6312
+ // Use enhanced cross-platform fullscreen method
6313
+ this.enterFullscreen().catch(err => {
6314
+ this.debugWarn('Fullscreen button failed:', err.message);
6315
+
6316
+ // Platform-specific error messages
6317
+ if (isIOS) {
6318
+ this.showTemporaryMessage('iOS: Use device rotation or video controls for fullscreen');
6319
+ } else if (isAndroid) {
6320
+ this.showTemporaryMessage('Android: Try rotating device to landscape mode');
6321
+ } else if (isBrave) {
6322
+ this.showTemporaryMessage('Brave Browser: Please allow fullscreen in site settings');
6323
+ } else {
6324
+ this.showTemporaryMessage('Fullscreen not supported in this browser');
6325
+ }
6326
+ });
6128
6327
  }
6129
6328
  });
6130
6329
 
@@ -6762,6 +6961,58 @@ export class WebPlayer extends BasePlayer {
6762
6961
  }
6763
6962
  }
6764
6963
 
6964
+ /**
6965
+ * Detect if user is on a mobile device
6966
+ */
6967
+ private isMobileDevice(): boolean {
6968
+ const userAgent = navigator.userAgent.toLowerCase();
6969
+ const mobileKeywords = ['android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone', 'mobile'];
6970
+ const isMobileUserAgent = mobileKeywords.some(keyword => userAgent.includes(keyword));
6971
+ const isSmallScreen = window.innerWidth <= 768;
6972
+ const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
6973
+
6974
+ return isMobileUserAgent || (isSmallScreen && hasTouchScreen);
6975
+ }
6976
+
6977
+ /**
6978
+ * Check if Picture-in-Picture is supported by the browser
6979
+ */
6980
+ private isPipSupported(): boolean {
6981
+ return !!(
6982
+ document.pictureInPictureEnabled &&
6983
+ HTMLVideoElement.prototype.requestPictureInPicture &&
6984
+ typeof HTMLVideoElement.prototype.requestPictureInPicture === 'function'
6985
+ );
6986
+ }
6987
+
6988
+ /**
6989
+ * Detect if user is on iOS device
6990
+ */
6991
+ private isIOSDevice(): boolean {
6992
+ const userAgent = navigator.userAgent.toLowerCase();
6993
+ return /iphone|ipad|ipod/.test(userAgent);
6994
+ }
6995
+
6996
+ /**
6997
+ * Detect if user is on Android device
6998
+ */
6999
+ private isAndroidDevice(): boolean {
7000
+ const userAgent = navigator.userAgent.toLowerCase();
7001
+ return /android/.test(userAgent);
7002
+ }
7003
+
7004
+ /**
7005
+ * Check if fullscreen is supported on current platform
7006
+ */
7007
+ private isFullscreenSupported(): boolean {
7008
+ return !!(
7009
+ document.fullscreenEnabled ||
7010
+ (document as any).webkitFullscreenEnabled ||
7011
+ (document as any).mozFullScreenEnabled ||
7012
+ (document as any).msFullscreenEnabled
7013
+ );
7014
+ }
7015
+
6765
7016
  private handleVolumeChange(e: MouseEvent): void {
6766
7017
  const slider = document.getElementById('uvf-volume-slider');
6767
7018
  if (!slider) return;