unified-video-framework 1.4.147 → 1.4.149

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.
@@ -83,6 +83,9 @@ export class WebPlayer extends BasePlayer {
83
83
  // Fullscreen fallback tracking
84
84
  private hasTriedButtonFallback: boolean = false;
85
85
  private lastUserInteraction: number = 0;
86
+
87
+ // Progress bar tooltip state
88
+ private showTimeTooltip: boolean = false;
86
89
 
87
90
  // Debug logging helper
88
91
  private debugLog(message: string, ...args: any[]): void {
@@ -153,8 +156,9 @@ export class WebPlayer extends BasePlayer {
153
156
  this.video = document.createElement('video');
154
157
  this.video.className = 'uvf-video';
155
158
  this.video.controls = false; // We'll use custom controls
159
+ // For autoplay to work, video must be muted in most browsers
156
160
  this.video.autoplay = this.config.autoPlay ?? false;
157
- this.video.muted = this.config.muted ?? false;
161
+ this.video.muted = this.config.autoPlay ? true : (this.config.muted ?? false);
158
162
  this.video.loop = this.config.loop ?? false;
159
163
  this.video.playsInline = this.config.playsInline ?? true;
160
164
  this.video.preload = this.config.preload ?? 'metadata';
@@ -193,6 +197,7 @@ export class WebPlayer extends BasePlayer {
193
197
  this.setupKeyboardShortcuts();
194
198
  this.setupWatermark();
195
199
  this.setupFullscreenListeners();
200
+ this.setupUserInteractionTracking();
196
201
 
197
202
  // Initialize paywall controller if provided
198
203
  try {
@@ -518,7 +523,15 @@ export class WebPlayer extends BasePlayer {
518
523
 
519
524
  // Start playback if autoPlay is enabled
520
525
  if (this.config.autoPlay) {
521
- this.play();
526
+ // Attempt autoplay, but handle gracefully if blocked
527
+ this.play().catch(error => {
528
+ if (this.isAutoplayRestrictionError(error)) {
529
+ this.debugWarn('HLS autoplay blocked, showing play overlay');
530
+ this.showPlayOverlay();
531
+ } else {
532
+ this.debugError('HLS autoplay failed:', error);
533
+ }
534
+ });
522
535
  }
523
536
  });
524
537
 
@@ -681,6 +694,190 @@ export class WebPlayer extends BasePlayer {
681
694
  );
682
695
  }
683
696
 
697
+ private isAutoplayRestrictionError(err: any): boolean {
698
+ if (!err) return false;
699
+
700
+ const message = (err.message || '').toLowerCase();
701
+ const name = (err.name || '').toLowerCase();
702
+
703
+ // Common autoplay restriction error patterns
704
+ return (
705
+ name === 'notallowederror' ||
706
+ message.includes('user didn\'t interact') ||
707
+ message.includes('autoplay') ||
708
+ message.includes('gesture') ||
709
+ message.includes('user activation') ||
710
+ message.includes('play() failed') ||
711
+ message.includes('user interaction')
712
+ );
713
+ }
714
+
715
+ private showPlayOverlay(): void {
716
+ // Remove existing overlay
717
+ this.hidePlayOverlay();
718
+
719
+ const overlay = document.createElement('div');
720
+ overlay.id = 'uvf-play-overlay';
721
+ overlay.className = 'uvf-play-overlay';
722
+
723
+ const playButton = document.createElement('button');
724
+ playButton.className = 'uvf-play-button';
725
+ playButton.innerHTML = `
726
+ <svg viewBox="0 0 24 24" fill="currentColor">
727
+ <path d="M8 5v14l11-7z"/>
728
+ </svg>
729
+ `;
730
+
731
+ const message = document.createElement('div');
732
+ message.className = 'uvf-play-message';
733
+ message.textContent = 'Click to play';
734
+
735
+ overlay.appendChild(playButton);
736
+ overlay.appendChild(message);
737
+
738
+ // Add click handler
739
+ playButton.addEventListener('click', async (e) => {
740
+ e.stopPropagation();
741
+ this.lastUserInteraction = Date.now();
742
+
743
+ try {
744
+ await this.play();
745
+ } catch (error) {
746
+ this.debugError('Failed to play after user interaction:', error);
747
+ }
748
+ });
749
+
750
+ // Also allow clicking anywhere on overlay
751
+ overlay.addEventListener('click', async (e) => {
752
+ if (e.target === overlay) {
753
+ e.stopPropagation();
754
+ this.lastUserInteraction = Date.now();
755
+
756
+ try {
757
+ await this.play();
758
+ } catch (error) {
759
+ this.debugError('Failed to play after user interaction:', error);
760
+ }
761
+ }
762
+ });
763
+
764
+ // Add styles
765
+ const style = document.createElement('style');
766
+ style.textContent = `
767
+ .uvf-play-overlay {
768
+ position: absolute;
769
+ top: 0;
770
+ left: 0;
771
+ width: 100%;
772
+ height: 100%;
773
+ background: rgba(0, 0, 0, 0.7);
774
+ display: flex;
775
+ flex-direction: column;
776
+ justify-content: center;
777
+ align-items: center;
778
+ z-index: 1000;
779
+ cursor: pointer;
780
+ }
781
+
782
+ .uvf-play-button {
783
+ width: 80px;
784
+ height: 80px;
785
+ border-radius: 50%;
786
+ background: rgba(255, 255, 255, 0.9);
787
+ border: none;
788
+ color: #000;
789
+ cursor: pointer;
790
+ display: flex;
791
+ align-items: center;
792
+ justify-content: center;
793
+ transition: all 0.3s ease;
794
+ margin-bottom: 16px;
795
+ }
796
+
797
+ .uvf-play-button:hover {
798
+ background: #fff;
799
+ transform: scale(1.1);
800
+ }
801
+
802
+ .uvf-play-button svg {
803
+ width: 32px;
804
+ height: 32px;
805
+ margin-left: 4px;
806
+ }
807
+
808
+ .uvf-play-message {
809
+ color: white;
810
+ font-size: 16px;
811
+ font-weight: 500;
812
+ text-align: center;
813
+ opacity: 0.9;
814
+ }
815
+ `;
816
+
817
+ // Add to page if not already added
818
+ if (!document.getElementById('uvf-play-overlay-styles')) {
819
+ style.id = 'uvf-play-overlay-styles';
820
+ document.head.appendChild(style);
821
+ }
822
+
823
+ // Add to player
824
+ if (this.playerWrapper) {
825
+ this.playerWrapper.appendChild(overlay);
826
+ }
827
+ }
828
+
829
+ private hidePlayOverlay(): void {
830
+ const overlay = document.getElementById('uvf-play-overlay');
831
+ if (overlay) {
832
+ overlay.remove();
833
+ }
834
+ }
835
+
836
+ private updateTimeTooltip(e: MouseEvent): void {
837
+ const progressBar = document.getElementById('uvf-progress-bar');
838
+ const tooltip = document.getElementById('uvf-time-tooltip');
839
+ if (!progressBar || !tooltip || !this.video) return;
840
+
841
+ const rect = progressBar.getBoundingClientRect();
842
+ const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
843
+ const percent = (x / rect.width);
844
+ const time = percent * this.video.duration;
845
+
846
+ // Update tooltip content and position
847
+ tooltip.textContent = this.formatTime(time);
848
+ tooltip.style.left = `${x}px`;
849
+ tooltip.classList.add('visible');
850
+ }
851
+
852
+ private hideTimeTooltip(): void {
853
+ const tooltip = document.getElementById('uvf-time-tooltip');
854
+ if (tooltip) {
855
+ tooltip.classList.remove('visible');
856
+ }
857
+ }
858
+
859
+ private setupUserInteractionTracking(): void {
860
+ // Track various user interactions to enable autoplay
861
+ const interactionEvents = ['click', 'mousedown', 'keydown', 'touchstart'];
862
+
863
+ const updateLastInteraction = () => {
864
+ this.lastUserInteraction = Date.now();
865
+ this.debugLog('User interaction detected at:', this.lastUserInteraction);
866
+ };
867
+
868
+ // Listen on document for global interactions
869
+ interactionEvents.forEach(eventType => {
870
+ document.addEventListener(eventType, updateLastInteraction, { passive: true });
871
+ });
872
+
873
+ // Also listen on player wrapper for more specific interactions
874
+ if (this.playerWrapper) {
875
+ interactionEvents.forEach(eventType => {
876
+ this.playerWrapper!.addEventListener(eventType, updateLastInteraction, { passive: true });
877
+ });
878
+ }
879
+ }
880
+
684
881
  async play(): Promise<void> {
685
882
  if (!this.video) throw new Error('Video element not initialized');
686
883
 
@@ -710,6 +907,8 @@ export class WebPlayer extends BasePlayer {
710
907
  this.video.pause();
711
908
  }
712
909
 
910
+ // Hide the play overlay if it exists
911
+ this.hidePlayOverlay();
713
912
  await super.play();
714
913
  } catch (err) {
715
914
  this._playPromise = null;
@@ -717,6 +916,14 @@ export class WebPlayer extends BasePlayer {
717
916
  // Benign: pause() raced play(); ignore the error.
718
917
  return;
719
918
  }
919
+
920
+ // Check if this is an autoplay restriction error
921
+ if (this.isAutoplayRestrictionError(err)) {
922
+ this.debugWarn('Autoplay blocked by browser policy - showing play overlay');
923
+ this.showPlayOverlay();
924
+ return; // Don't throw error for autoplay restrictions
925
+ }
926
+
720
927
  this.handleError({
721
928
  code: 'PLAY_ERROR',
722
929
  message: `Failed to start playbook: ${err}`,
@@ -2031,10 +2238,22 @@ export class WebPlayer extends BasePlayer {
2031
2238
  width: 100%;
2032
2239
  position: relative;
2033
2240
  cursor: pointer;
2034
- padding: 16px 0;
2241
+ padding: 6px 0;
2035
2242
  overflow: visible;
2036
2243
  }
2037
2244
 
2245
+ /* Extended touch area for better mobile UX without affecting visual spacing */
2246
+ .uvf-progress-bar-wrapper::before {
2247
+ content: '';
2248
+ position: absolute;
2249
+ top: -8px;
2250
+ bottom: -8px;
2251
+ left: 0;
2252
+ right: 0;
2253
+ cursor: pointer;
2254
+ z-index: 10;
2255
+ }
2256
+
2038
2257
  .uvf-progress-bar {
2039
2258
  width: 100%;
2040
2259
  height: 2px;
@@ -2115,30 +2334,102 @@ export class WebPlayer extends BasePlayer {
2115
2334
  left: 0;
2116
2335
  height: 100%;
2117
2336
  background: linear-gradient(90deg,
2118
- #ff4500 0%,
2119
- #ff5722 25%,
2120
- #ff6b35 50%,
2121
- #ff7043 75%,
2122
- #ff8c69 100%
2337
+ var(--uvf-accent-1, #ff4500) 0%,
2338
+ var(--uvf-accent-1, #ff5722) 25%,
2339
+ var(--uvf-accent-2, #ff6b35) 50%,
2340
+ var(--uvf-accent-2, #ff7043) 75%,
2341
+ var(--uvf-accent-2, #ff8c69) 100%
2123
2342
  );
2124
2343
  border-radius: 4px;
2125
2344
  pointer-events: none;
2126
2345
  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
2127
2346
  z-index: 2;
2128
- box-shadow: 0 0 12px rgba(255, 87, 34, 0.3);
2347
+ box-shadow: 0 0 12px var(--uvf-accent-1-20, rgba(255, 87, 34, 0.3));
2129
2348
  }
2130
2349
 
2131
2350
  .uvf-progress-bar-wrapper:hover .uvf-progress-filled {
2132
2351
  border-radius: 6px;
2133
2352
  background: linear-gradient(90deg,
2134
- #ff4500 0%,
2135
- #ff5722 20%,
2136
- #ff6b35 40%,
2137
- #ff7043 60%,
2138
- #ff8c69 80%,
2139
- #ffa500 100%
2353
+ var(--uvf-accent-1, #ff4500) 0%,
2354
+ var(--uvf-accent-1, #ff5722) 20%,
2355
+ var(--uvf-accent-2, #ff6b35) 40%,
2356
+ var(--uvf-accent-2, #ff7043) 60%,
2357
+ var(--uvf-accent-2, #ff8c69) 80%,
2358
+ var(--uvf-accent-2, #ffa500) 100%
2140
2359
  );
2141
- box-shadow: 0 0 20px rgba(255, 87, 34, 0.5);
2360
+ box-shadow: 0 0 20px var(--uvf-accent-1-20, rgba(255, 87, 34, 0.5));
2361
+ }
2362
+
2363
+ /* Progress Bar Handle/Thumb */
2364
+ .uvf-progress-handle {
2365
+ position: absolute;
2366
+ top: 50%;
2367
+ left: 0;
2368
+ width: 14px;
2369
+ height: 14px;
2370
+ background: #fff;
2371
+ border: 2px solid var(--uvf-accent-1, #ff5722);
2372
+ border-radius: 50%;
2373
+ transform: translate(-50%, -50%);
2374
+ cursor: grab;
2375
+ opacity: 0;
2376
+ transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
2377
+ z-index: 3;
2378
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
2379
+ }
2380
+
2381
+ .uvf-progress-bar-wrapper:hover .uvf-progress-handle {
2382
+ opacity: 1;
2383
+ transform: translate(-50%, -50%) scale(1);
2384
+ }
2385
+
2386
+ .uvf-progress-handle:hover {
2387
+ transform: translate(-50%, -50%) scale(1.2);
2388
+ box-shadow: 0 3px 12px rgba(0, 0, 0, 0.4);
2389
+ }
2390
+
2391
+ .uvf-progress-handle:active,
2392
+ .uvf-progress-handle.dragging {
2393
+ cursor: grabbing;
2394
+ transform: translate(-50%, -50%) scale(1.3);
2395
+ box-shadow: 0 4px 16px rgba(255, 87, 34, 0.4);
2396
+ }
2397
+
2398
+ /* Time Tooltip */
2399
+ .uvf-time-tooltip {
2400
+ position: absolute;
2401
+ bottom: 100%;
2402
+ left: 0;
2403
+ margin-bottom: 8px;
2404
+ padding: 4px 8px;
2405
+ background: rgba(0, 0, 0, 0.8);
2406
+ color: #fff;
2407
+ font-size: 12px;
2408
+ font-weight: 500;
2409
+ border-radius: 4px;
2410
+ white-space: nowrap;
2411
+ opacity: 0;
2412
+ transform: translateX(-50%) translateY(4px);
2413
+ transition: all 0.2s ease;
2414
+ pointer-events: none;
2415
+ z-index: 20;
2416
+ backdrop-filter: blur(4px);
2417
+ border: 1px solid rgba(255, 255, 255, 0.1);
2418
+ }
2419
+
2420
+ .uvf-time-tooltip::after {
2421
+ content: '';
2422
+ position: absolute;
2423
+ top: 100%;
2424
+ left: 50%;
2425
+ transform: translateX(-50%);
2426
+ border: 4px solid transparent;
2427
+ border-top-color: rgba(0, 0, 0, 0.8);
2428
+ }
2429
+
2430
+ .uvf-time-tooltip.visible {
2431
+ opacity: 1;
2432
+ transform: translateX(-50%) translateY(0);
2142
2433
  }
2143
2434
 
2144
2435
 
@@ -2146,7 +2437,7 @@ export class WebPlayer extends BasePlayer {
2146
2437
  /* Mobile responsive design with enhanced touch targets */
2147
2438
  @media (max-width: 768px) {
2148
2439
  .uvf-progress-bar-wrapper {
2149
- padding: 20px 0; /* Larger touch area */
2440
+ padding: 8px 0; /* Optimized touch area */
2150
2441
  }
2151
2442
 
2152
2443
  .uvf-progress-bar {
@@ -4788,7 +5079,9 @@ export class WebPlayer extends BasePlayer {
4788
5079
  <div class="uvf-progress-bar">
4789
5080
  <div class="uvf-progress-buffered" id="uvf-progress-buffered"></div>
4790
5081
  <div class="uvf-progress-filled" id="uvf-progress-filled"></div>
5082
+ <div class="uvf-progress-handle" id="uvf-progress-handle"></div>
4791
5083
  </div>
5084
+ <div class="uvf-time-tooltip" id="uvf-time-tooltip">00:00</div>
4792
5085
  `;
4793
5086
  progressSection.appendChild(progressBar);
4794
5087
 
@@ -5145,7 +5438,7 @@ export class WebPlayer extends BasePlayer {
5145
5438
  });
5146
5439
 
5147
5440
 
5148
- // Progress bar click
5441
+ // Progress bar interactions
5149
5442
  progressBar?.addEventListener('click', (e) => {
5150
5443
  this.handleProgressChange(e as MouseEvent);
5151
5444
  });
@@ -5155,7 +5448,31 @@ export class WebPlayer extends BasePlayer {
5155
5448
  this.handleProgressChange(e as MouseEvent);
5156
5449
  });
5157
5450
 
5158
- // Global mouse events for enhanced dragging
5451
+ // Hover tooltip functionality
5452
+ progressBar?.addEventListener('mouseenter', () => {
5453
+ this.showTimeTooltip = true;
5454
+ });
5455
+
5456
+ progressBar?.addEventListener('mouseleave', () => {
5457
+ this.showTimeTooltip = false;
5458
+ this.hideTimeTooltip();
5459
+ });
5460
+
5461
+ progressBar?.addEventListener('mousemove', (e) => {
5462
+ if (!this.isDragging && this.showTimeTooltip) {
5463
+ this.updateTimeTooltip(e as MouseEvent);
5464
+ }
5465
+ });
5466
+
5467
+ // Touch support for mobile devices
5468
+ progressBar?.addEventListener('touchstart', (e) => {
5469
+ e.preventDefault(); // Prevent scrolling
5470
+ this.isDragging = true;
5471
+ const touch = e.touches[0];
5472
+ this.handleProgressChange(touch);
5473
+ }, { passive: false });
5474
+
5475
+ // Global mouse and touch events for enhanced dragging
5159
5476
  document.addEventListener('mousemove', (e) => {
5160
5477
  if (this.isVolumeSliding) {
5161
5478
  this.handleVolumeChange(e);
@@ -5165,6 +5482,14 @@ export class WebPlayer extends BasePlayer {
5165
5482
  }
5166
5483
  });
5167
5484
 
5485
+ document.addEventListener('touchmove', (e) => {
5486
+ if (this.isDragging && progressBar) {
5487
+ e.preventDefault(); // Prevent scrolling
5488
+ const touch = e.touches[0];
5489
+ this.handleProgressChange(touch);
5490
+ }
5491
+ }, { passive: false });
5492
+
5168
5493
  document.addEventListener('mouseup', () => {
5169
5494
  if (this.isVolumeSliding) {
5170
5495
  this.isVolumeSliding = false;
@@ -5177,16 +5502,34 @@ export class WebPlayer extends BasePlayer {
5177
5502
 
5178
5503
  if (this.isDragging) {
5179
5504
  this.isDragging = false;
5505
+ // Remove dragging class from handle
5506
+ const handle = document.getElementById('uvf-progress-handle');
5507
+ handle?.classList.remove('dragging');
5508
+ }
5509
+ });
5510
+
5511
+ document.addEventListener('touchend', () => {
5512
+ if (this.isDragging) {
5513
+ this.isDragging = false;
5514
+ // Remove dragging class from handle
5515
+ const handle = document.getElementById('uvf-progress-handle');
5516
+ handle?.classList.remove('dragging');
5180
5517
  }
5181
5518
  });
5182
5519
 
5183
5520
  // Update progress bar
5184
5521
  this.video.addEventListener('timeupdate', () => {
5185
5522
  const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
5523
+ const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
5186
5524
 
5187
5525
  if (this.video && progressFilled) {
5188
5526
  const percent = (this.video.currentTime / this.video.duration) * 100;
5189
5527
  progressFilled.style.width = percent + '%';
5528
+
5529
+ // Update handle position (only when not dragging)
5530
+ if (progressHandle && !this.isDragging) {
5531
+ progressHandle.style.left = percent + '%';
5532
+ }
5190
5533
  }
5191
5534
 
5192
5535
  // Update time display using the dedicated method
@@ -5933,7 +6276,7 @@ export class WebPlayer extends BasePlayer {
5933
6276
  }
5934
6277
  }
5935
6278
 
5936
- private handleProgressChange(e: MouseEvent): void {
6279
+ private handleProgressChange(e: MouseEvent | Touch): void {
5937
6280
  const progressBar = document.getElementById('uvf-progress-bar');
5938
6281
  const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
5939
6282
  const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
@@ -5950,6 +6293,12 @@ export class WebPlayer extends BasePlayer {
5950
6293
  }
5951
6294
  if (progressHandle) {
5952
6295
  progressHandle.style.left = percent + '%';
6296
+ // Add dragging class for visual feedback
6297
+ if (this.isDragging) {
6298
+ progressHandle.classList.add('dragging');
6299
+ } else {
6300
+ progressHandle.classList.remove('dragging');
6301
+ }
5953
6302
  }
5954
6303
 
5955
6304
  this.seek(time);