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)
|
|
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
|
-
|
|
977
|
+
// Standard PiP API (Chrome, Firefox, etc.)
|
|
978
|
+
if ('requestPictureInPicture' in this.video) {
|
|
914
979
|
await (this.video as any).requestPictureInPicture();
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
1801
|
-
left:
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
-
|
|
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 ((
|
|
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
|
-
|
|
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
|
|