unified-video-framework 1.4.118 → 1.4.119

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.
@@ -906,26 +906,122 @@ export class WebPlayer extends BasePlayer {
906
906
  }
907
907
  }
908
908
 
909
+ /**
910
+ * Checks if Picture-in-Picture is supported on the current device/browser
911
+ */
912
+ isPictureInPictureSupported(): boolean {
913
+ if (!this.video) return false;
914
+
915
+ // Check standard PiP API support
916
+ const hasStandardPiP = 'requestPictureInPicture' in this.video &&
917
+ 'pictureInPictureEnabled' in document;
918
+
919
+ // Check for Safari/WebKit PiP support
920
+ const hasSafariPiP = 'webkitSupportsPresentationMode' in this.video &&
921
+ typeof (this.video as any).webkitSupportsPresentationMode === 'function' &&
922
+ (this.video as any).webkitSupportsPresentationMode('picture-in-picture');
923
+
924
+ // Mobile browser detection for PiP support
925
+ const userAgent = navigator.userAgent.toLowerCase();
926
+ const isIOS = /iphone|ipad|ipod/.test(userAgent);
927
+ const isAndroid = /android/.test(userAgent);
928
+ const isChrome = /chrome/.test(userAgent) && !/edge/.test(userAgent);
929
+ const isSafari = /safari/.test(userAgent) && !/chrome/.test(userAgent);
930
+ const isFirefox = /firefox/.test(userAgent);
931
+
932
+ // iOS Safari supports PiP from iOS 14+ (with video element)
933
+ const iosSupport = isIOS && isSafari && hasSafariPiP;
934
+
935
+ // Android Chrome supports PiP from Chrome 70+
936
+ const androidChromeSupport = isAndroid && isChrome && hasStandardPiP;
937
+
938
+ // Firefox mobile doesn't support PiP yet
939
+ const firefoxSupport = isFirefox && hasStandardPiP && !this.isMobileDevice();
940
+
941
+ this.debugLog('PiP Support Detection:', {
942
+ hasStandardPiP,
943
+ hasSafariPiP,
944
+ isIOS,
945
+ isAndroid,
946
+ isChrome,
947
+ isSafari,
948
+ isFirefox,
949
+ iosSupport,
950
+ androidChromeSupport,
951
+ firefoxSupport,
952
+ overall: hasStandardPiP || iosSupport || androidChromeSupport || firefoxSupport
953
+ });
954
+
955
+ return hasStandardPiP || iosSupport || androidChromeSupport || firefoxSupport;
956
+ }
957
+
958
+ /**
959
+ * Detects if running on a mobile device
960
+ */
961
+ private isMobileDevice(): boolean {
962
+ const userAgent = navigator.userAgent.toLowerCase();
963
+ return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(userAgent) ||
964
+ (!!(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) && /macintosh/.test(userAgent));
965
+ }
966
+
909
967
  async enterPictureInPicture(): Promise<void> {
910
- if (!this.video) return;
968
+ if (!this.video) {
969
+ throw new Error('Video element not available');
970
+ }
971
+
972
+ if (!this.isPictureInPictureSupported()) {
973
+ throw new Error('Picture-in-Picture not supported on this device/browser');
974
+ }
911
975
 
912
976
  try {
913
- if ((this.video as any).requestPictureInPicture) {
977
+ // Standard PiP API (Chrome, Firefox, etc.)
978
+ if ('requestPictureInPicture' in this.video) {
914
979
  await (this.video as any).requestPictureInPicture();
915
- } else {
916
- throw new Error('Picture-in-Picture not supported');
980
+ this.debugLog('PiP entered using standard API');
981
+ return;
917
982
  }
983
+
984
+ // Safari/WebKit PiP API
985
+ if ('webkitSetPresentationMode' in this.video) {
986
+ (this.video as any).webkitSetPresentationMode('picture-in-picture');
987
+ this.debugLog('PiP entered using WebKit API');
988
+ return;
989
+ }
990
+
991
+ throw new Error('No supported PiP API available');
918
992
  } catch (error) {
919
- console.error('Failed to enter PiP:', error);
993
+ this.debugError('Failed to enter PiP:', (error as Error).message);
920
994
  throw error;
921
995
  }
922
996
  }
923
997
 
924
998
  async exitPictureInPicture(): Promise<void> {
925
999
  try {
926
- if ((document as any).exitPictureInPicture) {
1000
+ // Check if currently in PiP mode
1001
+ const inStandardPiP = (document as any).pictureInPictureElement;
1002
+ const inWebkitPiP = this.video &&
1003
+ 'webkitPresentationMode' in this.video &&
1004
+ (this.video as any).webkitPresentationMode === 'picture-in-picture';
1005
+
1006
+ if (!inStandardPiP && !inWebkitPiP) {
1007
+ this.debugLog('Not currently in PiP mode');
1008
+ return;
1009
+ }
1010
+
1011
+ // Standard PiP exit
1012
+ if (inStandardPiP && 'exitPictureInPicture' in document) {
927
1013
  await (document as any).exitPictureInPicture();
1014
+ this.debugLog('PiP exited using standard API');
1015
+ return;
1016
+ }
1017
+
1018
+ // Safari/WebKit PiP exit
1019
+ if (inWebkitPiP && this.video && 'webkitSetPresentationMode' in this.video) {
1020
+ (this.video as any).webkitSetPresentationMode('inline');
1021
+ this.debugLog('PiP exited using WebKit API');
1022
+ return;
928
1023
  }
1024
+
929
1025
  } catch (error) {
930
1026
  this.debugWarn('Failed to exit PiP:', (error as Error).message);
931
1027
  // Don't re-throw the error to prevent breaking the user experience
@@ -1793,16 +1889,48 @@ export class WebPlayer extends BasePlayer {
1793
1889
  aspect-ratio: 16 / 9;
1794
1890
  background: radial-gradient(ellipse at center, #1a1a2e 0%, #000 100%);
1795
1891
  overflow: hidden;
1892
+ display: flex;
1893
+ align-items: center;
1894
+ justify-content: center;
1796
1895
  }
1797
1896
 
1798
1897
  .uvf-video {
1799
1898
  position: absolute;
1800
- top: 0;
1801
- left: 0;
1802
- width: 100%;
1803
- height: 100%;
1899
+ top: 50%;
1900
+ left: 50%;
1901
+ transform: translate(-50%, -50%);
1902
+ max-width: 100%;
1903
+ max-height: 100%;
1904
+ width: auto;
1905
+ height: auto;
1804
1906
  background: #000;
1805
1907
  object-fit: contain;
1908
+ /* Ensure proper centering and scaling */
1909
+ object-position: center;
1910
+ }
1911
+
1912
+ /* Mobile-specific video centering improvements */
1913
+ @media screen and (max-width: 767px) {
1914
+ .uvf-video {
1915
+ /* Force full width/height on mobile for better centering */
1916
+ width: 100%;
1917
+ height: 100%;
1918
+ top: 0;
1919
+ left: 0;
1920
+ transform: none;
1921
+ object-fit: contain;
1922
+ object-position: center;
1923
+ }
1924
+
1925
+ .uvf-video-container {
1926
+ /* Remove aspect ratio constraint on mobile for full height usage */
1927
+ aspect-ratio: unset;
1928
+ min-height: 100%;
1929
+ height: 100%;
1930
+ display: flex;
1931
+ align-items: center;
1932
+ justify-content: center;
1933
+ }
1806
1934
  }
1807
1935
 
1808
1936
  .uvf-watermark-layer {
@@ -2925,6 +3053,10 @@ export class WebPlayer extends BasePlayer {
2925
3053
  @media screen and (max-width: 767px) {
2926
3054
  html, body {
2927
3055
  overflow-x: hidden;
3056
+ /* Prevent iOS Safari address bar bounce */
3057
+ position: fixed;
3058
+ height: 100%;
3059
+ width: 100%;
2928
3060
  }
2929
3061
 
2930
3062
  .uvf-player-wrapper {
@@ -2934,6 +3066,23 @@ export class WebPlayer extends BasePlayer {
2934
3066
 
2935
3067
  /* Prevent zoom on double tap */
2936
3068
  touch-action: manipulation;
3069
+
3070
+ /* Ensure full viewport usage */
3071
+ position: relative;
3072
+ width: 100vw;
3073
+ height: 100vh;
3074
+
3075
+ /* iOS Safari fix for viewport height */
3076
+ min-height: -webkit-fill-available;
3077
+ }
3078
+
3079
+ .uvf-video-container {
3080
+ /* Full viewport container */
3081
+ width: 100vw;
3082
+ height: 100vh;
3083
+ min-height: -webkit-fill-available;
3084
+ position: relative;
3085
+ overflow: hidden;
2937
3086
  }
2938
3087
 
2939
3088
  .uvf-video {
@@ -2946,12 +3095,38 @@ export class WebPlayer extends BasePlayer {
2946
3095
  /* Ensure hardware acceleration */
2947
3096
  -webkit-transform: translateZ(0);
2948
3097
  transform: translateZ(0);
3098
+
3099
+ /* Full viewport video with proper centering */
3100
+ width: 100%;
3101
+ height: 100%;
3102
+ object-fit: contain;
3103
+ object-position: center center;
2949
3104
  }
2950
3105
 
2951
3106
  /* Fix for controls being cut off by virtual keyboard */
2952
3107
  .uvf-controls-bar {
2953
3108
  position: fixed !important;
2954
3109
  bottom: var(--uvf-safe-area-bottom, 0) !important;
3110
+ left: var(--uvf-safe-area-left, 0) !important;
3111
+ right: var(--uvf-safe-area-right, 0) !important;
3112
+ width: auto !important;
3113
+ z-index: 9999;
3114
+ }
3115
+
3116
+ /* Top controls safe area positioning */
3117
+ .uvf-top-controls {
3118
+ position: fixed !important;
3119
+ top: var(--uvf-safe-area-top, 10px) !important;
3120
+ right: var(--uvf-safe-area-right, 10px) !important;
3121
+ z-index: 9999;
3122
+ }
3123
+
3124
+ .uvf-title-bar {
3125
+ position: fixed !important;
3126
+ top: var(--uvf-safe-area-top, 10px) !important;
3127
+ left: var(--uvf-safe-area-left, 10px) !important;
3128
+ right: calc(120px + var(--uvf-safe-area-right, 10px)) !important; /* Leave space for top controls */
3129
+ z-index: 9999;
2955
3130
  }
2956
3131
 
2957
3132
  /* Ensure controls stay above virtual keyboards */
@@ -2960,6 +3135,61 @@ export class WebPlayer extends BasePlayer {
2960
3135
  bottom: max(var(--uvf-safe-area-bottom, 0), env(keyboard-inset-height, 0)) !important;
2961
3136
  }
2962
3137
  }
3138
+
3139
+ /* Enhanced safe area support for newer devices */
3140
+ @supports (padding: max(0px)) {
3141
+ .uvf-controls-bar {
3142
+ padding-bottom: max(16px, calc(16px + var(--uvf-safe-area-bottom, 0)));
3143
+ padding-left: max(12px, calc(12px + var(--uvf-safe-area-left, 0)));
3144
+ padding-right: max(12px, calc(12px + var(--uvf-safe-area-right, 0)));
3145
+ }
3146
+
3147
+ .uvf-top-controls {
3148
+ top: max(10px, calc(10px + var(--uvf-safe-area-top, 0))) !important;
3149
+ right: max(10px, calc(10px + var(--uvf-safe-area-right, 0))) !important;
3150
+ }
3151
+
3152
+ .uvf-title-bar {
3153
+ top: max(10px, calc(10px + var(--uvf-safe-area-top, 0))) !important;
3154
+ left: max(10px, calc(10px + var(--uvf-safe-area-left, 0))) !important;
3155
+ }
3156
+ }
3157
+ }
3158
+
3159
+ /* Specific fixes for iPhone X series and newer with notches */
3160
+ @media screen and (max-width: 767px) and (orientation: portrait) {
3161
+ @supports (top: env(safe-area-inset-top)) {
3162
+ .uvf-responsive-container,
3163
+ .uvf-player-wrapper,
3164
+ .uvf-video-container {
3165
+ height: 100vh;
3166
+ height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
3167
+ }
3168
+ }
3169
+ }
3170
+
3171
+ /* Landscape orientation fixes for mobile */
3172
+ @media screen and (max-width: 767px) and (orientation: landscape) {
3173
+ html, body {
3174
+ height: 100vh;
3175
+ overflow: hidden;
3176
+ }
3177
+
3178
+ .uvf-responsive-container,
3179
+ .uvf-player-wrapper,
3180
+ .uvf-video-container {
3181
+ height: 100vh;
3182
+ width: 100vw;
3183
+ }
3184
+
3185
+ @supports (height: 100dvh) {
3186
+ .uvf-responsive-container,
3187
+ .uvf-player-wrapper,
3188
+ .uvf-video-container {
3189
+ height: 100dvh;
3190
+ width: 100dvw;
3191
+ }
3192
+ }
2963
3193
  }
2964
3194
 
2965
3195
  /* Enhanced Responsive Media Queries with UX Best Practices */
@@ -4311,13 +4541,20 @@ export class WebPlayer extends BasePlayer {
4311
4541
  epgBtn.style.display = 'none'; // Initially hidden, will be shown when EPG data is available
4312
4542
  rightControls.appendChild(epgBtn);
4313
4543
 
4314
- // PiP button
4544
+ // PiP button - conditionally add based on support
4315
4545
  const pipBtn = document.createElement('button');
4316
4546
  pipBtn.className = 'uvf-control-btn';
4317
4547
  pipBtn.id = 'uvf-pip-btn';
4318
4548
  pipBtn.title = 'Picture-in-Picture';
4319
4549
  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"/></svg>';
4320
- rightControls.appendChild(pipBtn);
4550
+
4551
+ // Only show PiP button if supported
4552
+ if (this.isPictureInPictureSupported()) {
4553
+ rightControls.appendChild(pipBtn);
4554
+ this.debugLog('PiP button added - support detected');
4555
+ } else {
4556
+ this.debugLog('PiP button not added - no support detected');
4557
+ }
4321
4558
 
4322
4559
  // Fullscreen button
4323
4560
  const fullscreenBtn = document.createElement('button');
@@ -5586,15 +5823,49 @@ export class WebPlayer extends BasePlayer {
5586
5823
  }
5587
5824
  }
5588
5825
 
5826
+ /**
5827
+ * Checks if currently in Picture-in-Picture mode
5828
+ */
5829
+ isPictureInPictureActive(): boolean {
5830
+ // Check standard PiP
5831
+ const inStandardPiP = !!(document as any).pictureInPictureElement;
5832
+
5833
+ // Check Safari/WebKit PiP
5834
+ const inWebkitPiP = !!(this.video &&
5835
+ 'webkitPresentationMode' in this.video &&
5836
+ (this.video as any).webkitPresentationMode === 'picture-in-picture');
5837
+
5838
+ return inStandardPiP || inWebkitPiP;
5839
+ }
5840
+
5589
5841
  private async togglePiP(): Promise<void> {
5842
+ if (!this.isPictureInPictureSupported()) {
5843
+ this.showShortcutIndicator('PiP not supported');
5844
+ this.debugWarn('PiP not supported on this device/browser');
5845
+ return;
5846
+ }
5847
+
5590
5848
  try {
5591
- if ((document as any).pictureInPictureElement) {
5849
+ if (this.isPictureInPictureActive()) {
5592
5850
  await this.exitPictureInPicture();
5851
+ this.showShortcutIndicator('Exit PiP');
5852
+ this.debugLog('PiP deactivated');
5593
5853
  } else {
5594
5854
  await this.enterPictureInPicture();
5855
+ this.showShortcutIndicator('Enter PiP');
5856
+ this.debugLog('PiP activated');
5595
5857
  }
5596
5858
  } catch (error) {
5597
- console.error('PiP toggle failed:', error);
5859
+ const errorMessage = (error as Error).message;
5860
+ this.showShortcutIndicator(`PiP Error: ${errorMessage}`);
5861
+ this.debugError('PiP toggle failed:', errorMessage);
5862
+
5863
+ // Show user-friendly message for common errors
5864
+ if (errorMessage.includes('not supported')) {
5865
+ this.showShortcutIndicator('PiP not supported');
5866
+ } else if (errorMessage.includes('user gesture')) {
5867
+ this.showShortcutIndicator('PiP requires user interaction');
5868
+ }
5598
5869
  }
5599
5870
  }
5600
5871