unified-video-framework 1.4.117 → 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;
928
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;
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
@@ -1790,19 +1886,51 @@ export class WebPlayer extends BasePlayer {
1790
1886
  .uvf-video-container {
1791
1887
  position: relative;
1792
1888
  width: 100%;
1793
- // aspect-ratio: 16 / 9;
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 {
@@ -1989,6 +2117,7 @@ export class WebPlayer extends BasePlayer {
1989
2117
  width: 100%;
1990
2118
  position: relative;
1991
2119
  cursor: pointer;
2120
+ padding: 16px 0;
1992
2121
  overflow: visible;
1993
2122
  }
1994
2123
 
@@ -2102,105 +2231,10 @@ export class WebPlayer extends BasePlayer {
2102
2231
 
2103
2232
  /* Mobile responsive design with enhanced touch targets */
2104
2233
  @media (max-width: 768px) {
2105
- .uvf-player-wrapper {
2106
- position: relative;
2107
- width: 100% !important;
2108
- height: auto !important;
2109
- min-height: 200px;
2110
- }
2111
-
2112
- .uvf-video-container {
2113
- width: 100% !important;
2114
- height: auto !important;
2115
- aspect-ratio: 16/9;
2116
- position: relative;
2117
- }
2118
-
2119
- .uvf-video {
2120
- width: 100% !important;
2121
- height: 100% !important;
2122
- object-fit: contain;
2123
- }
2124
-
2125
- /* Fix controls bar positioning */
2126
- .uvf-controls-bar {
2127
- position: absolute;
2128
- bottom: 0;
2129
- left: 0;
2130
- right: 0;
2131
- padding: 8px 12px !important;
2132
- padding-bottom: max(8px, env(safe-area-inset-bottom)) !important;
2133
- background: linear-gradient(to top,
2134
- rgba(0,0,0,0.9) 0%,
2135
- rgba(0,0,0,0.7) 70%,
2136
- transparent 100%) !important;
2137
- z-index: 1000;
2138
- }
2139
-
2140
- /* Ensure controls are properly sized */
2141
- .uvf-controls-row {
2142
- display: flex;
2143
- align-items: center;
2144
- gap: 8px;
2145
- flex-wrap: nowrap;
2146
- min-height: 44px; /* Minimum touch target size */
2147
- }
2148
-
2149
- /* Touch-friendly button sizes */
2150
- .uvf-control-btn {
2151
- width: 44px !important;
2152
- height: 44px !important;
2153
- min-width: 44px !important;
2154
- min-height: 44px !important;
2155
- border-radius: 22px !important;
2156
- }
2157
-
2158
- .uvf-control-btn.play-pause {
2159
- width: 52px !important;
2160
- height: 52px !important;
2161
- min-width: 52px !important;
2162
- min-height: 52px !important;
2163
- }
2164
-
2165
- /* Progress bar adjustments */
2166
- .uvf-progress-section {
2167
- margin-bottom: 12px !important;
2168
- padding: 0 8px;
2169
- }
2170
-
2171
- .uvf-progress-bar {
2172
- height: 4px !important;
2173
- margin-bottom: 8px;
2174
- }
2175
-
2176
- /* Time display adjustments */
2177
- .uvf-time-display {
2178
- font-size: 12px !important;
2179
- min-width: 90px !important;
2180
- padding: 4px 8px !important;
2181
- background: rgba(0,0,0,0.5);
2182
- border-radius: 12px;
2183
- margin: 0 4px;
2234
+ .uvf-progress-bar-wrapper {
2235
+ padding: 20px 0; /* Larger touch area */
2184
2236
  }
2185
2237
 
2186
- /* Right controls spacing */
2187
- .uvf-right-controls {
2188
- gap: 6px !important;
2189
- margin-left: auto;
2190
- }
2191
-
2192
- /* Hide non-essential elements on mobile */
2193
- .uvf-quality-badge {
2194
- display: none !important;
2195
- }
2196
-
2197
- /* Ensure settings menu is accessible */
2198
- .uvf-settings-menu {
2199
- bottom: 60px !important;
2200
- right: 12px !important;
2201
- max-height: 60vh !important;
2202
- min-width: 160px !important;
2203
- }
2204
2238
  .uvf-progress-bar {
2205
2239
  height: 3px; /* Slightly thicker on mobile */
2206
2240
  }
@@ -2208,31 +2242,7 @@ export class WebPlayer extends BasePlayer {
2208
2242
  .uvf-progress-bar-wrapper:hover .uvf-progress-bar {
2209
2243
  height: 5px;
2210
2244
  }
2211
- }
2212
-
2213
- /* Mobile Landscape */
2214
- @media screen and (max-width: 767px) and (orientation: landscape) {
2215
- .uvf-controls-bar {
2216
- padding: 6px 10px !important;
2217
- padding-bottom: max(6px, env(safe-area-inset-bottom)) !important;
2218
- }
2219
-
2220
- .uvf-control-btn {
2221
- width: 40px !important;
2222
- height: 40px !important;
2223
- min-width: 40px !important;
2224
- min-height: 40px !important;
2225
- }
2226
2245
 
2227
- .uvf-control-btn.play-pause {
2228
- width: 46px !important;
2229
- height: 46px !important;
2230
- }
2231
-
2232
- .uvf-time-display {
2233
- font-size: 11px !important;
2234
- min-width: 80px !important;
2235
- }
2236
2246
  }
2237
2247
 
2238
2248
  /* Controls Row */
@@ -2962,28 +2972,276 @@ export class WebPlayer extends BasePlayer {
2962
2972
  }
2963
2973
  }
2964
2974
 
2975
+ /* Safe Area Variables - Support for modern mobile devices */
2976
+ :root {
2977
+ /* iOS Safe Area Fallbacks */
2978
+ --uvf-safe-area-top: env(safe-area-inset-top, 0px);
2979
+ --uvf-safe-area-right: env(safe-area-inset-right, 0px);
2980
+ --uvf-safe-area-bottom: env(safe-area-inset-bottom, 0px);
2981
+ --uvf-safe-area-left: env(safe-area-inset-left, 0px);
2982
+
2983
+ /* Dynamic Viewport Support */
2984
+ --uvf-dvh: 1dvh;
2985
+ --uvf-svh: 1svh;
2986
+ --uvf-lvh: 1lvh;
2987
+ }
2988
+
2989
+ /* Cross-Browser Mobile Viewport Fixes */
2990
+
2991
+ /* Modern browsers with dynamic viewport support */
2992
+ @supports (height: 100dvh) {
2993
+ .uvf-player-wrapper,
2994
+ .uvf-video-container {
2995
+ height: 100dvh;
2996
+ }
2997
+
2998
+ .uvf-responsive-container {
2999
+ height: 100dvh;
3000
+ }
3001
+ }
3002
+
3003
+ /* iOS Safari specific fixes - address bar handling */
3004
+ @supports (-webkit-appearance: none) {
3005
+ .uvf-player-wrapper.uvf-fullscreen,
3006
+ .uvf-video-container.uvf-fullscreen {
3007
+ height: -webkit-fill-available;
3008
+ min-height: -webkit-fill-available;
3009
+ }
3010
+
3011
+ /* Handle iOS Safari's dynamic address bar */
3012
+ @media screen and (max-width: 767px) {
3013
+ .uvf-responsive-container {
3014
+ height: -webkit-fill-available;
3015
+ min-height: 100vh;
3016
+ }
3017
+
3018
+ .uvf-player-wrapper {
3019
+ height: -webkit-fill-available;
3020
+ min-height: 100vh;
3021
+ }
3022
+ }
3023
+ }
3024
+
3025
+ /* Android Chrome specific fixes */
3026
+ @supports (display: -webkit-box) {
3027
+ .uvf-responsive-container {
3028
+ min-height: 100vh;
3029
+ }
3030
+
3031
+ /* Fix for Android Chrome's address bar behavior */
3032
+ @media screen and (max-width: 767px) {
3033
+ .uvf-video-container {
3034
+ min-height: calc(100vh - 56px); /* Chrome mobile address bar height */
3035
+ }
3036
+ }
3037
+ }
3038
+
3039
+ /* Samsung Internet Browser fixes */
3040
+ @media screen and (-webkit-min-device-pixel-ratio: 1) {
3041
+ @media screen and (max-width: 767px) {
3042
+ .uvf-responsive-container {
3043
+ position: fixed;
3044
+ top: 0;
3045
+ left: 0;
3046
+ width: 100vw;
3047
+ height: 100vh;
3048
+ }
3049
+ }
3050
+ }
3051
+
3052
+ /* Universal mobile fixes for all browsers */
3053
+ @media screen and (max-width: 767px) {
3054
+ html, body {
3055
+ overflow-x: hidden;
3056
+ /* Prevent iOS Safari address bar bounce */
3057
+ position: fixed;
3058
+ height: 100%;
3059
+ width: 100%;
3060
+ }
3061
+
3062
+ .uvf-player-wrapper {
3063
+ /* Prevent scroll bounce on iOS */
3064
+ -webkit-overflow-scrolling: touch;
3065
+ overflow: hidden;
3066
+
3067
+ /* Prevent zoom on double tap */
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;
3086
+ }
3087
+
3088
+ .uvf-video {
3089
+ /* Prevent video from being selectable */
3090
+ -webkit-user-select: none;
3091
+ -moz-user-select: none;
3092
+ -ms-user-select: none;
3093
+ user-select: none;
3094
+
3095
+ /* Ensure hardware acceleration */
3096
+ -webkit-transform: translateZ(0);
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;
3104
+ }
3105
+
3106
+ /* Fix for controls being cut off by virtual keyboard */
3107
+ .uvf-controls-bar {
3108
+ position: fixed !important;
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;
3130
+ }
3131
+
3132
+ /* Ensure controls stay above virtual keyboards */
3133
+ @supports (bottom: env(keyboard-inset-height)) {
3134
+ .uvf-controls-bar {
3135
+ bottom: max(var(--uvf-safe-area-bottom, 0), env(keyboard-inset-height, 0)) !important;
3136
+ }
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
+ }
3193
+ }
3194
+
2965
3195
  /* Enhanced Responsive Media Queries with UX Best Practices */
2966
- /* Mobile devices (portrait) - Enhanced UX */
3196
+ /* Mobile devices (portrait) - Enhanced UX with Safe Areas */
2967
3197
  @media screen and (max-width: 767px) and (orientation: portrait) {
2968
3198
  .uvf-responsive-container {
2969
3199
  padding: 0;
2970
3200
  width: 100vw !important;
3201
+ height: calc(100vh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
2971
3202
  margin: 0;
3203
+ position: relative;
3204
+ overflow: hidden;
3205
+ }
3206
+
3207
+ @supports (height: 100dvh) {
3208
+ .uvf-responsive-container {
3209
+ height: calc(100dvh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
3210
+ }
2972
3211
  }
2973
3212
 
2974
3213
  .uvf-responsive-container .uvf-player-wrapper {
2975
3214
  width: 100vw !important;
3215
+ height: 100% !important;
3216
+ min-height: calc(100vh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
3217
+ }
3218
+
3219
+ @supports (height: 100dvh) {
3220
+ .uvf-responsive-container .uvf-player-wrapper {
3221
+ min-height: calc(100dvh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
3222
+ }
2976
3223
  }
2977
3224
 
2978
3225
  .uvf-responsive-container .uvf-video-container {
2979
3226
  width: 100vw !important;
2980
- // aspect-ratio: unset !important;
3227
+ height: 100% !important;
3228
+ aspect-ratio: unset !important;
3229
+ min-height: inherit;
2981
3230
  }
2982
3231
 
2983
- /* Enhanced mobile controls bar with better spacing */
3232
+ /* Enhanced mobile controls bar with safe area padding */
2984
3233
  .uvf-controls-bar {
3234
+ position: absolute;
3235
+ bottom: 0;
3236
+ left: 0;
3237
+ right: 0;
2985
3238
  padding: 16px 12px;
3239
+ padding-bottom: calc(16px + var(--uvf-safe-area-bottom));
3240
+ padding-left: calc(12px + var(--uvf-safe-area-left));
3241
+ padding-right: calc(12px + var(--uvf-safe-area-right));
2986
3242
  background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 80%, var(--uvf-overlay-transparent) 100%);
3243
+ box-sizing: border-box;
3244
+ z-index: 1000;
2987
3245
  }
2988
3246
 
2989
3247
  .uvf-progress-section {
@@ -3155,11 +3413,11 @@ export class WebPlayer extends BasePlayer {
3155
3413
  }
3156
3414
  }
3157
3415
 
3158
- /* Enhanced top controls for mobile with proper alignment */
3416
+ /* Enhanced top controls for mobile with safe area support */
3159
3417
  .uvf-top-controls {
3160
3418
  position: absolute;
3161
- top: 12px;
3162
- right: 12px;
3419
+ top: calc(12px + var(--uvf-safe-area-top));
3420
+ right: calc(12px + var(--uvf-safe-area-right));
3163
3421
  display: flex;
3164
3422
  align-items: center;
3165
3423
  gap: 8px;
@@ -3187,9 +3445,12 @@ export class WebPlayer extends BasePlayer {
3187
3445
  display: flex;
3188
3446
  }
3189
3447
 
3190
- /* Enhanced title bar for mobile */
3448
+ /* Enhanced title bar for mobile with safe area support */
3191
3449
  .uvf-title-bar {
3192
3450
  padding: 12px;
3451
+ padding-top: calc(12px + var(--uvf-safe-area-top));
3452
+ padding-left: calc(12px + var(--uvf-safe-area-left));
3453
+ padding-right: calc(12px + var(--uvf-safe-area-right));
3193
3454
  }
3194
3455
 
3195
3456
  .uvf-video-title {
@@ -3316,30 +3577,55 @@ export class WebPlayer extends BasePlayer {
3316
3577
  }
3317
3578
  }
3318
3579
 
3319
- /* Mobile devices (landscape) - Optimized for fullscreen viewing */
3580
+ /* Mobile devices (landscape) - Optimized for fullscreen viewing with safe areas */
3320
3581
  @media screen and (max-width: 767px) and (orientation: landscape) {
3321
3582
  .uvf-responsive-container {
3322
3583
  width: 100vw !important;
3323
- height: 100vh !important;
3584
+ height: calc(100vh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
3324
3585
  margin: 0;
3325
3586
  padding: 0;
3587
+ position: relative;
3588
+ overflow: hidden;
3589
+ }
3590
+
3591
+ @supports (height: 100dvh) {
3592
+ .uvf-responsive-container {
3593
+ height: calc(100dvh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
3594
+ }
3326
3595
  }
3327
3596
 
3328
3597
  .uvf-responsive-container .uvf-player-wrapper {
3329
3598
  width: 100vw !important;
3330
- height: 100vh !important;
3599
+ height: 100% !important;
3600
+ min-height: calc(100vh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
3601
+ }
3602
+
3603
+ @supports (height: 100dvh) {
3604
+ .uvf-responsive-container .uvf-player-wrapper {
3605
+ min-height: calc(100dvh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
3606
+ }
3331
3607
  }
3332
3608
 
3333
3609
  .uvf-responsive-container .uvf-video-container {
3334
3610
  width: 100vw !important;
3335
- height: 100vh !important;
3611
+ height: 100% !important;
3336
3612
  aspect-ratio: unset !important;
3613
+ min-height: inherit;
3337
3614
  }
3338
3615
 
3339
- /* Compact controls for landscape */
3616
+ /* Compact controls for landscape with safe area padding */
3340
3617
  .uvf-controls-bar {
3618
+ position: absolute;
3619
+ bottom: 0;
3620
+ left: 0;
3621
+ right: 0;
3341
3622
  padding: 10px 12px;
3623
+ padding-bottom: calc(10px + var(--uvf-safe-area-bottom));
3624
+ padding-left: calc(12px + var(--uvf-safe-area-left));
3625
+ padding-right: calc(12px + var(--uvf-safe-area-right));
3342
3626
  background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 80%, var(--uvf-overlay-transparent) 100%);
3627
+ box-sizing: border-box;
3628
+ z-index: 1000;
3343
3629
  }
3344
3630
 
3345
3631
  .uvf-progress-section {
@@ -3375,13 +3661,20 @@ export class WebPlayer extends BasePlayer {
3375
3661
  height: 22px;
3376
3662
  }
3377
3663
 
3378
- /* Compact top controls */
3664
+ /* Compact top controls with safe area padding */
3379
3665
  .uvf-top-controls {
3380
- top: 8px;
3381
- right: 12px;
3666
+ top: calc(8px + var(--uvf-safe-area-top));
3667
+ right: calc(12px + var(--uvf-safe-area-right));
3382
3668
  gap: 6px;
3383
3669
  }
3384
3670
 
3671
+ .uvf-title-bar {
3672
+ padding: 8px 12px;
3673
+ padding-top: calc(8px + var(--uvf-safe-area-top));
3674
+ padding-left: calc(12px + var(--uvf-safe-area-left));
3675
+ padding-right: calc(12px + var(--uvf-safe-area-right));
3676
+ }
3677
+
3385
3678
  .uvf-top-btn {
3386
3679
  width: 40px;
3387
3680
  height: 40px;
@@ -3428,17 +3721,7 @@ export class WebPlayer extends BasePlayer {
3428
3721
  height: 16px;
3429
3722
  }
3430
3723
  }
3431
- @media (max-width: 768px) {
3432
- .uvf-video-container {
3433
- aspect-ratio: auto !important;
3434
- height: 100% !important;
3435
- }
3436
- .uvf-controls-bar {
3437
- bottom: 0 !important;
3438
- padding-bottom: 10px !important;
3439
- }
3440
- }
3441
-
3724
+
3442
3725
  /* Tablet devices - Enhanced UX with desktop features */
3443
3726
  @media screen and (min-width: 768px) and (max-width: 1023px) {
3444
3727
  .uvf-controls-bar {
@@ -4258,13 +4541,20 @@ export class WebPlayer extends BasePlayer {
4258
4541
  epgBtn.style.display = 'none'; // Initially hidden, will be shown when EPG data is available
4259
4542
  rightControls.appendChild(epgBtn);
4260
4543
 
4261
- // PiP button
4544
+ // PiP button - conditionally add based on support
4262
4545
  const pipBtn = document.createElement('button');
4263
4546
  pipBtn.className = 'uvf-control-btn';
4264
4547
  pipBtn.id = 'uvf-pip-btn';
4265
4548
  pipBtn.title = 'Picture-in-Picture';
4266
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>';
4267
- 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
+ }
4268
4558
 
4269
4559
  // Fullscreen button
4270
4560
  const fullscreenBtn = document.createElement('button');
@@ -5533,15 +5823,49 @@ export class WebPlayer extends BasePlayer {
5533
5823
  }
5534
5824
  }
5535
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
+
5536
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
+
5537
5848
  try {
5538
- if ((document as any).pictureInPictureElement) {
5849
+ if (this.isPictureInPictureActive()) {
5539
5850
  await this.exitPictureInPicture();
5851
+ this.showShortcutIndicator('Exit PiP');
5852
+ this.debugLog('PiP deactivated');
5540
5853
  } else {
5541
5854
  await this.enterPictureInPicture();
5855
+ this.showShortcutIndicator('Enter PiP');
5856
+ this.debugLog('PiP activated');
5542
5857
  }
5543
5858
  } catch (error) {
5544
- 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
+ }
5545
5869
  }
5546
5870
  }
5547
5871