unified-video-framework 1.4.164 → 1.4.165

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.
@@ -51,6 +51,9 @@ export class WebPlayer extends BasePlayer {
51
51
  private currentPlaybackRate = 1;
52
52
  private isDragging: boolean = false;
53
53
 
54
+ // Settings menu state guard to keep controls interactive while open
55
+ private isSettingsOpen: boolean = false;
56
+
54
57
  // Settings configuration
55
58
  private settingsConfig = {
56
59
  enabled: true, // Show settings button
@@ -4398,278 +4401,61 @@ export class WebPlayer extends BasePlayer {
4398
4401
  }
4399
4402
 
4400
4403
  /* Enhanced Responsive Media Queries with UX Best Practices */
4401
- /* Mobile Portrait Layout - CENTERED PLAYER with TOP/BOTTOM BLACK AREAS */
4404
+ /* Mobile devices (portrait) - Enhanced UX with Safe Areas */
4402
4405
  @media screen and (max-width: 767px) and (orientation: portrait) {
4403
- /* CENTERED LAYOUT: 25% top black + 50% player + 25% bottom black */
4404
4406
  .uvf-responsive-container {
4405
- display: flex;
4406
- flex-direction: column;
4407
- height: 100vh;
4408
- height: 100dvh;
4409
- background: #000;
4410
- overflow: hidden;
4411
- position: fixed;
4412
- top: 0;
4413
- left: 0;
4414
- width: 100vw;
4415
4407
  padding: 0;
4408
+ width: 100vw !important;
4409
+ height: calc(100vh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
4416
4410
  margin: 0;
4411
+ position: relative;
4412
+ overflow: hidden;
4417
4413
  }
4418
4414
 
4419
- /* TOP BLACK AREA - 25% of viewport - COMPLETELY EMPTY */
4420
- .uvf-responsive-container::before {
4421
- content: '';
4422
- flex: 0 0 25vh;
4423
- background: #000;
4424
- pointer-events: none;
4425
- }
4426
-
4427
- /* BOTTOM BLACK AREA - 25% of viewport - COMPLETELY EMPTY */
4428
- .uvf-responsive-container::after {
4429
- content: '';
4430
- flex: 0 0 25vh;
4431
- background: #000;
4432
- pointer-events: none;
4415
+ @supports (height: 100dvh) {
4416
+ .uvf-responsive-container {
4417
+ height: calc(100dvh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
4418
+ }
4433
4419
  }
4434
4420
 
4435
- /* CENTERED VIDEO PLAYER WRAPPER - 50% of viewport */
4436
4421
  .uvf-responsive-container .uvf-player-wrapper {
4437
- flex: 1; /* Takes remaining 50% */
4438
- width: 100vw;
4439
- display: flex;
4440
- align-items: center;
4441
- justify-content: center;
4442
- background: #000;
4443
- position: relative;
4444
- overflow: hidden !important; /* CRITICAL: Ensure nothing extends beyond this area */
4445
- /* STRICT CONTAINMENT - Force all child elements within bounds */
4446
- contain: layout style paint size !important;
4447
- isolation: isolate !important;
4448
- /* Create strict clipping boundary */
4449
- clip: rect(0, 100vw, 50vh, 0) !important;
4422
+ width: 100vw !important;
4423
+ height: 100% !important;
4424
+ min-height: calc(100vh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
4450
4425
  }
4451
4426
 
4452
- /* Video container fills player wrapper */
4453
- .uvf-responsive-container .uvf-video-container {
4454
- width: 100%;
4455
- height: 100%;
4456
- position: relative;
4457
- background: #000;
4458
- border-radius: 0;
4459
- overflow: hidden;
4427
+ @supports (height: 100dvh) {
4428
+ .uvf-responsive-container .uvf-player-wrapper {
4429
+ min-height: calc(100dvh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
4430
+ }
4460
4431
  }
4461
4432
 
4462
- /* Video element fills container */
4463
- .uvf-responsive-container .uvf-video {
4464
- width: 100%;
4465
- height: 100%;
4466
- object-fit: contain;
4467
- background: #000;
4433
+ .uvf-responsive-container .uvf-video-container {
4434
+ width: 100vw !important;
4435
+ height: 100% !important;
4436
+ aspect-ratio: unset !important;
4437
+ min-height: inherit;
4468
4438
  }
4469
4439
 
4470
- /* CONTROLS STRICTLY CONTAINED WITHIN VIDEO AREA - NEVER EXTEND TO BLACK AREAS */
4440
+ /* Enhanced mobile controls bar with safe area padding - iOS Safari specific fixes */
4471
4441
  .uvf-controls-bar {
4472
4442
  position: absolute !important;
4473
- /* Keep controls INSIDE video container with margins from all edges */
4474
- bottom: 20px !important; /* 20px margin from video bottom */
4475
- left: 16px !important; /* 16px margin from video left */
4476
- right: 16px !important; /* 16px margin from video right */
4477
- top: auto !important;
4478
- background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.7) 60%, transparent 100%);
4479
- padding: 12px 16px;
4480
- border-radius: 12px;
4481
- z-index: 1000;
4482
- /* Ensure controls are visible and contained */
4483
- opacity: 1 !important;
4484
- visibility: visible !important;
4485
- display: flex !important;
4486
- flex-direction: column !important;
4487
- /* Hardware acceleration */
4488
- -webkit-transform: translate3d(0,0,0);
4489
- transform: translate3d(0,0,0);
4490
- /* CRITICAL: Prevent any overflow into black areas */
4491
- max-height: calc(100% - 40px) !important; /* Leave 20px margin from top and bottom */
4492
- max-width: calc(100% - 32px) !important; /* Leave 16px margin from left and right */
4493
- box-sizing: border-box !important;
4494
- /* Visual containment indicators */
4495
- border: 1px solid rgba(255, 255, 255, 0.1);
4496
- backdrop-filter: blur(10px);
4497
- /* STRICT CSS containment with size to prevent overflow */
4498
- contain: layout style paint size !important;
4499
- /* Absolute overflow prevention */
4500
- overflow: hidden !important;
4501
- /* Force clip to container bounds */
4502
- clip-path: inset(0) !important;
4503
- }
4504
-
4505
- /* Force controls and all child elements to stay within video container */
4506
- .uvf-responsive-container .uvf-controls-bar {
4507
- opacity: 1 !important;
4508
- visibility: visible !important;
4509
- transform: translateY(0) !important;
4510
- pointer-events: auto !important;
4511
- }
4512
-
4513
- /* CRITICAL: Prevent ALL child elements from extending beyond controls container */
4514
- .uvf-responsive-container .uvf-controls-bar *,
4515
- .uvf-responsive-container .uvf-controls-bar *::before,
4516
- .uvf-responsive-container .uvf-controls-bar *::after {
4517
- max-width: 100% !important;
4518
- max-height: 100% !important;
4519
- overflow: hidden !important;
4520
- box-sizing: border-box !important;
4521
- position: relative !important;
4522
- }
4523
-
4524
- /* Ensure settings menu stays within video container bounds */
4525
- .uvf-responsive-container .uvf-settings-menu {
4526
- max-height: calc(50vh - 80px) !important; /* Video height minus controls margin */
4527
- max-width: calc(100vw - 64px) !important; /* Video width minus side margins */
4528
- overflow-y: auto !important;
4529
- position: absolute !important;
4530
- /* Keep menu within video boundaries */
4531
- bottom: 60px !important;
4443
+ bottom: 0 !important;
4444
+ left: 0 !important;
4532
4445
  right: 0 !important;
4533
- left: auto !important;
4534
- top: auto !important;
4535
- /* Ensure no child elements extend beyond container */
4536
- contain: layout style paint;
4537
- }
4538
-
4539
- /* Ensure all child elements of controls stay within bounds */
4540
- .uvf-responsive-container .uvf-controls-bar * {
4541
- max-width: 100%;
4446
+ padding: 16px 12px;
4447
+ padding-bottom: calc(16px + var(--uvf-safe-area-bottom, 0px));
4448
+ padding-left: calc(12px + var(--uvf-safe-area-left, 0px));
4449
+ padding-right: calc(12px + var(--uvf-safe-area-right, 0px));
4450
+ background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 80%, var(--uvf-overlay-transparent) 100%);
4542
4451
  box-sizing: border-box;
4543
- }
4544
-
4545
- /* Progress section contained within controls */
4546
- .uvf-progress-section {
4547
- width: 100%;
4548
- margin-bottom: 8px;
4549
- opacity: 1 !important;
4550
- visibility: visible !important;
4551
- display: block !important;
4552
- }
4553
-
4554
- /* Progress bar styling */
4555
- .uvf-progress-bar-wrapper {
4556
- opacity: 1 !important;
4557
- visibility: visible !important;
4558
- }
4559
-
4560
- .uvf-progress-bar {
4561
- height: 4px;
4562
- background: rgba(255, 255, 255, 0.3);
4563
- border-radius: 2px;
4564
- }
4565
-
4566
- .uvf-progress-filled {
4567
- background: var(--uvf-accent-1, #8B5CF6);
4568
- height: 100%;
4569
- border-radius: 2px;
4570
- }
4571
-
4572
- /* Controls row alignment - ensure visibility */
4573
- .uvf-controls-row {
4574
- width: 100%;
4575
- display: flex !important;
4576
- align-items: center;
4577
- justify-content: flex-start;
4578
- gap: 12px;
4579
- opacity: 1 !important;
4580
- visibility: visible !important;
4581
- margin-top: 8px;
4582
- }
4583
-
4584
- /* Time display visibility */
4585
- .uvf-time-display {
4586
- color: #fff;
4587
- font-size: 12px;
4588
- font-weight: 500;
4589
- opacity: 1 !important;
4590
- visibility: visible !important;
4591
- display: block !important;
4592
- }
4593
-
4594
- /* Control buttons visibility */
4595
- .uvf-control-btn {
4596
- min-width: 44px;
4597
- min-height: 44px;
4598
- display: flex !important;
4599
- align-items: center;
4600
- justify-content: center;
4601
- border-radius: 50%;
4602
- background: rgba(255, 255, 255, 0.1);
4603
- backdrop-filter: blur(8px);
4604
- color: #fff;
4605
- opacity: 1 !important;
4606
- visibility: visible !important;
4607
- transition: all 0.2s ease;
4608
- }
4609
-
4610
- .uvf-control-btn:active {
4611
- transform: scale(0.95);
4612
- background: rgba(255, 255, 255, 0.2);
4613
- }
4614
-
4615
- /* Play/pause button prominence */
4616
- .uvf-control-btn.play-pause {
4617
- background: linear-gradient(135deg, var(--uvf-accent-1), var(--uvf-accent-2));
4618
- min-width: 52px;
4619
- min-height: 52px;
4620
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
4621
- }
4622
-
4623
- .uvf-control-btn.play-pause:active {
4624
- transform: scale(0.92);
4625
- box-shadow: 0 1px 4px rgba(0,0,0,0.4);
4626
- }
4627
-
4628
- .uvf-control-btn svg {
4629
- fill: #fff;
4630
- opacity: 1;
4631
- }
4632
-
4633
- /* Right controls */
4634
- .uvf-right-controls {
4635
- margin-left: auto;
4636
- display: flex !important;
4637
- align-items: center;
4638
- gap: 8px;
4639
- opacity: 1 !important;
4640
- visibility: visible !important;
4641
- }
4642
-
4643
- /* Center play button positioned within video */
4644
- .uvf-center-play-container {
4645
- position: absolute;
4646
- top: 50%;
4647
- left: 50%;
4648
- transform: translate(-50%, -50%);
4649
- z-index: 8;
4650
- pointer-events: none;
4651
- }
4652
-
4653
- .uvf-center-play-btn {
4654
- pointer-events: auto;
4655
- }
4656
-
4657
- /* Top controls within video */
4658
- .uvf-top-controls {
4659
- position: absolute;
4660
- top: calc(12px + var(--uvf-safe-area-top));
4661
- right: calc(16px + var(--uvf-safe-area-right));
4662
- z-index: 9;
4663
- }
4664
-
4665
- /* Title bar within video */
4666
- .uvf-title-bar {
4667
- position: absolute;
4668
- top: calc(12px + var(--uvf-safe-area-top));
4669
- left: calc(16px + var(--uvf-safe-area-left));
4670
- right: calc(80px + var(--uvf-safe-area-right));
4671
- z-index: 9;
4672
- padding: 8px 0;
4452
+ z-index: 1000 !important;
4453
+ /* iOS Safari specific fixes */
4454
+ transform: translateZ(0);
4455
+ -webkit-transform: translateZ(0);
4456
+ will-change: transform;
4457
+ /* Ensure proper stacking */
4458
+ isolation: isolate;
4673
4459
  }
4674
4460
 
4675
4461
  .uvf-progress-section {
@@ -6158,7 +5944,7 @@ export class WebPlayer extends BasePlayer {
6158
5944
  this.debugLog('Settings button NOT created - settings disabled');
6159
5945
  }
6160
5946
 
6161
- // EPG button (Electronic Program Guide) - Only show if EPG data is available
5947
+ // EPG button (Electronic Program Guide)
6162
5948
  const epgBtn = document.createElement('button');
6163
5949
  epgBtn.className = 'uvf-control-btn';
6164
5950
  epgBtn.id = 'uvf-epg-btn';
@@ -6169,17 +5955,8 @@ export class WebPlayer extends BasePlayer {
6169
5955
  <rect x="19" y="3" width="2" height="2"/>
6170
5956
  <path d="M17 1v2h2V1h2v2h1c.55 0 1 .45 1 1v16c0 .55-.45 1-1 1H2c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h1V1h2v2h12z" fill="none" stroke="currentColor" stroke-width="0.5"/>
6171
5957
  </svg>`;
6172
- // CRITICAL: EPG button should ONLY be visible when EPG data is available
6173
- epgBtn.style.display = 'none !important'; // Force hidden - will only show when EPG data is loaded
6174
-
6175
- // Only add to controls if EPG functionality is enabled and data will be available
6176
- const epgConfig = (this.config as any).epg;
6177
- if (epgConfig && epgConfig.enabled) {
6178
- rightControls.appendChild(epgBtn);
6179
- this.debugLog('EPG button created but hidden - will show when EPG data is loaded');
6180
- } else {
6181
- this.debugLog('EPG button not created - EPG functionality disabled');
6182
- }
5958
+ epgBtn.style.display = 'none'; // Initially hidden, will be shown when EPG data is available
5959
+ rightControls.appendChild(epgBtn);
6183
5960
 
6184
5961
  // PiP button - only show on desktop/supported browsers
6185
5962
  const pipBtn = document.createElement('button');
@@ -6617,7 +6394,12 @@ export class WebPlayer extends BasePlayer {
6617
6394
 
6618
6395
  // Force visibility if menu is active, hide if not active
6619
6396
  if (settingsMenu) {
6620
- if (settingsMenu.classList.contains('active')) {
6397
+ const activating = settingsMenu.classList.contains('active');
6398
+ if (activating) {
6399
+ this.isSettingsOpen = true;
6400
+ // Keep controls visible and interactive while menu is open
6401
+ this.showControls();
6402
+ if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
6621
6403
  settingsMenu.style.display = 'block';
6622
6404
  settingsMenu.style.visibility = 'visible';
6623
6405
  settingsMenu.style.opacity = '1';
@@ -6633,10 +6415,13 @@ export class WebPlayer extends BasePlayer {
6633
6415
  settingsMenu.style.padding = '10px 0';
6634
6416
  this.debugLog('Applied fallback styles to show menu');
6635
6417
  } else {
6418
+ this.isSettingsOpen = false;
6636
6419
  settingsMenu.style.display = 'none';
6637
6420
  settingsMenu.style.visibility = 'hidden';
6638
6421
  settingsMenu.style.opacity = '0';
6639
6422
  this.debugLog('Applied fallback styles to hide menu');
6423
+ // After closing, allow auto-hide again if playing
6424
+ this.scheduleHideControls();
6640
6425
  }
6641
6426
  }
6642
6427
 
@@ -6676,6 +6461,19 @@ export class WebPlayer extends BasePlayer {
6676
6461
  }
6677
6462
  });
6678
6463
 
6464
+ // Keep controls active while interacting with settings menu
6465
+ if (settingsMenu) {
6466
+ const keepAlive = () => {
6467
+ if (!this.isSettingsOpen) return;
6468
+ this.showControls();
6469
+ if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
6470
+ };
6471
+ settingsMenu.addEventListener('mouseenter', keepAlive);
6472
+ settingsMenu.addEventListener('mousemove', keepAlive);
6473
+ settingsMenu.addEventListener('touchstart', keepAlive, { passive: true } as any);
6474
+ settingsMenu.addEventListener('touchmove', keepAlive, { passive: true } as any);
6475
+ }
6476
+
6679
6477
  // Add Escape key handler for settings menu
6680
6478
  document.addEventListener('keydown', (e) => {
6681
6479
  if (e.key === 'Escape' && settingsMenu?.classList.contains('active')) {
@@ -7327,6 +7125,8 @@ export class WebPlayer extends BasePlayer {
7327
7125
 
7328
7126
  private hideControls(): void {
7329
7127
  if (!this.state.isPlaying) return;
7128
+ // Never hide controls while settings menu is open
7129
+ if (this.isSettingsOpen) return;
7330
7130
 
7331
7131
  const wrapper = this.container?.querySelector('.uvf-player-wrapper');
7332
7132
  if (wrapper) {
@@ -7337,12 +7137,14 @@ export class WebPlayer extends BasePlayer {
7337
7137
 
7338
7138
  private scheduleHideControls(): void {
7339
7139
  if (!this.state.isPlaying) return;
7140
+ // Do not schedule auto-hide while settings are open
7141
+ if (this.isSettingsOpen) return;
7340
7142
 
7341
7143
  if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
7342
7144
  // Use longer timeout in fullscreen for better UX
7343
7145
  const timeout = this.isFullscreen() ? 4000 : 3000;
7344
7146
  this.hideControlsTimeout = setTimeout(() => {
7345
- if (this.state.isPlaying && !this.controlsContainer?.matches(':hover')) {
7147
+ if (this.state.isPlaying && !this.controlsContainer?.matches(':hover') && !this.isSettingsOpen) {
7346
7148
  this.hideControls();
7347
7149
  }
7348
7150
  }, timeout);
@@ -8551,6 +8353,7 @@ export class WebPlayer extends BasePlayer {
8551
8353
  if (!settingsMenu) return;
8552
8354
 
8553
8355
  settingsMenu.classList.remove('active');
8356
+ this.isSettingsOpen = false;
8554
8357
 
8555
8358
  // Apply fallback styles to ensure menu is hidden
8556
8359
  settingsMenu.style.display = 'none';
@@ -8563,6 +8366,8 @@ export class WebPlayer extends BasePlayer {
8563
8366
  });
8564
8367
 
8565
8368
  this.debugLog('Settings menu hidden via hideSettingsMenu()');
8369
+ // Resume auto-hide if appropriate
8370
+ this.scheduleHideControls();
8566
8371
  }
8567
8372
 
8568
8373
  /**
@@ -9143,9 +8948,8 @@ export class WebPlayer extends BasePlayer {
9143
8948
  public showEPGButton(): void {
9144
8949
  const epgBtn = document.getElementById('uvf-epg-btn');
9145
8950
  if (epgBtn) {
9146
- // Remove any forced display none and set to block
9147
- epgBtn.style.setProperty('display', 'block', 'important');
9148
- this.debugLog('EPG button shown with !important override');
8951
+ epgBtn.style.display = 'block';
8952
+ this.debugLog('EPG button shown');
9149
8953
  } else {
9150
8954
  this.debugLog('EPG button not found in DOM');
9151
8955
  }
@@ -9157,8 +8961,8 @@ export class WebPlayer extends BasePlayer {
9157
8961
  public hideEPGButton(): void {
9158
8962
  const epgBtn = document.getElementById('uvf-epg-btn');
9159
8963
  if (epgBtn) {
9160
- epgBtn.style.setProperty('display', 'none', 'important');
9161
- this.debugLog('EPG button hidden with !important');
8964
+ epgBtn.style.display = 'none';
8965
+ this.debugLog('EPG button hidden');
9162
8966
  }
9163
8967
  }
9164
8968
 
@@ -9183,30 +8987,7 @@ export class WebPlayer extends BasePlayer {
9183
8987
  */
9184
8988
  public isEPGButtonVisible(): boolean {
9185
8989
  const epgBtn = document.getElementById('uvf-epg-btn');
9186
- if (epgBtn) {
9187
- const isVisible = epgBtn.style.display !== 'none';
9188
- this.debugLog(`EPG button visibility check: ${isVisible}, display style: ${epgBtn.style.display}`);
9189
- return isVisible;
9190
- }
9191
- this.debugLog('EPG button visibility check: false (button not found in DOM)');
9192
- return false;
9193
- }
9194
-
9195
- /**
9196
- * Debug method to log current EPG button state and configuration
9197
- */
9198
- public debugEPGState(): void {
9199
- const epgBtn = document.getElementById('uvf-epg-btn');
9200
- const epgConfig = (this.config as any).epg;
9201
- this.debugLog('=== EPG DEBUG STATE ===');
9202
- this.debugLog(`EPG Config Enabled: ${epgConfig?.enabled || false}`);
9203
- this.debugLog(`EPG Button Exists: ${!!epgBtn}`);
9204
- if (epgBtn) {
9205
- this.debugLog(`EPG Button Display Style: ${epgBtn.style.display}`);
9206
- this.debugLog(`EPG Button Computed Display: ${window.getComputedStyle(epgBtn).display}`);
9207
- this.debugLog(`EPG Button Visible: ${this.isEPGButtonVisible()}`);
9208
- }
9209
- this.debugLog('=====================');
8990
+ return epgBtn ? epgBtn.style.display !== 'none' : false;
9210
8991
  }
9211
8992
 
9212
8993
  private async cleanup(): Promise<void> {