vidply 1.0.30 → 1.0.31

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.
@@ -104,12 +104,12 @@
104
104
  "format": "esm"
105
105
  },
106
106
  "src/utils/VideoFrameCapture.js": {
107
- "bytes": 4092,
107
+ "bytes": 4640,
108
108
  "imports": [],
109
109
  "format": "esm"
110
110
  },
111
111
  "src/controls/ControlBar.js": {
112
- "bytes": 138319,
112
+ "bytes": 144875,
113
113
  "imports": [
114
114
  {
115
115
  "path": "src/utils/DOMUtils.js",
@@ -606,10 +606,10 @@
606
606
  "bytesInOutput": 256
607
607
  },
608
608
  "src/utils/VideoFrameCapture.js": {
609
- "bytesInOutput": 838
609
+ "bytesInOutput": 1009
610
610
  },
611
611
  "src/controls/ControlBar.js": {
612
- "bytesInOutput": 59666
612
+ "bytesInOutput": 61881
613
613
  },
614
614
  "src/controls/CaptionManager.js": {
615
615
  "bytesInOutput": 7279
@@ -633,7 +633,7 @@
633
633
  "bytesInOutput": 1869
634
634
  }
635
635
  },
636
- "bytes": 201314
636
+ "bytes": 203700
637
637
  },
