unified-video-framework 1.4.156 → 1.4.158
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.
- package/package.json +1 -1
- package/packages/web/dist/WebPlayer.d.ts +8 -0
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +220 -55
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.js +22 -0
- package/packages/web/dist/chapters/ChapterManager.js.map +1 -1
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts +1 -0
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -1
- package/packages/web/dist/chapters/types/ChapterTypes.js +2 -1
- package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +1 -0
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +1 -0
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +305 -72
- package/packages/web/src/chapters/ChapterManager.ts +32 -0
- package/packages/web/src/chapters/types/ChapterTypes.ts +5 -1
- package/packages/web/src/react/WebPlayerView.tsx +2 -0
- package/packages/core/dist/chapter-manager.d.ts +0 -39
- package/packages/ios/README.md +0 -84
- package/packages/web/dist/HTML5Player.js.map +0 -1
- package/packages/web/dist/epg/EPGController.d.ts +0 -78
- package/packages/web/dist/epg/EPGController.d.ts.map +0 -1
- package/packages/web/dist/epg/EPGController.js +0 -476
- package/packages/web/dist/epg/EPGController.js.map +0 -1
|
@@ -96,7 +96,23 @@ export class WebPlayer extends BasePlayer {
|
|
|
96
96
|
|
|
97
97
|
// Progress bar tooltip state
|
|
98
98
|
private showTimeTooltip: boolean = false;
|
|
99
|
-
|
|
99
|
+
|
|
100
|
+
// Autoplay enhancement state
|
|
101
|
+
private autoplayCapabilities: {
|
|
102
|
+
canAutoplay: boolean;
|
|
103
|
+
canAutoplayMuted: boolean;
|
|
104
|
+
canAutoplayUnmuted: boolean;
|
|
105
|
+
lastCheck: number;
|
|
106
|
+
} = {
|
|
107
|
+
canAutoplay: false,
|
|
108
|
+
canAutoplayMuted: false,
|
|
109
|
+
canAutoplayUnmuted: false,
|
|
110
|
+
lastCheck: 0
|
|
111
|
+
};
|
|
112
|
+
private autoplayRetryPending: boolean = false;
|
|
113
|
+
private autoplayRetryAttempts: number = 0;
|
|
114
|
+
private maxAutoplayRetries: number = 3;
|
|
115
|
+
|
|
100
116
|
// Chapter management
|
|
101
117
|
private chapterManager: ChapterManager | null = null;
|
|
102
118
|
private coreChapterManager: CoreChapterManager | null = null;
|
|
@@ -197,9 +213,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
197
213
|
this.video = document.createElement('video');
|
|
198
214
|
this.video.className = 'uvf-video';
|
|
199
215
|
this.video.controls = false; // We'll use custom controls
|
|
200
|
-
//
|
|
201
|
-
this.video.autoplay =
|
|
202
|
-
|
|
216
|
+
// Don't set autoplay attribute - we'll handle it programmatically with intelligent detection
|
|
217
|
+
this.video.autoplay = false;
|
|
218
|
+
// Respect user's muted preference, intelligent autoplay will handle browser policies
|
|
219
|
+
this.video.muted = this.config.muted ?? false;
|
|
203
220
|
this.video.loop = this.config.loop ?? false;
|
|
204
221
|
this.video.playsInline = this.config.playsInline ?? true;
|
|
205
222
|
this.video.preload = this.config.preload ?? 'metadata';
|
|
@@ -344,6 +361,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
344
361
|
this.state.isPlaying = true;
|
|
345
362
|
this.state.isPaused = false;
|
|
346
363
|
this.emit('onPlay');
|
|
364
|
+
|
|
365
|
+
// Hide play overlay when playback starts
|
|
366
|
+
this.hidePlayOverlay();
|
|
347
367
|
});
|
|
348
368
|
|
|
349
369
|
this.video.addEventListener('playing', () => {
|
|
@@ -352,6 +372,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
352
372
|
this._deferredPause = false;
|
|
353
373
|
try { this.video?.pause(); } catch (_) {}
|
|
354
374
|
}
|
|
375
|
+
|
|
376
|
+
// Also hide overlay on playing event (more reliable)
|
|
377
|
+
this.hidePlayOverlay();
|
|
378
|
+
|
|
379
|
+
// Stop buffering state
|
|
380
|
+
this.setBuffering(false);
|
|
355
381
|
});
|
|
356
382
|
|
|
357
383
|
this.video.addEventListener('pause', () => {
|
|
@@ -573,14 +599,20 @@ export class WebPlayer extends BasePlayer {
|
|
|
573
599
|
|
|
574
600
|
// Start playback if autoPlay is enabled
|
|
575
601
|
if (this.config.autoPlay) {
|
|
576
|
-
//
|
|
577
|
-
this.
|
|
578
|
-
if (
|
|
579
|
-
this.debugWarn('
|
|
602
|
+
// Use intelligent autoplay with capability detection
|
|
603
|
+
this.attemptIntelligentAutoplay().then(success => {
|
|
604
|
+
if (!success) {
|
|
605
|
+
this.debugWarn('❌ Intelligent autoplay failed, showing play overlay');
|
|
580
606
|
this.showPlayOverlay();
|
|
607
|
+
// Set up retry on user interaction
|
|
608
|
+
this.setupAutoplayRetry();
|
|
581
609
|
} else {
|
|
582
|
-
this.
|
|
610
|
+
this.debugLog('✅ Intelligent autoplay succeeded');
|
|
583
611
|
}
|
|
612
|
+
}).catch(error => {
|
|
613
|
+
this.debugError('HLS autoplay failed:', error);
|
|
614
|
+
this.showPlayOverlay();
|
|
615
|
+
this.setupAutoplayRetry();
|
|
584
616
|
});
|
|
585
617
|
}
|
|
586
618
|
});
|
|
@@ -746,10 +778,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
746
778
|
|
|
747
779
|
private isAutoplayRestrictionError(err: any): boolean {
|
|
748
780
|
if (!err) return false;
|
|
749
|
-
|
|
781
|
+
|
|
750
782
|
const message = (err.message || '').toLowerCase();
|
|
751
783
|
const name = (err.name || '').toLowerCase();
|
|
752
|
-
|
|
784
|
+
|
|
753
785
|
// Common autoplay restriction error patterns
|
|
754
786
|
return (
|
|
755
787
|
name === 'notallowederror' ||
|
|
@@ -762,121 +794,322 @@ export class WebPlayer extends BasePlayer {
|
|
|
762
794
|
);
|
|
763
795
|
}
|
|
764
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Detect browser autoplay capabilities
|
|
799
|
+
* Tests both muted and unmuted autoplay support
|
|
800
|
+
*/
|
|
801
|
+
private async detectAutoplayCapabilities(): Promise<void> {
|
|
802
|
+
// Cache for 5 minutes to avoid repeated checks
|
|
803
|
+
const now = Date.now();
|
|
804
|
+
if (this.autoplayCapabilities.lastCheck && (now - this.autoplayCapabilities.lastCheck) < 300000) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
// Create a temporary video element for testing
|
|
810
|
+
const testVideo = document.createElement('video');
|
|
811
|
+
testVideo.muted = true;
|
|
812
|
+
testVideo.playsInline = true;
|
|
813
|
+
testVideo.style.position = 'absolute';
|
|
814
|
+
testVideo.style.opacity = '0';
|
|
815
|
+
testVideo.style.pointerEvents = 'none';
|
|
816
|
+
testVideo.style.width = '1px';
|
|
817
|
+
testVideo.style.height = '1px';
|
|
818
|
+
|
|
819
|
+
// Use a minimal data URL video
|
|
820
|
+
testVideo.src = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMQAAAAhmcmVlAAAA70D=';
|
|
821
|
+
|
|
822
|
+
document.body.appendChild(testVideo);
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
// Test muted autoplay
|
|
826
|
+
await testVideo.play();
|
|
827
|
+
this.autoplayCapabilities.canAutoplayMuted = true;
|
|
828
|
+
this.autoplayCapabilities.canAutoplay = true;
|
|
829
|
+
this.debugLog('✅ Muted autoplay is supported');
|
|
830
|
+
|
|
831
|
+
// Test unmuted autoplay
|
|
832
|
+
testVideo.pause();
|
|
833
|
+
testVideo.currentTime = 0;
|
|
834
|
+
testVideo.muted = false;
|
|
835
|
+
testVideo.volume = 0.5;
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
await testVideo.play();
|
|
839
|
+
this.autoplayCapabilities.canAutoplayUnmuted = true;
|
|
840
|
+
this.debugLog('✅ Unmuted autoplay is supported');
|
|
841
|
+
} catch (unmutedError) {
|
|
842
|
+
this.autoplayCapabilities.canAutoplayUnmuted = false;
|
|
843
|
+
this.debugLog('⚠️ Unmuted autoplay is blocked');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
testVideo.pause();
|
|
847
|
+
} catch (error) {
|
|
848
|
+
this.autoplayCapabilities.canAutoplay = false;
|
|
849
|
+
this.autoplayCapabilities.canAutoplayMuted = false;
|
|
850
|
+
this.autoplayCapabilities.canAutoplayUnmuted = false;
|
|
851
|
+
this.debugLog('❌ All autoplay is blocked');
|
|
852
|
+
} finally {
|
|
853
|
+
document.body.removeChild(testVideo);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
this.autoplayCapabilities.lastCheck = now;
|
|
857
|
+
} catch (error) {
|
|
858
|
+
this.debugError('Failed to detect autoplay capabilities:', error);
|
|
859
|
+
// Assume muted autoplay works as fallback
|
|
860
|
+
this.autoplayCapabilities.canAutoplayMuted = true;
|
|
861
|
+
this.autoplayCapabilities.canAutoplay = true;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Check if page has user activation (from navigation or interaction)
|
|
867
|
+
*/
|
|
868
|
+
private hasUserActivation(): boolean {
|
|
869
|
+
// Check if browser supports userActivation API
|
|
870
|
+
if (typeof navigator !== 'undefined' && (navigator as any).userActivation) {
|
|
871
|
+
const hasActivation = (navigator as any).userActivation.hasBeenActive;
|
|
872
|
+
this.debugLog(`🎯 User activation detected: ${hasActivation}`);
|
|
873
|
+
return hasActivation;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Fallback: Check if user has interacted with the page
|
|
877
|
+
const hasInteracted = this.lastUserInteraction > 0 &&
|
|
878
|
+
(Date.now() - this.lastUserInteraction) < 5000;
|
|
879
|
+
|
|
880
|
+
this.debugLog(`🎯 Recent user interaction: ${hasInteracted}`);
|
|
881
|
+
return hasInteracted;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Attempt intelligent autoplay based on detected capabilities
|
|
886
|
+
*/
|
|
887
|
+
private async attemptIntelligentAutoplay(): Promise<boolean> {
|
|
888
|
+
if (!this.config.autoPlay || !this.video) return false;
|
|
889
|
+
|
|
890
|
+
// Detect capabilities first
|
|
891
|
+
await this.detectAutoplayCapabilities();
|
|
892
|
+
|
|
893
|
+
// Check if user has activated the page (navigation counts as activation)
|
|
894
|
+
const hasActivation = this.hasUserActivation();
|
|
895
|
+
|
|
896
|
+
// Try unmuted autoplay if:
|
|
897
|
+
// 1. Browser supports unmuted autoplay OR user has activated the page
|
|
898
|
+
// 2. User hasn't explicitly set muted=true
|
|
899
|
+
const shouldTryUnmuted = (this.autoplayCapabilities.canAutoplayUnmuted || hasActivation)
|
|
900
|
+
&& this.config.muted !== true;
|
|
901
|
+
|
|
902
|
+
if (shouldTryUnmuted) {
|
|
903
|
+
this.video.muted = false;
|
|
904
|
+
this.video.volume = this.config.volume ?? 1.0;
|
|
905
|
+
this.debugLog(`🔊 Attempting unmuted autoplay (activation: ${hasActivation})`);
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
await this.play();
|
|
909
|
+
this.debugLog('✅ Unmuted autoplay successful');
|
|
910
|
+
return true;
|
|
911
|
+
} catch (error) {
|
|
912
|
+
this.debugLog('⚠️ Unmuted autoplay failed, trying muted');
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Fall back to muted autoplay
|
|
917
|
+
if (this.autoplayCapabilities.canAutoplayMuted || hasActivation) {
|
|
918
|
+
this.video.muted = true;
|
|
919
|
+
this.debugLog('🔇 Attempting muted autoplay');
|
|
920
|
+
|
|
921
|
+
try {
|
|
922
|
+
await this.play();
|
|
923
|
+
this.debugLog('✅ Muted autoplay successful');
|
|
924
|
+
return true;
|
|
925
|
+
} catch (error) {
|
|
926
|
+
this.debugLog('❌ Muted autoplay failed');
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Set up intelligent autoplay retry on user interaction
|
|
935
|
+
*/
|
|
936
|
+
private setupAutoplayRetry(): void {
|
|
937
|
+
if (!this.config.autoPlay || this.autoplayRetryAttempts >= this.maxAutoplayRetries) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const interactionEvents = ['click', 'mousedown', 'keydown', 'touchstart'];
|
|
942
|
+
|
|
943
|
+
const retryAutoplay = async () => {
|
|
944
|
+
if (this.autoplayRetryPending || this.state.isPlaying) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
this.autoplayRetryPending = true;
|
|
949
|
+
this.autoplayRetryAttempts++;
|
|
950
|
+
this.debugLog(`🔄 Attempting autoplay retry #${this.autoplayRetryAttempts}`);
|
|
951
|
+
|
|
952
|
+
try {
|
|
953
|
+
const success = await this.attemptIntelligentAutoplay();
|
|
954
|
+
if (success) {
|
|
955
|
+
this.debugLog('✅ Autoplay retry successful');
|
|
956
|
+
this.autoplayRetryPending = false;
|
|
957
|
+
// Remove event listeners after success
|
|
958
|
+
interactionEvents.forEach(eventType => {
|
|
959
|
+
document.removeEventListener(eventType, retryAutoplay);
|
|
960
|
+
});
|
|
961
|
+
} else {
|
|
962
|
+
this.autoplayRetryPending = false;
|
|
963
|
+
}
|
|
964
|
+
} catch (error) {
|
|
965
|
+
this.autoplayRetryPending = false;
|
|
966
|
+
this.debugError('Autoplay retry failed:', error);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
interactionEvents.forEach(eventType => {
|
|
971
|
+
document.addEventListener(eventType, retryAutoplay, { once: true, passive: true });
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
this.debugLog('🎯 Autoplay retry armed - waiting for user interaction');
|
|
975
|
+
}
|
|
976
|
+
|
|
765
977
|
private showPlayOverlay(): void {
|
|
766
978
|
// Remove existing overlay
|
|
767
979
|
this.hidePlayOverlay();
|
|
768
|
-
|
|
980
|
+
|
|
981
|
+
this.debugLog('📺 Showing play overlay due to autoplay restriction');
|
|
982
|
+
|
|
769
983
|
const overlay = document.createElement('div');
|
|
770
984
|
overlay.id = 'uvf-play-overlay';
|
|
771
985
|
overlay.className = 'uvf-play-overlay';
|
|
772
|
-
|
|
986
|
+
|
|
773
987
|
const playButton = document.createElement('button');
|
|
774
988
|
playButton.className = 'uvf-play-button';
|
|
989
|
+
playButton.setAttribute('aria-label', 'Play video');
|
|
775
990
|
playButton.innerHTML = `
|
|
776
991
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
777
992
|
<path d="M8 5v14l11-7z"/>
|
|
778
993
|
</svg>
|
|
779
994
|
`;
|
|
780
|
-
|
|
995
|
+
|
|
781
996
|
const message = document.createElement('div');
|
|
782
997
|
message.className = 'uvf-play-message';
|
|
783
998
|
message.textContent = 'Click to play';
|
|
784
|
-
|
|
999
|
+
|
|
785
1000
|
overlay.appendChild(playButton);
|
|
786
1001
|
overlay.appendChild(message);
|
|
787
|
-
|
|
788
|
-
//
|
|
789
|
-
|
|
1002
|
+
|
|
1003
|
+
// Enhanced click handler with better error handling
|
|
1004
|
+
const handlePlayClick = async (e: Event) => {
|
|
1005
|
+
e.preventDefault();
|
|
790
1006
|
e.stopPropagation();
|
|
791
1007
|
this.lastUserInteraction = Date.now();
|
|
792
|
-
|
|
1008
|
+
this.debugLog('▶️ User clicked play overlay');
|
|
1009
|
+
|
|
793
1010
|
try {
|
|
1011
|
+
// Ensure video is ready
|
|
1012
|
+
if (!this.video) {
|
|
1013
|
+
this.debugError('Video element not available');
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Try to play
|
|
794
1018
|
await this.play();
|
|
1019
|
+
this.debugLog('✅ Play successful after user click');
|
|
795
1020
|
} catch (error) {
|
|
796
|
-
this.debugError('Failed to play after user interaction:', error);
|
|
1021
|
+
this.debugError('❌ Failed to play after user interaction:', error);
|
|
1022
|
+
// Show error message on overlay
|
|
1023
|
+
message.textContent = 'Unable to play. Please try again.';
|
|
1024
|
+
message.style.color = '#ff6b6b';
|
|
797
1025
|
}
|
|
798
|
-
}
|
|
799
|
-
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// Add click handler to button
|
|
1029
|
+
playButton.addEventListener('click', handlePlayClick);
|
|
1030
|
+
|
|
800
1031
|
// Also allow clicking anywhere on overlay
|
|
801
1032
|
overlay.addEventListener('click', async (e) => {
|
|
802
1033
|
if (e.target === overlay) {
|
|
803
|
-
e
|
|
804
|
-
this.lastUserInteraction = Date.now();
|
|
805
|
-
|
|
806
|
-
try {
|
|
807
|
-
await this.play();
|
|
808
|
-
} catch (error) {
|
|
809
|
-
this.debugError('Failed to play after user interaction:', error);
|
|
810
|
-
}
|
|
1034
|
+
await handlePlayClick(e);
|
|
811
1035
|
}
|
|
812
1036
|
});
|
|
813
|
-
|
|
814
|
-
// Add styles
|
|
1037
|
+
|
|
1038
|
+
// Add enhanced styles with better visibility
|
|
815
1039
|
const style = document.createElement('style');
|
|
816
1040
|
style.textContent = `
|
|
817
1041
|
.uvf-play-overlay {
|
|
818
|
-
position: absolute;
|
|
819
|
-
top: 0;
|
|
820
|
-
left: 0;
|
|
821
|
-
width: 100
|
|
822
|
-
height: 100
|
|
823
|
-
background: rgba(0, 0, 0, 0.
|
|
824
|
-
display: flex;
|
|
825
|
-
flex-direction: column;
|
|
826
|
-
justify-content: center;
|
|
827
|
-
align-items: center;
|
|
828
|
-
z-index:
|
|
829
|
-
cursor: pointer;
|
|
1042
|
+
position: absolute !important;
|
|
1043
|
+
top: 0 !important;
|
|
1044
|
+
left: 0 !important;
|
|
1045
|
+
width: 100% !important;
|
|
1046
|
+
height: 100% !important;
|
|
1047
|
+
background: rgba(0, 0, 0, 0.85) !important;
|
|
1048
|
+
display: flex !important;
|
|
1049
|
+
flex-direction: column !important;
|
|
1050
|
+
justify-content: center !important;
|
|
1051
|
+
align-items: center !important;
|
|
1052
|
+
z-index: 999999 !important;
|
|
1053
|
+
cursor: pointer !important;
|
|
1054
|
+
backdrop-filter: blur(4px);
|
|
1055
|
+
-webkit-backdrop-filter: blur(4px);
|
|
830
1056
|
}
|
|
831
|
-
|
|
1057
|
+
|
|
832
1058
|
.uvf-play-button {
|
|
833
|
-
width:
|
|
834
|
-
height:
|
|
835
|
-
border-radius: 50
|
|
836
|
-
background: rgba(255, 255, 255, 0.
|
|
837
|
-
border:
|
|
838
|
-
color: #000;
|
|
839
|
-
cursor: pointer;
|
|
840
|
-
display: flex;
|
|
841
|
-
align-items: center;
|
|
842
|
-
justify-content: center;
|
|
843
|
-
transition: all 0.3s ease;
|
|
844
|
-
margin-bottom:
|
|
1059
|
+
width: 96px !important;
|
|
1060
|
+
height: 96px !important;
|
|
1061
|
+
border-radius: 50% !important;
|
|
1062
|
+
background: rgba(255, 255, 255, 0.95) !important;
|
|
1063
|
+
border: 3px solid rgba(255, 255, 255, 0.3) !important;
|
|
1064
|
+
color: #000 !important;
|
|
1065
|
+
cursor: pointer !important;
|
|
1066
|
+
display: flex !important;
|
|
1067
|
+
align-items: center !important;
|
|
1068
|
+
justify-content: center !important;
|
|
1069
|
+
transition: all 0.3s ease !important;
|
|
1070
|
+
margin-bottom: 20px !important;
|
|
1071
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
|
845
1072
|
}
|
|
846
|
-
|
|
1073
|
+
|
|
847
1074
|
.uvf-play-button:hover {
|
|
848
|
-
background: #fff;
|
|
849
|
-
transform: scale(1.
|
|
1075
|
+
background: #fff !important;
|
|
1076
|
+
transform: scale(1.15) !important;
|
|
1077
|
+
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4) !important;
|
|
850
1078
|
}
|
|
851
|
-
|
|
1079
|
+
|
|
852
1080
|
.uvf-play-button svg {
|
|
853
|
-
width:
|
|
854
|
-
height:
|
|
855
|
-
margin-left: 4px;
|
|
1081
|
+
width: 40px !important;
|
|
1082
|
+
height: 40px !important;
|
|
1083
|
+
margin-left: 4px !important;
|
|
856
1084
|
}
|
|
857
|
-
|
|
1085
|
+
|
|
858
1086
|
.uvf-play-message {
|
|
859
|
-
color: white;
|
|
860
|
-
font-size:
|
|
861
|
-
font-weight:
|
|
862
|
-
text-align: center;
|
|
863
|
-
opacity: 0.
|
|
1087
|
+
color: white !important;
|
|
1088
|
+
font-size: 18px !important;
|
|
1089
|
+
font-weight: 600 !important;
|
|
1090
|
+
text-align: center !important;
|
|
1091
|
+
opacity: 0.95 !important;
|
|
1092
|
+
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5) !important;
|
|
864
1093
|
}
|
|
865
1094
|
`;
|
|
866
|
-
|
|
1095
|
+
|
|
867
1096
|
// Add to page if not already added
|
|
868
1097
|
if (!document.getElementById('uvf-play-overlay-styles')) {
|
|
869
1098
|
style.id = 'uvf-play-overlay-styles';
|
|
870
1099
|
document.head.appendChild(style);
|
|
871
1100
|
}
|
|
872
|
-
|
|
1101
|
+
|
|
873
1102
|
// Add to player
|
|
874
1103
|
if (this.playerWrapper) {
|
|
875
1104
|
this.playerWrapper.appendChild(overlay);
|
|
1105
|
+
this.debugLog('✅ Play overlay added to player wrapper');
|
|
1106
|
+
} else {
|
|
1107
|
+
this.debugError('❌ Cannot show play overlay - playerWrapper not found');
|
|
876
1108
|
}
|
|
877
1109
|
}
|
|
878
1110
|
|
|
879
1111
|
private hidePlayOverlay(): void {
|
|
1112
|
+
this.debugLog('🔇 Hiding play overlay');
|
|
880
1113
|
const overlay = document.getElementById('uvf-play-overlay');
|
|
881
1114
|
if (overlay) {
|
|
882
1115
|
overlay.remove();
|
|
@@ -130,6 +130,9 @@ export class ChapterManager {
|
|
|
130
130
|
const nextSegment = this.getNextContentSegment(currentSegment);
|
|
131
131
|
const targetTime = nextSegment ? nextSegment.startTime : currentSegment.endTime;
|
|
132
132
|
|
|
133
|
+
// Store current playback state
|
|
134
|
+
const wasPlaying = !this.videoElement.paused;
|
|
135
|
+
|
|
133
136
|
// Emit skip event
|
|
134
137
|
this.emit('segmentSkipped', {
|
|
135
138
|
fromSegment: currentSegment,
|
|
@@ -140,6 +143,19 @@ export class ChapterManager {
|
|
|
140
143
|
|
|
141
144
|
// Seek to target time
|
|
142
145
|
this.videoElement.currentTime = targetTime;
|
|
146
|
+
|
|
147
|
+
// Resume playback if video was playing before skip (better UX)
|
|
148
|
+
const shouldResumePlayback = this.config.userPreferences?.resumePlaybackAfterSkip !== false;
|
|
149
|
+
if (shouldResumePlayback && wasPlaying && this.videoElement.paused) {
|
|
150
|
+
// Use a small delay to ensure seeking is complete
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
if (!this.videoElement.paused) return; // Don't play if already playing
|
|
153
|
+
this.videoElement.play().catch(() => {
|
|
154
|
+
// Handle autoplay restrictions gracefully
|
|
155
|
+
console.warn('[ChapterManager] Could not resume playback after skip - user interaction may be required');
|
|
156
|
+
});
|
|
157
|
+
}, 50);
|
|
158
|
+
}
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
/**
|
|
@@ -153,6 +169,9 @@ export class ChapterManager {
|
|
|
153
169
|
|
|
154
170
|
const fromSegment = this.currentSegment;
|
|
155
171
|
|
|
172
|
+
// Store current playback state
|
|
173
|
+
const wasPlaying = !this.videoElement.paused;
|
|
174
|
+
|
|
156
175
|
// Emit skip event
|
|
157
176
|
if (fromSegment) {
|
|
158
177
|
this.emit('segmentSkipped', {
|
|
@@ -165,6 +184,19 @@ export class ChapterManager {
|
|
|
165
184
|
|
|
166
185
|
// Seek to segment start
|
|
167
186
|
this.videoElement.currentTime = segment.startTime;
|
|
187
|
+
|
|
188
|
+
// Resume playback if video was playing before skip (better UX)
|
|
189
|
+
const shouldResumePlayback = this.config.userPreferences?.resumePlaybackAfterSkip !== false;
|
|
190
|
+
if (shouldResumePlayback && wasPlaying && this.videoElement.paused) {
|
|
191
|
+
// Use a small delay to ensure seeking is complete
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
if (!this.videoElement.paused) return; // Don't play if already playing
|
|
194
|
+
this.videoElement.play().catch(() => {
|
|
195
|
+
// Handle autoplay restrictions gracefully
|
|
196
|
+
console.warn('[ChapterManager] Could not resume playback after skip - user interaction may be required');
|
|
197
|
+
});
|
|
198
|
+
}, 50);
|
|
199
|
+
}
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
/**
|
|
@@ -119,6 +119,9 @@ export interface ChapterPreferences {
|
|
|
119
119
|
|
|
120
120
|
/** Remember user choices */
|
|
121
121
|
rememberChoices?: boolean;
|
|
122
|
+
|
|
123
|
+
/** Resume playback after skip (default: true for better UX) */
|
|
124
|
+
resumePlaybackAfterSkip?: boolean;
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
/**
|
|
@@ -209,7 +212,8 @@ export const DEFAULT_CHAPTER_CONFIG: ChapterConfig = {
|
|
|
209
212
|
autoSkipCredits: false,
|
|
210
213
|
showSkipButtons: true,
|
|
211
214
|
skipButtonTimeout: 5000,
|
|
212
|
-
rememberChoices: true
|
|
215
|
+
rememberChoices: true,
|
|
216
|
+
resumePlaybackAfterSkip: true
|
|
213
217
|
}
|
|
214
218
|
};
|
|
215
219
|
|
|
@@ -211,6 +211,7 @@ export type WebPlayerViewProps = {
|
|
|
211
211
|
showSkipButtons?: boolean; // Show skip buttons (default: true)
|
|
212
212
|
skipButtonTimeout?: number; // Button timeout in milliseconds (default: 5000)
|
|
213
213
|
rememberChoices?: boolean; // Remember user preferences (default: true)
|
|
214
|
+
resumePlaybackAfterSkip?: boolean; // Resume playback after skipping (default: true)
|
|
214
215
|
};
|
|
215
216
|
};
|
|
216
217
|
|
|
@@ -569,6 +570,7 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
569
570
|
showSkipButtons: props.chapters.userPreferences?.showSkipButtons ?? true,
|
|
570
571
|
skipButtonTimeout: props.chapters.userPreferences?.skipButtonTimeout ?? 5000,
|
|
571
572
|
rememberChoices: props.chapters.userPreferences?.rememberChoices ?? true,
|
|
573
|
+
resumePlaybackAfterSkip: props.chapters.userPreferences?.resumePlaybackAfterSkip ?? true,
|
|
572
574
|
}
|
|
573
575
|
} : { enabled: false }
|
|
574
576
|
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Chapter, ChapterSegment, ChapterConfig, EventHandler } from './interfaces';
|
|
2
|
-
export interface ChapterManagerEvents {
|
|
3
|
-
chapterchange: Chapter | null;
|
|
4
|
-
segmententered: ChapterSegment;
|
|
5
|
-
segmentexited: ChapterSegment;
|
|
6
|
-
segmentskipped: ChapterSegment;
|
|
7
|
-
}
|
|
8
|
-
export declare class ChapterManager {
|
|
9
|
-
private config;
|
|
10
|
-
private chapters;
|
|
11
|
-
private segments;
|
|
12
|
-
private currentChapter;
|
|
13
|
-
private activeSegments;
|
|
14
|
-
private lastProcessedTime;
|
|
15
|
-
private eventHandlers;
|
|
16
|
-
constructor(config?: ChapterConfig);
|
|
17
|
-
updateConfig(config: ChapterConfig): void;
|
|
18
|
-
loadChapterData(url: string): Promise<void>;
|
|
19
|
-
initialize(): Promise<void>;
|
|
20
|
-
processTimeUpdate(currentTime: number): void;
|
|
21
|
-
private processChapterChange;
|
|
22
|
-
private processSegments;
|
|
23
|
-
getCurrentChapter(currentTime: number): Chapter | null;
|
|
24
|
-
getChapters(): Chapter[];
|
|
25
|
-
getSegments(): ChapterSegment[];
|
|
26
|
-
getSegmentsAtTime(currentTime: number): ChapterSegment[];
|
|
27
|
-
skipSegment(segment: ChapterSegment): void;
|
|
28
|
-
seekToChapter(chapterId: string): Chapter | null;
|
|
29
|
-
getNextChapter(currentTime: number): Chapter | null;
|
|
30
|
-
getPreviousChapter(currentTime: number): Chapter | null;
|
|
31
|
-
on<K extends keyof ChapterManagerEvents>(event: K, handler: EventHandler): void;
|
|
32
|
-
off<K extends keyof ChapterManagerEvents>(event: K, handler?: EventHandler): void;
|
|
33
|
-
private emit;
|
|
34
|
-
reset(): void;
|
|
35
|
-
destroy(): void;
|
|
36
|
-
getCurrentChapterInfo(): Chapter | null;
|
|
37
|
-
isEnabled(): boolean;
|
|
38
|
-
}
|
|
39
|
-
//# sourceMappingURL=chapter-manager.d.ts.map
|