unified-video-framework 1.4.155 → 1.4.157

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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/packages/web/dist/WebPlayer.d.ts +7 -0
  3. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  4. package/packages/web/dist/WebPlayer.js +205 -54
  5. package/packages/web/dist/WebPlayer.js.map +1 -1
  6. package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -1
  7. package/packages/web/dist/chapters/ChapterManager.js +22 -0
  8. package/packages/web/dist/chapters/ChapterManager.js.map +1 -1
  9. package/packages/web/dist/chapters/types/ChapterTypes.d.ts +1 -0
  10. package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -1
  11. package/packages/web/dist/chapters/types/ChapterTypes.js +2 -1
  12. package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -1
  13. package/packages/web/dist/react/WebPlayerView.d.ts +80 -0
  14. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  15. package/packages/web/dist/react/WebPlayerView.js +50 -1
  16. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  17. package/packages/web/src/WebPlayer.ts +277 -71
  18. package/packages/web/src/chapters/ChapterManager.ts +32 -0
  19. package/packages/web/src/chapters/types/ChapterTypes.ts +5 -1
  20. package/packages/web/src/react/WebPlayerView.tsx +118 -1
  21. package/packages/core/dist/chapter-manager.d.ts +0 -39
  22. package/packages/ios/README.md +0 -84
  23. package/packages/web/dist/HTML5Player.js.map +0 -1
  24. package/packages/web/dist/epg/EPGController.d.ts +0 -78
  25. package/packages/web/dist/epg/EPGController.d.ts.map +0 -1
  26. package/packages/web/dist/epg/EPGController.js +0 -476
  27. 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,8 +213,9 @@ 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
- // For autoplay to work, video must be muted in most browsers
201
- this.video.autoplay = this.config.autoPlay ?? false;
216
+ // Don't set autoplay attribute - we'll handle it programmatically with intelligent detection
217
+ this.video.autoplay = false;
218
+ // Start muted if autoplay is enabled (browser policy), but we'll try unmuted if supported
202
219
  this.video.muted = this.config.autoPlay ? true : (this.config.muted ?? false);
203
220
  this.video.loop = this.config.loop ?? false;
204
221
  this.video.playsInline = this.config.playsInline ?? true;