638
638
  "dist/prod/vidply.de-FR3XX54P.min.js": {
639
639
  "imports": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidply",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "Universal, accessible video & audio player with ES6 modules",
5
5
  "type": "module",
6
6
  "main": "dist/prod/vidply.esm.min.js",
@@ -959,12 +959,19 @@ export class ControlBar {
959
959
 
960
960
  // Check if renderer supports preview (HTML5Renderer or HLSRenderer with native support)
961
961
  // We check if renderer has a media property that is a video element
962
+ // Note: Don't rely on constructor.name as it's minified in production builds
962
963
  const renderer = this.player.renderer;
963
964
  const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === 'VIDEO';
964
- const isHTML5Renderer = renderer && (
965
- renderer.constructor.name === 'HTML5Renderer' ||
966
- (renderer.constructor.name === 'HLSRenderer' && hasVideoMedia)
967
- );
965
+
966
+ // Check if it's HTML5Renderer by checking:
967
+ // 1. Has media property that is a video element
968
+ // 2. Media is the same as player.element (HTML5Renderer sets this.media = player.element)
969
+ // 3. Doesn't have hls property (HLSRenderer has hls property)
970
+ // 4. Has seek method (all renderers have this, but combined with above checks it's reliable)
971
+ const isHTML5Renderer = hasVideoMedia &&
972
+ renderer.media === this.player.element &&
973
+ !renderer.hls &&
974
+ typeof renderer.seek === 'function';
968
975
 
969
976
  this.previewSupported = isHTML5Renderer && hasVideoMedia;
970
977
 
@@ -972,34 +979,57 @@ export class ControlBar {
972
979
  // Create a hidden video element for capturing frames
973
980
  this.previewVideo = document.createElement('video');
974
981
  this.previewVideo.muted = true;
975
- this.previewVideo.preload = 'metadata';
982
+ this.previewVideo.preload = 'auto'; // Need more than metadata to capture frames
983
+ this.previewVideo.playsInline = true;
976
984
  this.previewVideo.style.position = 'absolute';
977
985
  this.previewVideo.style.visibility = 'hidden';
978
986
  this.previewVideo.style.width = '1px';
979
987
  this.previewVideo.style.height = '1px';
980
988
  this.previewVideo.style.top = '-9999px';
981
989
 
982
- // Copy source from main video
990
+ // Copy source and attributes from main video
983
991
  const mainVideo = renderer.media || this.player.element;
992
+ let videoSrc = null;
993
+
984
994
  if (mainVideo.src) {
985
- this.previewVideo.src = mainVideo.src;
995
+ videoSrc = mainVideo.src;
986
996
  } else {
987
997
  const source = mainVideo.querySelector('source');
988
998
  if (source) {
989
- this.previewVideo.src = source.src;
999
+ videoSrc = source.src;
990
1000
  }
991
1001
  }
992
1002
 
1003
+ if (!videoSrc) {
1004
+ this.player.log('No video source found for preview', 'warn');
1005
+ this.previewSupported = false;
1006
+ return;
1007
+ }
1008
+
1009
+ // Copy crossOrigin if set (important for CORS)
1010
+ if (mainVideo.crossOrigin) {
1011
+ this.previewVideo.crossOrigin = mainVideo.crossOrigin;
1012
+ }
1013
+
993
1014
  // Handle errors gracefully
994
- this.previewVideo.addEventListener('error', () => {
995
- this.player.log('Preview video failed to load', 'warn');
1015
+ this.previewVideo.addEventListener('error', (e) => {
1016
+ this.player.log('Preview video failed to load:', e, 'warn');
996
1017
  this.previewSupported = false;
997
1018
  });
998
1019
 
999
- // Append to player container (hidden)
1020
+ // Wait for metadata to be loaded before using
1021
+ this.previewVideo.addEventListener('loadedmetadata', () => {
1022
+ this.previewVideoReady = true;
1023
+ }, { once: true });
1024
+
1025
+ // Append to player container (hidden) BEFORE setting src
1000
1026
  if (this.player.container) {
1001
1027
  this.player.container.appendChild(this.previewVideo);
1002
1028
  }
1029
+
1030
+ // Set source after appending to DOM
1031
+ this.previewVideo.src = videoSrc;
1032
+ this.previewVideoReady = false;
1003
1033
  }
1004
1034
  }
1005
1035
 
@@ -1013,6 +1043,55 @@ export class ControlBar {
1013
1043
  return null;
1014
1044
  }
1015
1045
 
1046
+ // Wait for preview video to be ready if not yet loaded
1047
+ if (!this.previewVideoReady) {
1048
+ if (this.previewVideo.readyState < 2) {
1049
+ // Wait for at least HAVE_CURRENT_DATA (2) to ensure we can capture frames
1050
+ await new Promise((resolve, reject) => {
1051
+ const timeout = setTimeout(() => {
1052
+ reject(new Error('Preview video data load timeout'));
1053
+ }, 10000);
1054
+
1055
+ const cleanup = () => {
1056
+ clearTimeout(timeout);
1057
+ this.previewVideo.removeEventListener('loadeddata', checkReady);
1058
+ this.previewVideo.removeEventListener('canplay', checkReady);
1059
+ this.previewVideo.removeEventListener('error', onError);
1060
+ };
1061
+
1062
+ const checkReady = () => {
1063
+ if (this.previewVideo.readyState >= 2) {
1064
+ cleanup();
1065
+ this.previewVideoReady = true;
1066
+ resolve();
1067
+ }
1068
+ };
1069
+
1070
+ const onError = () => {
1071
+ cleanup();
1072
+ reject(new Error('Preview video failed to load'));
1073
+ };
1074
+
1075
+ // Try loadeddata first (faster), fallback to canplay
1076
+ if (this.previewVideo.readyState >= 1) {
1077
+ this.previewVideo.addEventListener('loadeddata', checkReady);
1078
+ }
1079
+ this.previewVideo.addEventListener('canplay', checkReady);
1080
+ this.previewVideo.addEventListener('error', onError);
1081
+
1082
+ // If already ready, resolve immediately
1083
+ if (this.previewVideo.readyState >= 2) {
1084
+ checkReady();
1085
+ }
1086
+ }).catch(() => {
1087
+ this.previewSupported = false;
1088
+ return null;
1089
+ });
1090
+ } else {
1091
+ this.previewVideoReady = true;
1092
+ }
1093
+ }
1094
+
1016
1095
  // Check cache first
1017
1096
  const cacheKey = Math.floor(time);
1018
1097
  if (this.previewThumbnailCache.has(cacheKey)) {
@@ -1029,8 +1108,9 @@ export class ControlBar {
1029
1108
  });
1030
1109
 
1031
1110
  if (dataURL) {
1032
- // Cache the thumbnail (limit cache size)
1033
- if (this.previewThumbnailCache.size > 20) {
1111
+ // Cache the thumbnail (limit cache size to 20 entries using LRU-like behavior)
1112
+ if (this.previewThumbnailCache.size >= 20) {
1113
+ // Delete oldest entry (first key in insertion order)
1034
1114
  const firstKey = this.previewThumbnailCache.keys().next().value;
1035
1115
  this.previewThumbnailCache.delete(firstKey);
1036
1116
  }
@@ -1045,7 +1125,7 @@ export class ControlBar {
1045
1125
  * @param {number} time - Time in seconds
1046
1126
  */
1047
1127
  async updatePreviewThumbnail(time) {
1048
- if (!this.previewSupported) {
1128
+ if (!this.previewSupported || !this.controls.progressPreview) {
1049
1129
  return;
1050
1130
  }
1051
1131
 
@@ -1056,12 +1136,27 @@ export class ControlBar {
1056
1136
 
1057
1137
  // Debounce thumbnail generation to avoid excessive seeking
1058
1138
  this.previewThumbnailTimeout = setTimeout(async () => {
1059
- const thumbnail = await this.generatePreviewThumbnail(time);
1060
- if (thumbnail && this.controls.progressPreview) {
1061
- this.controls.progressPreview.style.backgroundImage = `url(${thumbnail})`;
1062
- this.controls.progressPreview.style.display = 'block';
1139
+ try {
1140
+ const thumbnail = await this.generatePreviewThumbnail(time);
1141
+ if (thumbnail && this.controls.progressPreview) {
1142
+ // Set background image and make visible
1143
+ this.controls.progressPreview.style.backgroundImage = `url("${thumbnail}")`;
1144
+ this.controls.progressPreview.style.display = 'block';
1145
+ this.controls.progressPreview.style.backgroundRepeat = 'no-repeat';
1146
+ this.controls.progressPreview.style.backgroundPosition = 'center';
1147
+ } else {
1148
+ // Hide if thumbnail generation failed
1149
+ if (this.controls.progressPreview) {
1150
+ this.controls.progressPreview.style.display = 'none';
1151
+ }
1152
+ }
1153
+ this.currentPreviewTime = time;
1154
+ } catch (error) {
1155
+ this.player.log('Preview thumbnail update failed:', error, 'warn');
1156
+ if (this.controls.progressPreview) {
1157
+ this.controls.progressPreview.style.display = 'none';
1158
+ }
1063
1159
  }
1064
- this.currentPreviewTime = time;
1065
1160
  }, 100);
1066
1161
  }
1067
1162
 
@@ -1109,8 +1204,10 @@ export class ControlBar {
1109
1204
  this.controls.progressTooltip.style.left = `${left}px`;
1110
1205
  this.controls.progressTooltip.style.display = 'block';
1111
1206
 
1112
- // Update preview thumbnail
1113
- this.updatePreviewThumbnail(time);
1207
+ // Update preview thumbnail (only if supported)
1208
+ if (this.previewSupported) {
1209
+ this.updatePreviewThumbnail(time);
1210
+ }
1114
1211
  }
1115
1212
  });
1116
1213
 
@@ -2815,6 +2912,12 @@ export class ControlBar {
2815
2912
  this.updateDuration();
2816
2913
  this.ensureQualityButton();
2817
2914
  this.updateQualityIndicator();
2915
+ // Update preview video source when metadata loads (for playlists)
2916
+ this.updatePreviewVideoSource();
2917
+ });
2918
+ this.player.on('sourcechange', () => {
2919
+ // Update preview video source when source changes (for playlists)
2920
+ this.updatePreviewVideoSource();
2818
2921
  });
2819
2922
  this.player.on('volumechange', () => this.updateVolumeDisplay());
2820
2923
  this.player.on('progress', () => this.updateBuffered());
@@ -3476,6 +3579,49 @@ export class ControlBar {
3476
3579
  this.element.style.display = 'none';
3477
3580
  }
3478
3581
 
3582
+ /**
3583
+ * Update preview video source when player source changes (for playlists)
3584
+ * Also re-initializes if preview wasn't set up initially
3585
+ */
3586
+ updatePreviewVideoSource() {
3587
+ const renderer = this.player.renderer;
3588
+ if (!renderer || !renderer.media || renderer.media.tagName !== 'VIDEO') {
3589
+ return;
3590
+ }
3591
+
3592
+ // If preview wasn't initialized yet, try to initialize it now
3593
+ if (!this.previewSupported && !this.previewVideo) {
3594
+ this.initPreviewThumbnail();
3595
+ }
3596
+
3597
+ if (!this.previewSupported || !this.previewVideo) {
3598
+ return;
3599
+ }
3600
+
3601
+ const mainVideo = renderer.media;
3602
+ const newSrc = mainVideo.src || mainVideo.querySelector('source')?.src;
3603
+
3604
+ if (newSrc && this.previewVideo.src !== newSrc) {
3605
+ // Clear cache when source changes
3606
+ this.previewThumbnailCache.clear();
3607
+ this.previewVideoReady = false;
3608
+ this.previewVideo.src = newSrc;
3609
+
3610
+ // Copy crossOrigin if set
3611
+ if (mainVideo.crossOrigin) {
3612
+ this.previewVideo.crossOrigin = mainVideo.crossOrigin;
3613
+ }
3614
+
3615
+ // Wait for new source to load
3616
+ this.previewVideo.addEventListener('loadedmetadata', () => {
3617
+ this.previewVideoReady = true;
3618
+ }, { once: true });
3619
+ } else if (newSrc && !this.previewVideoReady && this.previewVideo.readyState >= 1) {
3620
+ // If source is the same but video is ready, mark as ready
3621
+ this.previewVideoReady = true;
3622
+ }
3623
+ }
3624
+
3479
3625
  /**
3480
3626
  * Cleanup preview thumbnail resources
3481
3627
  */
@@ -97,13 +97,22 @@ export async function captureVideoFrame(video, time, options = {}) {
97
97
 
98
98
  // Check if video is already at the right time and ready
99
99
  const timeDiff = Math.abs(video.currentTime - time);
100
+ // Need at least HAVE_METADATA (1) to know duration, but HAVE_CURRENT_DATA (2) is better for frame capture
100
101
  if (timeDiff < 0.1 && video.readyState >= 2) {
101
102
  // Video is already at the right position, capture immediately
102
103
  captureFrame();
103
- } else {
104
- // Seek to the desired time
104
+ } else if (video.readyState >= 1) {
105
+ // Video has metadata, we can seek
105
106
  video.addEventListener('seeked', onSeeked);
106
107
  video.currentTime = time;
108
+ } else {
109
+ // Video not ready yet, wait for metadata first
110
+ const onLoadedMetadata = () => {
111
+ video.removeEventListener('loadedmetadata', onLoadedMetadata);
112
+ video.addEventListener('seeked', onSeeked);
113
+ video.currentTime = time;
114
+ };
115
+ video.addEventListener('loadedmetadata', onLoadedMetadata);
107
116
  }
108
117
  });
109
118
  }