unified-video-framework 1.4.234 → 1.4.236

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.
@@ -140,6 +140,15 @@ export class WebPlayer extends BasePlayer {
140
140
 
141
141
  // Premium qualities configuration
142
142
  private premiumQualities: any = null;
143
+
144
+ // Ad playing state (set by Google Ads Manager)
145
+ private isAdPlaying: boolean = false;
146
+
147
+ // Fallback source management
148
+ private fallbackSourceIndex: number = -1;
149
+ private fallbackErrors: Array<{ url: string; error: any }> = [];
150
+ private isLoadingFallback: boolean = false;
151
+ private currentRetryAttempt: number = 0;
143
152
 
144
153
  // Debug logging helper
145
154
  private debugLog(message: string, ...args: any[]): void {
@@ -560,6 +569,12 @@ export class WebPlayer extends BasePlayer {
560
569
  // Reset autoplay flag for new source
561
570
  this.autoplayAttempted = false;
562
571
 
572
+ // Reset fallback state for new source
573
+ this.fallbackSourceIndex = -1;
574
+ this.fallbackErrors = [];
575
+ this.isLoadingFallback = false;
576
+ this.currentRetryAttempt = 0;
577
+
563
578
  // Clean up previous instances
564
579
  await this.cleanup();
565
580
 
@@ -567,49 +582,254 @@ export class WebPlayer extends BasePlayer {
567
582
  throw new Error('Video element not initialized');
568
583
  }
569
584
 
585
+ // Setup error handler for automatic fallback
586
+ this.setupFallbackErrorHandler();
570
587
 
571
588
  // Detect source type
572
589
  const sourceType = this.detectSourceType(source);
573
590
 
574
591
  try {
575
- switch (sourceType) {
576
- case 'hls':
577
- await this.loadHLS(source.url);
578
- break;
579
- case 'dash':
580
- await this.loadDASH(source.url);
581
- break;
582
- default:
583
- await this.loadNative(source.url);
592
+ await this.loadVideoSource(source.url, sourceType, source);
593
+ } catch (error) {
594
+ // Try fallback sources if available
595
+ const fallbackLoaded = await this.tryFallbackSource(error);
596
+ if (!fallbackLoaded) {
597
+ this.handleError({
598
+ code: 'LOAD_ERROR',
599
+ message: `Failed to load video: ${error}`,
600
+ type: 'network',
601
+ fatal: true,
602
+ details: error
603
+ });
604
+ throw error;
584
605
  }
606
+ }
607
+ }
608
+
609
+ /**
610
+ * Load a video source (main or fallback)
611
+ */
612
+ private async loadVideoSource(url: string, sourceType: string, source: any): Promise<void> {
613
+ switch (sourceType) {
614
+ case 'hls':
615
+ await this.loadHLS(url);
616
+ break;
617
+ case 'dash':
618
+ await this.loadDASH(url);
619
+ break;
620
+ default:
621
+ await this.loadNative(url);
622
+ }
585
623
 
586
- // Load subtitles if provided
587
- if (source.subtitles && source.subtitles.length > 0) {
588
- this.loadSubtitles(source.subtitles);
624
+ // Load subtitles if provided
625
+ if (source.subtitles && source.subtitles.length > 0) {
626
+ this.loadSubtitles(source.subtitles);
627
+ }
628
+
629
+ // Apply metadata
630
+ if (source.metadata) {
631
+ if (source.metadata.posterUrl && this.video) {
632
+ this.video.poster = source.metadata.posterUrl;
589
633
  }
634
+ // Update player UI with metadata (title, description, thumbnail)
635
+ this.updateMetadataUI();
636
+ } else {
637
+ // Clear to defaults if no metadata
638
+ this.updateMetadataUI();
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Setup error handler for automatic fallback on video errors
644
+ */
645
+ private setupFallbackErrorHandler(): void {
646
+ if (!this.video) return;
647
+
648
+ // Remove existing error handlers to avoid duplicates
649
+ const newVideo = this.video.cloneNode(false) as HTMLVideoElement;
650
+ this.video.replaceWith(newVideo);
651
+ this.video = newVideo;
590
652
 
591
- // Apply metadata
592
- if (source.metadata) {
593
- if (source.metadata.posterUrl && this.video) {
594
- this.video.poster = source.metadata.posterUrl;
653
+ this.video.addEventListener('error', async (e) => {
654
+ if (this.isLoadingFallback) return; // Avoid recursive fallback attempts
655
+
656
+ const error = this.video?.error;
657
+ if (error) {
658
+ this.debugLog(`Video error detected (code: ${error.code}):`, error.message);
659
+
660
+ // Try fallback on media errors
661
+ const fallbackLoaded = await this.tryFallbackSource(error);
662
+
663
+ if (!fallbackLoaded) {
664
+ // No more fallbacks available, handle final error
665
+ this.handleError({
666
+ code: `MEDIA_ERR_${error.code}`,
667
+ message: error.message || this.getMediaErrorMessage(error.code),
668
+ type: 'media',
669
+ fatal: true,
670
+ details: error
671
+ });
595
672
  }
596
- // Update player UI with metadata (title, description, thumbnail)
597
- this.updateMetadataUI();
598
- } else {
599
- // Clear to defaults if no metadata
600
- this.updateMetadataUI();
601
673
  }
674
+ });
675
+ }
602
676
 
603
- } catch (error) {
604
- this.handleError({
605
- code: 'LOAD_ERROR',
606
- message: `Failed to load video: ${error}`,
607
- type: 'network',
608
- fatal: true,
609
- details: error
610
- });
611
- throw error;
677
+ /**
678
+ * Try loading the next fallback source
679
+ */
680
+ private async tryFallbackSource(error: any): Promise<boolean> {
681
+ if (this.isLoadingFallback) return false;
682
+ if (!this.source?.fallbackSources || this.source.fallbackSources.length === 0) {
683
+ return this.showFallbackPoster();
612
684
  }
685
+
686
+ this.isLoadingFallback = true;
687
+
688
+ // Record current error
689
+ const currentUrl = this.fallbackSourceIndex === -1
690
+ ? this.source.url
691
+ : this.source.fallbackSources[this.fallbackSourceIndex]?.url;
692
+
693
+ this.fallbackErrors.push({ url: currentUrl, error });
694
+ this.debugLog(`Source failed: ${currentUrl}`, error);
695
+
696
+ // Check retry attempts for current source
697
+ const maxRetries = this.source.fallbackRetryAttempts || 1;
698
+ if (this.currentRetryAttempt < maxRetries) {
699
+ this.currentRetryAttempt++;
700
+ this.debugLog(`Retrying source (attempt ${this.currentRetryAttempt}/${maxRetries}): ${currentUrl}`);
701
+
702
+ const retryDelay = this.source.fallbackRetryDelay || 1000;
703
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
704
+
705
+ try {
706
+ const sourceType = this.detectSourceType({ url: currentUrl, type: this.source.type });
707
+ await this.loadVideoSource(currentUrl, sourceType, this.source);
708
+ this.isLoadingFallback = false;
709
+ this.currentRetryAttempt = 0;
710
+ this.debugLog(`Retry successful for: ${currentUrl}`);
711
+ return true;
712
+ } catch (retryError) {
713
+ this.debugLog(`Retry failed for: ${currentUrl}`, retryError);
714
+ // Continue to next fallback
715
+ }
716
+ }
717
+
718
+ // Move to next fallback source
719
+ this.currentRetryAttempt = 0;
720
+ this.fallbackSourceIndex++;
721
+
722
+ if (this.fallbackSourceIndex >= this.source.fallbackSources.length) {
723
+ // All sources exhausted
724
+ this.isLoadingFallback = false;
725
+ this.debugLog('All video sources failed. Attempting to show fallback poster.');
726
+
727
+ // Trigger callback if provided
728
+ if (this.source.onAllSourcesFailed) {
729
+ try {
730
+ this.source.onAllSourcesFailed(this.fallbackErrors);
731
+ } catch (callbackError) {
732
+ console.error('Error in onAllSourcesFailed callback:', callbackError);
733
+ }
734
+ }
735
+
736
+ return this.showFallbackPoster();
737
+ }
738
+
739
+ // Try next fallback source
740
+ const fallbackSource = this.source.fallbackSources[this.fallbackSourceIndex];
741
+ this.debugLog(`Trying fallback source ${this.fallbackSourceIndex + 1}/${this.source.fallbackSources.length}: ${fallbackSource.url}`);
742
+
743
+ const retryDelay = this.source.fallbackRetryDelay || 1000;
744
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
745
+
746
+ try {
747
+ const sourceType = this.detectSourceType(fallbackSource);
748
+ await this.loadVideoSource(fallbackSource.url, sourceType, this.source);
749
+ this.isLoadingFallback = false;
750
+ this.debugLog(`Successfully loaded fallback source: ${fallbackSource.url}`);
751
+ this.showNotification(`Switched to backup source ${this.fallbackSourceIndex + 1}`);
752
+ return true;
753
+ } catch (fallbackError) {
754
+ this.debugLog(`Fallback source failed: ${fallbackSource.url}`, fallbackError);
755
+ this.isLoadingFallback = false;
756
+ // Recursively try next fallback
757
+ return this.tryFallbackSource(fallbackError);
758
+ }
759
+ }
760
+
761
+ /**
762
+ * Show fallback poster image when all video sources fail
763
+ */
764
+ private showFallbackPoster(): boolean {
765
+ if (!this.source?.fallbackPoster) {
766
+ this.debugLog('No fallback poster available');
767
+ return false;
768
+ }
769
+
770
+ this.debugLog('Showing fallback poster:', this.source.fallbackPoster);
771
+
772
+ if (this.video) {
773
+ // Hide video element, show poster
774
+ this.video.style.display = 'none';
775
+ this.video.poster = this.source.fallbackPoster;
776
+ }
777
+
778
+ // Create poster overlay
779
+ const posterOverlay = document.createElement('div');
780
+ posterOverlay.id = 'uvf-fallback-poster';
781
+ posterOverlay.style.cssText = `
782
+ position: absolute;
783
+ top: 0;
784
+ left: 0;
785
+ width: 100%;
786
+ height: 100%;
787
+ background-image: url('${this.source.fallbackPoster}');
788
+ background-size: cover;
789
+ background-position: center;
790
+ background-repeat: no-repeat;
791
+ z-index: 10;
792
+ display: flex;
793
+ align-items: center;
794
+ justify-content: center;
795
+ `;
796
+
797
+ // Add error message overlay
798
+ const errorMessage = document.createElement('div');
799
+ errorMessage.style.cssText = `
800
+ background: rgba(0, 0, 0, 0.8);
801
+ color: white;
802
+ padding: 20px 30px;
803
+ border-radius: 8px;
804
+ text-align: center;
805
+ max-width: 400px;
806
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
807
+ `;
808
+ errorMessage.innerHTML = `
809
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-bottom: 10px;">
810
+ <circle cx="12" cy="12" r="10"></circle>
811
+ <line x1="12" y1="8" x2="12" y2="12"></line>
812
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
813
+ </svg>
814
+ <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;">Video Unavailable</div>
815
+ <div style="font-size: 14px; opacity: 0.9;">This video cannot be played at the moment.</div>
816
+ `;
817
+
818
+ posterOverlay.appendChild(errorMessage);
819
+
820
+ // Remove existing fallback poster if any
821
+ const existingPoster = this.playerWrapper?.querySelector('#uvf-fallback-poster');
822
+ if (existingPoster) {
823
+ existingPoster.remove();
824
+ }
825
+
826
+ // Add to player
827
+ if (this.playerWrapper) {
828
+ this.playerWrapper.appendChild(posterOverlay);
829
+ }
830
+
831
+ this.showNotification('Video unavailable');
832
+ return true;
613
833
  }
614
834
 
615
835
  private detectSourceType(source: VideoSource): string {
@@ -6653,6 +6873,13 @@ export class WebPlayer extends BasePlayer {
6653
6873
  return;
6654
6874
  }
6655
6875
 
6876
+ // Block all keyboard controls during ads (Google IMA handles ad controls)
6877
+ if (this.isAdPlaying) {
6878
+ this.debugLog('Keyboard blocked: Ad is playing');
6879
+ e.preventDefault();
6880
+ return;
6881
+ }
6882
+
6656
6883
  // Debug logging
6657
6884
  this.debugLog('Keyboard event:', e.key, 'target:', target.tagName);
6658
6885
 
@@ -8795,6 +9022,21 @@ export class WebPlayer extends BasePlayer {
8795
9022
  }
8796
9023
  }
8797
9024
 
9025
+ /**
9026
+ * Set ad playing state (called by Google Ads Manager)
9027
+ */
9028
+ public setAdPlaying(isPlaying: boolean): void {
9029
+ this.isAdPlaying = isPlaying;
9030
+ this.debugLog('Ad playing state:', isPlaying);
9031
+ }
9032
+
9033
+ /**
9034
+ * Check if ad is currently playing
9035
+ */
9036
+ public isAdCurrentlyPlaying(): boolean {
9037
+ return this.isAdPlaying;
9038
+ }
9039
+
8798
9040
  /**
8799
9041
  * Check if a quality level is premium
8800
9042
  */
@@ -9441,18 +9683,45 @@ export class WebPlayer extends BasePlayer {
9441
9683
  }
9442
9684
 
9443
9685
  private async shareVideo(): Promise<void> {
9444
- const shareData: ShareData = { url: window.location.href };
9445
- const t = (this.source?.metadata?.title || '').toString().trim();
9446
- const d = (this.source?.metadata?.description || '').toString().trim();
9686
+ // Get share configuration
9687
+ const shareConfig = this.config.share;
9688
+
9689
+ // Determine share URL
9690
+ let shareUrl: string;
9691
+ if (shareConfig?.url) {
9692
+ // Use custom static URL
9693
+ shareUrl = shareConfig.url;
9694
+ } else if (shareConfig?.generateUrl) {
9695
+ // Use dynamic URL generator
9696
+ try {
9697
+ shareUrl = shareConfig.generateUrl({
9698
+ videoId: this.source?.metadata?.videoId,
9699
+ metadata: this.source?.metadata
9700
+ });
9701
+ } catch (error) {
9702
+ console.warn('Share URL generator failed, falling back to window.location.href:', error);
9703
+ shareUrl = window.location.href;
9704
+ }
9705
+ } else {
9706
+ // Default: Use current page URL
9707
+ shareUrl = window.location.href;
9708
+ }
9709
+
9710
+ // Prepare share data
9711
+ const shareData: ShareData = { url: shareUrl };
9712
+
9713
+ // Get title and text from config or metadata
9714
+ const t = (shareConfig?.title || this.source?.metadata?.title || '').toString().trim();
9715
+ const d = (shareConfig?.text || this.source?.metadata?.description || '').toString().trim();
9447
9716
  if (t) shareData.title = t;
9448
9717
  if (d) shareData.text = d;
9449
-
9718
+
9450
9719
  try {
9451
9720
  if (navigator.share) {
9452
9721
  await navigator.share(shareData);
9453
9722
  } else {
9454
9723
  // Fallback: Copy to clipboard
9455
- await navigator.clipboard.writeText(window.location.href);
9724
+ await navigator.clipboard.writeText(shareUrl);
9456
9725
  this.showNotification('Link copied to clipboard');
9457
9726
  }
9458
9727
  } catch (error) {
@@ -137,7 +137,16 @@ export type WebPlayerViewProps = {
137
137
 
138
138
  // Framework branding control
139
139
  showFrameworkBranding?: boolean;
140
-
140
+
141
+ // Share configuration
142
+ share?: {
143
+ enabled?: boolean; // Enable/disable share button (default: true)
144
+ url?: string; // Custom share URL (default: window.location.href)
145
+ title?: string; // Custom share title (default: video metadata title)
146
+ text?: string; // Custom share description (default: video metadata description)
147
+ generateUrl?: (videoData: { videoId?: string; metadata?: any }) => string; // Dynamic URL generator
148
+ };
149
+
141
150
  // Watermark configuration (can be boolean for simple enable/disable or object for full config)
142
151
  watermark?: boolean | {
143
152
  enabled?: boolean;
@@ -201,6 +210,13 @@ export type WebPlayerViewProps = {
201
210
  subtitles?: SubtitleTrack[];
202
211
  metadata?: VideoMetadata;
203
212
 
213
+ // Fallback configuration
214
+ fallbackSources?: Array<{ url: string; type?: 'mp4' | 'hls' | 'dash' | 'webm' | 'auto'; priority?: number }>;
215
+ fallbackPoster?: string; // Static image to show when all video sources fail
216
+ fallbackRetryDelay?: number; // Delay in ms before trying next fallback (default: 1000)
217
+ fallbackRetryAttempts?: number; // Number of retry attempts per source (default: 1)
218
+ onAllSourcesFailed?: (errors: Array<{ url: string; error: any }>) => void; // Callback when all sources fail
219
+
204
220
  // Optional Google Cast sender SDK loader
205
221
  cast?: boolean;
206
222
 
@@ -838,6 +854,7 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
838
854
  settings: props.settings,
839
855
  showFrameworkBranding: props.showFrameworkBranding,
840
856
  watermark: watermarkConfig,
857
+ share: props.share, // Add share configuration
841
858
  qualityFilter: props.qualityFilter, // Add quality filter to config
842
859
  premiumQualities: props.premiumQualities, // Add premium qualities config
843
860
  // Navigation configuration
@@ -879,6 +896,11 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
879
896
  type: props.type ?? 'auto',
880
897
  subtitles: props.subtitles,
881
898
  metadata: props.metadata,
899
+ fallbackSources: props.fallbackSources,
900
+ fallbackPoster: props.fallbackPoster,
901
+ fallbackRetryDelay: props.fallbackRetryDelay,
902
+ fallbackRetryAttempts: props.fallbackRetryAttempts,
903
+ onAllSourcesFailed: props.onAllSourcesFailed,
882
904
  };
883
905
 
884
906
  await player.load(source);
@@ -1076,18 +1098,34 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1076
1098
  companionAdSlots: props.googleAds.companionAdSlots,
1077
1099
  onAdStart: () => {
1078
1100
  setIsAdPlaying(true);
1101
+ // Notify player to block keyboard controls
1102
+ if (typeof (player as any).setAdPlaying === 'function') {
1103
+ (player as any).setAdPlaying(true);
1104
+ }
1079
1105
  props.googleAds?.onAdStart?.();
1080
1106
  },
1081
1107
  onAdEnd: () => {
1082
1108
  setIsAdPlaying(false);
1109
+ // Notify player to unblock keyboard controls
1110
+ if (typeof (player as any).setAdPlaying === 'function') {
1111
+ (player as any).setAdPlaying(false);
1112
+ }
1083
1113
  props.googleAds?.onAdEnd?.();
1084
1114
  },
1085
1115
  onAdError: (error) => {
1086
1116
  setIsAdPlaying(false);
1117
+ // Notify player to unblock keyboard controls
1118
+ if (typeof (player as any).setAdPlaying === 'function') {
1119
+ (player as any).setAdPlaying(false);
1120
+ }
1087
1121
  props.googleAds?.onAdError?.(error);
1088
1122
  },
1089
1123
  onAllAdsComplete: () => {
1090
1124
  setIsAdPlaying(false);
1125
+ // Notify player to unblock keyboard controls
1126
+ if (typeof (player as any).setAdPlaying === 'function') {
1127
+ (player as any).setAdPlaying(false);
1128
+ }
1091
1129
  props.googleAds?.onAllAdsComplete?.();
1092
1130
  },
1093
1131
  onAdCuePoints: (cuePoints: number[]) => {
@@ -1170,10 +1208,15 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1170
1208
  JSON.stringify(props.settings),
1171
1209
  props.showFrameworkBranding,
1172
1210
  JSON.stringify(props.watermark),
1211
+ JSON.stringify(props.share),
1173
1212
  JSON.stringify(props.navigation),
1174
1213
  JSON.stringify(props.googleAds),
1175
1214
  JSON.stringify(props.qualityFilter),
1176
1215
  JSON.stringify(props.premiumQualities),
1216
+ JSON.stringify(props.fallbackSources),
1217
+ props.fallbackPoster,
1218
+ props.fallbackRetryDelay,
1219
+ props.fallbackRetryAttempts,
1177
1220
  ]);
1178
1221
 
1179
1222
  // Helper function to filter quality levels based on qualityFilter prop