@@ -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
- // Attempt autoplay, but handle gracefully if blocked
577
- this.play().catch(error => {
578
- if (this.isAutoplayRestrictionError(error)) {
579
- this.debugWarn('HLS autoplay blocked, showing play overlay');
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.debugError('HLS autoplay failed:', error);
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,295 @@ 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
+ * Attempt intelligent autoplay based on detected capabilities
867
+ */
868
+ private async attemptIntelligentAutoplay(): Promise<boolean> {
869
+ if (!this.config.autoPlay || !this.video) return false;
870
+
871
+ // Detect capabilities first
872
+ await this.detectAutoplayCapabilities();
873
+
874
+ // Try unmuted autoplay if supported and not explicitly muted
875
+ if (this.autoplayCapabilities.canAutoplayUnmuted && !this.config.muted) {
876
+ this.video.muted = false;
877
+ this.video.volume = this.config.volume ?? 1.0;
878
+ this.debugLog('🔊 Attempting unmuted autoplay');
879
+
880
+ try {
881
+ await this.play();
882
+ this.debugLog('✅ Unmuted autoplay successful');
883
+ return true;
884
+ } catch (error) {
885
+ this.debugLog('⚠️ Unmuted autoplay failed, trying muted');
886
+ }
887
+ }
888
+
889
+ // Fall back to muted autoplay
890
+ if (this.autoplayCapabilities.canAutoplayMuted) {
891
+ this.video.muted = true;
892
+ this.debugLog('🔇 Attempting muted autoplay');
893
+
894
+ try {
895
+ await this.play();
896
+ this.debugLog('✅ Muted autoplay successful');
897
+ return true;
898
+ } catch (error) {
899
+ this.debugLog('❌ Muted autoplay failed');
900
+ }
901
+ }
902
+
903
+ return false;
904
+ }
905
+
906
+ /**
907
+ * Set up intelligent autoplay retry on user interaction
908
+ */
909
+ private setupAutoplayRetry(): void {
910
+ if (!this.config.autoPlay || this.autoplayRetryAttempts >= this.maxAutoplayRetries) {
911
+ return;
912
+ }
913
+
914
+ const interactionEvents = ['click', 'mousedown', 'keydown', 'touchstart'];
915
+
916
+ const retryAutoplay = async () => {
917
+ if (this.autoplayRetryPending || this.state.isPlaying) {
918
+ return;
919
+ }
920
+
921
+ this.autoplayRetryPending = true;
922
+ this.autoplayRetryAttempts++;
923
+ this.debugLog(`🔄 Attempting autoplay retry #${this.autoplayRetryAttempts}`);
924
+
925
+ try {
926
+ const success = await this.attemptIntelligentAutoplay();
927
+ if (success) {
928
+ this.debugLog('✅ Autoplay retry successful');
929
+ this.autoplayRetryPending = false;
930
+ // Remove event listeners after success
931
+ interactionEvents.forEach(eventType => {
932
+ document.removeEventListener(eventType, retryAutoplay);
933
+ });
934
+ } else {
935
+ this.autoplayRetryPending = false;
936
+ }
937
+ } catch (error) {
938
+ this.autoplayRetryPending = false;
939
+ this.debugError('Autoplay retry failed:', error);
940
+ }
941
+ };
942
+
943
+ interactionEvents.forEach(eventType => {
944
+ document.addEventListener(eventType, retryAutoplay, { once: true, passive: true });
945
+ });
946
+
947
+ this.debugLog('🎯 Autoplay retry armed - waiting for user interaction');
948
+ }
949
+
765
950
  private showPlayOverlay(): void {
766
951
  // Remove existing overlay
767
952
  this.hidePlayOverlay();
768
-
953
+
954
+ this.debugLog('📺 Showing play overlay due to autoplay restriction');
955
+
769
956
  const overlay = document.createElement('div');
770
957
  overlay.id = 'uvf-play-overlay';
771
958
  overlay.className = 'uvf-play-overlay';
772
-
959
+
773
960
  const playButton = document.createElement('button');
774
961
  playButton.className = 'uvf-play-button';
962
+ playButton.setAttribute('aria-label', 'Play video');
775
963
  playButton.innerHTML = `
776
964
  <svg viewBox="0 0 24 24" fill="currentColor">
777
965
  <path d="M8 5v14l11-7z"/>
778
966
  </svg>
779
967
  `;
780
-
968
+
781
969
  const message = document.createElement('div');
782
970
  message.className = 'uvf-play-message';
783
971
  message.textContent = 'Click to play';
784
-
972
+
785
973
  overlay.appendChild(playButton);
786
974
  overlay.appendChild(message);
787
-
788
- // Add click handler
789
- playButton.addEventListener('click', async (e) => {
975
+
976
+ // Enhanced click handler with better error handling
977
+ const handlePlayClick = async (e: Event) => {
978
+ e.preventDefault();
790
979
  e.stopPropagation();
791
980
  this.lastUserInteraction = Date.now();
792
-
981
+ this.debugLog('▶️ User clicked play overlay');
982
+
793
983
  try {
984
+ // Ensure video is ready
985
+ if (!this.video) {
986
+ this.debugError('Video element not available');
987
+ return;
988
+ }
989
+
990
+ // Try to play
794
991
  await this.play();
992
+ this.debugLog('✅ Play successful after user click');
795
993
  } catch (error) {
796
- this.debugError('Failed to play after user interaction:', error);
994
+ this.debugError('Failed to play after user interaction:', error);
995
+ // Show error message on overlay
996
+ message.textContent = 'Unable to play. Please try again.';
997
+ message.style.color = '#ff6b6b';
797
998
  }
798
- });
799
-
999
+ };
1000
+
1001
+ // Add click handler to button
1002
+ playButton.addEventListener('click', handlePlayClick);
1003
+
800
1004
  // Also allow clicking anywhere on overlay
801
1005
  overlay.addEventListener('click', async (e) => {
802
1006
  if (e.target === overlay) {
803
- e.stopPropagation();
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
- }
1007
+ await handlePlayClick(e);
811
1008
  }
812
1009
  });
813
-
814
- // Add styles
1010
+
1011
+ // Add enhanced styles with better visibility
815
1012
  const style = document.createElement('style');
816
1013
  style.textContent = `
817
1014
  .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.7);
824
- display: flex;
825
- flex-direction: column;
826
- justify-content: center;
827
- align-items: center;
828
- z-index: 1000;
829
- cursor: pointer;
1015
+ position: absolute !important;
1016
+ top: 0 !important;
1017
+ left: 0 !important;
1018
+ width: 100% !important;
1019
+ height: 100% !important;
1020
+ background: rgba(0, 0, 0, 0.85) !important;
1021
+ display: flex !important;
1022
+ flex-direction: column !important;
1023
+ justify-content: center !important;
1024
+ align-items: center !important;
1025
+ z-index: 999999 !important;
1026
+ cursor: pointer !important;
1027
+ backdrop-filter: blur(4px);
1028
+ -webkit-backdrop-filter: blur(4px);
830
1029
  }
831
-
1030
+
832
1031
  .uvf-play-button {
833
- width: 80px;
834
- height: 80px;
835
- border-radius: 50%;
836
- background: rgba(255, 255, 255, 0.9);
837
- border: none;
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: 16px;
1032
+ width: 96px !important;
1033
+ height: 96px !important;
1034
+ border-radius: 50% !important;
1035
+ background: rgba(255, 255, 255, 0.95) !important;
1036
+ border: 3px solid rgba(255, 255, 255, 0.3) !important;
1037
+ color: #000 !important;
1038
+ cursor: pointer !important;
1039
+ display: flex !important;
1040
+ align-items: center !important;
1041
+ justify-content: center !important;
1042
+ transition: all 0.3s ease !important;
1043
+ margin-bottom: 20px !important;
1044
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
845
1045
  }
846
-
1046
+
847
1047
  .uvf-play-button:hover {
848
- background: #fff;
849
- transform: scale(1.1);
1048
+ background: #fff !important;
1049
+ transform: scale(1.15) !important;
1050
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4) !important;
850
1051
  }
851
-
1052
+
852
1053
  .uvf-play-button svg {
853
- width: 32px;
854
- height: 32px;
855
- margin-left: 4px;
1054
+ width: 40px !important;
1055
+ height: 40px !important;
1056
+ margin-left: 4px !important;
856
1057
  }
857
-
1058
+
858
1059
  .uvf-play-message {
859
- color: white;
860
- font-size: 16px;
861
- font-weight: 500;
862
- text-align: center;
863
- opacity: 0.9;
1060
+ color: white !important;
1061
+ font-size: 18px !important;
1062
+ font-weight: 600 !important;
1063
+ text-align: center !important;
1064
+ opacity: 0.95 !important;
1065
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5) !important;
864
1066
  }
865
1067
  `;
866
-
1068
+
867
1069
  // Add to page if not already added
868
1070
  if (!document.getElementById('uvf-play-overlay-styles')) {
869
1071
  style.id = 'uvf-play-overlay-styles';
870
1072
  document.head.appendChild(style);
871
1073
  }
872
-
1074
+
873
1075
  // Add to player
874
1076
  if (this.playerWrapper) {
875
1077
  this.playerWrapper.appendChild(overlay);
1078
+ this.debugLog('✅ Play overlay added to player wrapper');
1079
+ } else {
1080
+ this.debugError('❌ Cannot show play overlay - playerWrapper not found');
876
1081
  }
877
1082
  }
878
1083
 
879
1084
  private hidePlayOverlay(): void {
1085
+ this.debugLog('🔇 Hiding play overlay');
880
1086
  const overlay = document.getElementById('uvf-play-overlay');
881
1087
  if (overlay) {
882
1088
  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
 
@@ -161,6 +161,71 @@ export type WebPlayerViewProps = {
161
161
  onEPGCatchup?: (program: EPGProgram, channel: EPGProgramRow) => void | Promise<void>;
162
162
  onEPGProgramSelect?: (program: EPGProgram, channel: EPGProgramRow) => void;
163
163
  onEPGChannelSelect?: (channel: EPGProgramRow) => void;
164
+
165
+ // Chapter & Skip Configuration
166
+ chapters?: {
167
+ enabled?: boolean; // Enable/disable chapters (default: false)
168
+ data?: { // Chapter data
169
+ videoId: string;
170
+ duration: number;
171
+ segments: Array<{
172
+ id: string;
173
+ type: 'intro' | 'recap' | 'content' | 'credits' | 'ad' | 'sponsor' | 'offensive';
174
+ startTime: number;
175
+ endTime: number;
176
+ title: string;
177
+ skipLabel?: string; // Custom skip button text
178
+ description?: string;
179
+ thumbnail?: string;
180
+ autoSkip?: boolean; // Enable auto-skip for this segment
181
+ autoSkipDelay?: number; // Countdown delay in seconds
182
+ metadata?: Record<string, any>;
183
+ }>;
184
+ };
185
+ dataUrl?: string; // URL to fetch chapters from API
186
+ autoHide?: boolean; // Auto-hide skip button after showing (default: true)
187
+ autoHideDelay?: number; // Hide delay in milliseconds (default: 5000)
188
+ showChapterMarkers?: boolean; // Show progress bar markers (default: true)
189
+ skipButtonPosition?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; // Default: 'bottom-right'
190
+ customStyles?: { // Custom styling for skip elements
191
+ skipButton?: {
192
+ backgroundColor?: string;
193
+ borderColor?: string;
194
+ textColor?: string;
195
+ fontSize?: string;
196
+ borderRadius?: string;
197
+ padding?: string;
198
+ fontWeight?: string;
199
+ };
200
+ progressMarkers?: {
201
+ intro?: string; // Color for intro markers
202
+ recap?: string; // Color for recap markers
203
+ credits?: string; // Color for credits markers
204
+ ad?: string; // Color for ad markers
205
+ };
206
+ };
207
+ userPreferences?: { // User skip preferences
208
+ autoSkipIntro?: boolean; // Auto-skip intro segments (default: false)
209
+ autoSkipRecap?: boolean; // Auto-skip recap segments (default: false)
210
+ autoSkipCredits?: boolean; // Auto-skip credits segments (default: false)
211
+ showSkipButtons?: boolean; // Show skip buttons (default: true)
212
+ skipButtonTimeout?: number; // Button timeout in milliseconds (default: 5000)
213
+ rememberChoices?: boolean; // Remember user preferences (default: true)
214
+ resumePlaybackAfterSkip?: boolean; // Resume playback after skipping (default: true)
215
+ };
216
+ };
217
+
218
+ // Chapter Event Callbacks
219
+ onChapterChange?: (chapter: any) => void; // Core chapter changed
220
+ onSegmentEntered?: (segment: any) => void; // Segment entered
221
+ onSegmentExited?: (segment: any) => void; // Segment exited
222
+ onSegmentSkipped?: (segment: any) => void; // Segment skipped
223
+ onChapterSegmentEntered?: (data: { segment: any; timestamp: number }) => void; // Web-specific segment entered
224
+ onChapterSegmentSkipped?: (data: { fromSegment: any; toSegment?: any; timestamp: number }) => void; // Web-specific segment skipped
225
+ onChapterSkipButtonShown?: (data: { segment: any; position: string }) => void; // Skip button shown
226
+ onChapterSkipButtonHidden?: (data: { segment: any; reason: string }) => void; // Skip button hidden
227
+ onChaptersLoaded?: (data: { segmentCount: number; chapters: any[] }) => void; // Chapters loaded
228
+ onChaptersLoadError?: (data: { error: Error; url?: string }) => void; // Chapters load error
164
229
  };
165
230
 
166
231
  export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
@@ -487,7 +552,27 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
487
552
  customControls: props.customControls,
488
553
  settings: props.settings,
489
554
  showFrameworkBranding: props.showFrameworkBranding,
490
- watermark: watermarkConfig
555
+ watermark: watermarkConfig,
556
+ // Chapter configuration
557
+ chapters: props.chapters ? {
558
+ enabled: props.chapters.enabled ?? false,
559
+ data: props.chapters.data,
560
+ dataUrl: props.chapters.dataUrl,
561
+ autoHide: props.chapters.autoHide ?? true,
562
+ autoHideDelay: props.chapters.autoHideDelay ?? 5000,
563
+ showChapterMarkers: props.chapters.showChapterMarkers ?? true,
564
+ skipButtonPosition: props.chapters.skipButtonPosition ?? 'bottom-right',
565
+ customStyles: props.chapters.customStyles,
566
+ userPreferences: {
567
+ autoSkipIntro: props.chapters.userPreferences?.autoSkipIntro ?? false,
568
+ autoSkipRecap: props.chapters.userPreferences?.autoSkipRecap ?? false,
569
+ autoSkipCredits: props.chapters.userPreferences?.autoSkipCredits ?? false,
570
+ showSkipButtons: props.chapters.userPreferences?.showSkipButtons ?? true,
571
+ skipButtonTimeout: props.chapters.userPreferences?.skipButtonTimeout ?? 5000,
572
+ rememberChoices: props.chapters.userPreferences?.rememberChoices ?? true,
573
+ resumePlaybackAfterSkip: props.chapters.userPreferences?.resumePlaybackAfterSkip ?? true,
574
+ }
575
+ } : { enabled: false }
491
576
  };
492
577
 
493
578
  try {
@@ -523,6 +608,38 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
523
608
  });
524
609
  }
525
610
 
611
+ // Chapter event listeners
612
+ if (props.onChapterChange && typeof (player as any).on === 'function') {
613
+ (player as any).on('chapterchange', props.onChapterChange);
614
+ }
615
+ if (props.onSegmentEntered && typeof (player as any).on === 'function') {
616
+ (player as any).on('segmententered', props.onSegmentEntered);
617
+ }
618
+ if (props.onSegmentExited && typeof (player as any).on === 'function') {
619
+ (player as any).on('segmentexited', props.onSegmentExited);
620
+ }
621
+ if (props.onSegmentSkipped && typeof (player as any).on === 'function') {
622
+ (player as any).on('segmentskipped', props.onSegmentSkipped);
623
+ }
624
+ if (props.onChapterSegmentEntered && typeof (player as any).on === 'function') {
625
+ (player as any).on('chapterSegmentEntered', props.onChapterSegmentEntered);
626
+ }
627
+ if (props.onChapterSegmentSkipped && typeof (player as any).on === 'function') {
628
+ (player as any).on('chapterSegmentSkipped', props.onChapterSegmentSkipped);
629
+ }
630
+ if (props.onChapterSkipButtonShown && typeof (player as any).on === 'function') {
631
+ (player as any).on('chapterSkipButtonShown', props.onChapterSkipButtonShown);
632
+ }
633
+ if (props.onChapterSkipButtonHidden && typeof (player as any).on === 'function') {
634
+ (player as any).on('chapterSkipButtonHidden', props.onChapterSkipButtonHidden);
635
+ }
636
+ if (props.onChaptersLoaded && typeof (player as any).on === 'function') {
637
+ (player as any).on('chaptersLoaded', props.onChaptersLoaded);
638
+ }
639
+ if (props.onChaptersLoadError && typeof (player as any).on === 'function') {
640
+ (player as any).on('chaptersLoadError', props.onChaptersLoadError);
641
+ }
642
+
526
643
  props.onReady?.(player);
527
644
  }
528
645
  } catch (err) {