vidply 1.0.30 → 1.0.32

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": 145861,
113
113
  "imports": [
114
114
  {
115
115
  "path": "src/utils/DOMUtils.js",
@@ -346,7 +346,7 @@
346
346
  "format": "esm"
347
347
  },
348
348
  "src/renderers/HLSRenderer.js": {
349
- "bytes": 9900,
349
+ "bytes": 10310,
350
350
  "imports": [
351
351
  {
352
352
  "path": "src/renderers/HTML5Renderer.js",
@@ -362,7 +362,7 @@
362
362
  "format": "esm"
363
363
  },
364
364
  "src/core/Player.js": {
365
- "bytes": 209146,
365
+ "bytes": 214379,
366
366
  "imports": [
367
367
  {
368
368
  "path": "src/utils/EventEmitter.js",
@@ -473,7 +473,7 @@
473
473
  "format": "esm"
474
474
  },
475
475
  "src/features/PlaylistManager.js": {
476
- "bytes": 48411,
476
+ "bytes": 49922,
477
477
  "imports": [
478
478
  {
479
479
  "path": "src/utils/DOMUtils.js",
@@ -529,7 +529,7 @@
529
529
  },
530
530
  "bytes": 4730
531
531
  },
532
- "dist/prod/vidply.HLSRenderer-3CG7BZKA.min.js": {
532
+ "dist/prod/vidply.HLSRenderer-VWNJD2CB.min.js": {
533
533
  "imports": [
534
534
  {
535
535
  "path": "dist/prod/vidply.HTML5Renderer-KKW3OLHM.min.js",
@@ -542,10 +542,10 @@
542
542
  "entryPoint": "src/renderers/HLSRenderer.js",
543
543
  "inputs": {
544
544
  "src/renderers/HLSRenderer.js": {
545
- "bytesInOutput": 5781
545
+ "bytesInOutput": 5884
546
546
  }
547
547
  },
548
- "bytes": 5925
548
+ "bytes": 6028
549
549
  },
550
550
  "dist/prod/vidply.SoundCloudRenderer-MOR2CUFH.min.js": {
551
551
  "imports": [],
@@ -584,7 +584,7 @@
584
584
  "kind": "dynamic-import"
585
585
  },
586
586
  {
587
- "path": "dist/prod/vidply.HLSRenderer-3CG7BZKA.min.js",
587
+ "path": "dist/prod/vidply.HLSRenderer-VWNJD2CB.min.js",
588
588
  "kind": "dynamic-import"
589
589
  },
590
590
  {
@@ -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": 62339
613
613
  },
614
614
  "src/controls/CaptionManager.js": {
615
615
  "bytesInOutput": 7279
@@ -624,7 +624,7 @@
624
624
  "bytesInOutput": 19117
625
625
  },
626
626
  "src/core/Player.js": {
627
- "bytesInOutput": 76504
627
+ "bytesInOutput": 76547
628
628
  },
629
629
  "src/features/PlaylistManager.js": {
630
630
  "bytesInOutput": 21345
@@ -633,7 +633,7 @@
633
633
  "bytesInOutput": 1869
634
634
  }
635
635
  },
636
- "bytes": 201314
636
+ "bytes": 204201
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.32",
4
4
  "description": "Universal, accessible video & audio player with ES6 modules",
5
5
  "type": "module",
6
6
  "main": "dist/prod/vidply.esm.min.js",
@@ -715,7 +715,22 @@ export class ControlBar {
715
715
  }
716
716
 
717
717
  // 4. Playback speed button
718
- if (this.player.options.speedButton) {
718
+ // IMPORTANT: Don't rely on renderer.constructor.name here.
719
+ // In production builds, class names are minified (e.g. "class s"), which would break the check.
720
+ // Instead, detect HLS by the current source URL.
721
+ const src = this.player.currentSource
722
+ || this.player.element?.getAttribute?.('src')
723
+ || this.player.element?.currentSrc
724
+ || this.player.element?.src
725
+ || this.player.element?.querySelector?.('source')?.getAttribute?.('src')
726
+ || this.player.element?.querySelector?.('source')?.src
727
+ || '';
728
+ const isHlsSource = typeof src === 'string' && src.includes('.m3u8');
729
+ const isVideoElement = this.player.element?.tagName?.toLowerCase() === 'video';
730
+ const hideSpeedForThisPlayer =
731
+ (!!this.player.options.hideSpeedForHls && isHlsSource)
732
+ || (!!this.player.options.hideSpeedForHlsVideo && isHlsSource && isVideoElement);
733
+ if (this.player.options.speedButton && !hideSpeedForThisPlayer) {
719
734
  const btn = this.createSpeedButton();
720
735
  btn.dataset.overflowPriority = '1';
721
736
  btn.dataset.overflowPriorityMobile = '3';
@@ -959,12 +974,19 @@ export class ControlBar {
959
974
 
960
975
  // Check if renderer supports preview (HTML5Renderer or HLSRenderer with native support)
961
976
  // We check if renderer has a media property that is a video element
977
+ // Note: Don't rely on constructor.name as it's minified in production builds
962
978
  const renderer = this.player.renderer;
963
979
  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
- );
980
+
981
+ // Check if it's HTML5Renderer by checking:
982
+ // 1. Has media property that is a video element
983
+ // 2. Media is the same as player.element (HTML5Renderer sets this.media = player.element)
984
+ // 3. Doesn't have hls property (HLSRenderer has hls property)
985
+ // 4. Has seek method (all renderers have this, but combined with above checks it's reliable)
986
+ const isHTML5Renderer = hasVideoMedia &&
987
+ renderer.media === this.player.element &&
988
+ !renderer.hls &&
989
+ typeof renderer.seek === 'function';
968
990
 
969
991
  this.previewSupported = isHTML5Renderer && hasVideoMedia;
970
992
 
@@ -972,34 +994,57 @@ export class ControlBar {
972
994
  // Create a hidden video element for capturing frames
973
995
  this.previewVideo = document.createElement('video');
974
996
  this.previewVideo.muted = true;
975
- this.previewVideo.preload = 'metadata';
997
+ this.previewVideo.preload = 'auto'; // Need more than metadata to capture frames
998
+ this.previewVideo.playsInline = true;
976
999
  this.previewVideo.style.position = 'absolute';
977
1000
  this.previewVideo.style.visibility = 'hidden';
978
1001
  this.previewVideo.style.width = '1px';
979
1002
  this.previewVideo.style.height = '1px';
980
1003
  this.previewVideo.style.top = '-9999px';
981
1004
 
982
- // Copy source from main video
1005
+ // Copy source and attributes from main video
983
1006
  const mainVideo = renderer.media || this.player.element;
1007
+ let videoSrc = null;
1008
+
984
1009
  if (mainVideo.src) {
985
- this.previewVideo.src = mainVideo.src;
1010
+ videoSrc = mainVideo.src;
986
1011
  } else {
987
1012
  const source = mainVideo.querySelector('source');
988
1013
  if (source) {
989
- this.previewVideo.src = source.src;
1014
+ videoSrc = source.src;
990
1015
  }
991
1016
  }
992
1017
 
1018
+ if (!videoSrc) {
1019
+ this.player.log('No video source found for preview', 'warn');
1020
+ this.previewSupported = false;
1021
+ return;
1022
+ }
1023
+
1024
+ // Copy crossOrigin if set (important for CORS)
1025
+ if (mainVideo.crossOrigin) {
1026
+ this.previewVideo.crossOrigin = mainVideo.crossOrigin;
1027
+ }
1028
+
993
1029
  // Handle errors gracefully
994
- this.previewVideo.addEventListener('error', () => {
995
- this.player.log('Preview video failed to load', 'warn');
1030
+ this.previewVideo.addEventListener('error', (e) => {
1031
+ this.player.log('Preview video failed to load:', e, 'warn');
996
1032
  this.previewSupported = false;
997
1033
  });
998
1034
 
999
- // Append to player container (hidden)
1035
+ // Wait for metadata to be loaded before using
1036
+ this.previewVideo.addEventListener('loadedmetadata', () => {
1037
+ this.previewVideoReady = true;
1038
+ }, { once: true });
1039
+
1040
+ // Append to player container (hidden) BEFORE setting src
1000
1041
  if (this.player.container) {
1001
1042
  this.player.container.appendChild(this.previewVideo);
1002
1043
  }
1044
+
1045
+ // Set source after appending to DOM
1046
+ this.previewVideo.src = videoSrc;
1047
+ this.previewVideoReady = false;
1003
1048
  }
1004
1049
  }
1005
1050
 
@@ -1013,6 +1058,55 @@ export class ControlBar {
1013
1058
  return null;
1014
1059
  }
1015
1060
 
1061
+ // Wait for preview video to be ready if not yet loaded
1062
+ if (!this.previewVideoReady) {
1063
+ if (this.previewVideo.readyState < 2) {
1064
+ // Wait for at least HAVE_CURRENT_DATA (2) to ensure we can capture frames
1065
+ await new Promise((resolve, reject) => {
1066
+ const timeout = setTimeout(() => {
1067
+ reject(new Error('Preview video data load timeout'));
1068
+ }, 10000);
1069
+
1070
+ const cleanup = () => {
1071
+ clearTimeout(timeout);
1072
+ this.previewVideo.removeEventListener('loadeddata', checkReady);
1073
+ this.previewVideo.removeEventListener('canplay', checkReady);
1074
+ this.previewVideo.removeEventListener('error', onError);
1075
+ };
1076
+
1077
+ const checkReady = () => {
1078
+ if (this.previewVideo.readyState >= 2) {
1079
+ cleanup();
1080
+ this.previewVideoReady = true;
1081
+ resolve();
1082
+ }
1083
+ };
1084
+
1085
+ const onError = () => {
1086
+ cleanup();
1087
+ reject(new Error('Preview video failed to load'));
1088
+ };
1089
+
1090
+ // Try loadeddata first (faster), fallback to canplay
1091
+ if (this.previewVideo.readyState >= 1) {
1092
+ this.previewVideo.addEventListener('loadeddata', checkReady);
1093
+ }
1094
+ this.previewVideo.addEventListener('canplay', checkReady);
1095
+ this.previewVideo.addEventListener('error', onError);
1096
+
1097
+ // If already ready, resolve immediately
1098
+ if (this.previewVideo.readyState >= 2) {
1099
+ checkReady();
1100
+ }
1101
+ }).catch(() => {
1102
+ this.previewSupported = false;
1103
+ return null;
1104
+ });
1105
+ } else {
1106
+ this.previewVideoReady = true;
1107
+ }
1108
+ }
1109
+
1016
1110
  // Check cache first
1017
1111
  const cacheKey = Math.floor(time);
1018
1112
  if (this.previewThumbnailCache.has(cacheKey)) {
@@ -1029,8 +1123,9 @@ export class ControlBar {
1029
1123
  });
1030
1124
 
1031
1125
  if (dataURL) {
1032
- // Cache the thumbnail (limit cache size)
1033
- if (this.previewThumbnailCache.size > 20) {
1126
+ // Cache the thumbnail (limit cache size to 20 entries using LRU-like behavior)
1127
+ if (this.previewThumbnailCache.size >= 20) {
1128
+ // Delete oldest entry (first key in insertion order)
1034
1129
  const firstKey = this.previewThumbnailCache.keys().next().value;
1035
1130
  this.previewThumbnailCache.delete(firstKey);
1036
1131
  }
@@ -1045,7 +1140,7 @@ export class ControlBar {
1045
1140
  * @param {number} time - Time in seconds
1046
1141
  */
1047
1142
  async updatePreviewThumbnail(time) {
1048
- if (!this.previewSupported) {
1143
+ if (!this.previewSupported || !this.controls.progressPreview) {
1049
1144
  return;
1050
1145
  }
1051
1146
 
@@ -1056,12 +1151,27 @@ export class ControlBar {
1056
1151
 
1057
1152
  // Debounce thumbnail generation to avoid excessive seeking
1058
1153
  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';
1154
+ try {
1155
+ const thumbnail = await this.generatePreviewThumbnail(time);
1156
+ if (thumbnail && this.controls.progressPreview) {
1157
+ // Set background image and make visible
1158
+ this.controls.progressPreview.style.backgroundImage = `url("${thumbnail}")`;
1159
+ this.controls.progressPreview.style.display = 'block';
1160
+ this.controls.progressPreview.style.backgroundRepeat = 'no-repeat';
1161
+ this.controls.progressPreview.style.backgroundPosition = 'center';
1162
+ } else {
1163
+ // Hide if thumbnail generation failed
1164
+ if (this.controls.progressPreview) {
1165
+ this.controls.progressPreview.style.display = 'none';
1166
+ }
1167
+ }
1168
+ this.currentPreviewTime = time;
1169
+ } catch (error) {
1170
+ this.player.log('Preview thumbnail update failed:', error, 'warn');
1171
+ if (this.controls.progressPreview) {
1172
+ this.controls.progressPreview.style.display = 'none';
1173
+ }
1063
1174
  }
1064
- this.currentPreviewTime = time;
1065
1175
  }, 100);
1066
1176
  }
1067
1177
 
@@ -1109,8 +1219,10 @@ export class ControlBar {
1109
1219
  this.controls.progressTooltip.style.left = `${left}px`;
1110
1220
  this.controls.progressTooltip.style.display = 'block';
1111
1221
 
1112
- // Update preview thumbnail
1113
- this.updatePreviewThumbnail(time);
1222
+ // Update preview thumbnail (only if supported)
1223
+ if (this.previewSupported) {
1224
+ this.updatePreviewThumbnail(time);
1225
+ }
1114
1226
  }
1115
1227
  });
1116
1228
 
@@ -2815,6 +2927,12 @@ export class ControlBar {
2815
2927
  this.updateDuration();
2816
2928
  this.ensureQualityButton();
2817
2929
  this.updateQualityIndicator();
2930
+ // Update preview video source when metadata loads (for playlists)
2931
+ this.updatePreviewVideoSource();
2932
+ });
2933
+ this.player.on('sourcechange', () => {
2934
+ // Update preview video source when source changes (for playlists)
2935
+ this.updatePreviewVideoSource();
2818
2936
  });
2819
2937
  this.player.on('volumechange', () => this.updateVolumeDisplay());
2820
2938
  this.player.on('progress', () => this.updateBuffered());
@@ -3476,6 +3594,49 @@ export class ControlBar {
3476
3594
  this.element.style.display = 'none';
3477
3595
  }
3478
3596
 
3597
+ /**
3598
+ * Update preview video source when player source changes (for playlists)
3599
+ * Also re-initializes if preview wasn't set up initially
3600
+ */
3601
+ updatePreviewVideoSource() {
3602
+ const renderer = this.player.renderer;
3603
+ if (!renderer || !renderer.media || renderer.media.tagName !== 'VIDEO') {
3604
+ return;
3605
+ }
3606
+
3607
+ // If preview wasn't initialized yet, try to initialize it now
3608
+ if (!this.previewSupported && !this.previewVideo) {
3609
+ this.initPreviewThumbnail();
3610
+ }
3611
+
3612
+ if (!this.previewSupported || !this.previewVideo) {
3613
+ return;
3614
+ }
3615
+
3616
+ const mainVideo = renderer.media;
3617
+ const newSrc = mainVideo.src || mainVideo.querySelector('source')?.src;
3618
+
3619
+ if (newSrc && this.previewVideo.src !== newSrc) {
3620
+ // Clear cache when source changes
3621
+ this.previewThumbnailCache.clear();
3622
+ this.previewVideoReady = false;
3623
+ this.previewVideo.src = newSrc;
3624
+
3625
+ // Copy crossOrigin if set
3626
+ if (mainVideo.crossOrigin) {
3627
+ this.previewVideo.crossOrigin = mainVideo.crossOrigin;
3628
+ }
3629
+
3630
+ // Wait for new source to load
3631
+ this.previewVideo.addEventListener('loadedmetadata', () => {
3632
+ this.previewVideoReady = true;
3633
+ }, { once: true });
3634
+ } else if (newSrc && !this.previewVideoReady && this.previewVideo.readyState >= 1) {
3635
+ // If source is the same but video is ready, mark as ready
3636
+ this.previewVideoReady = true;
3637
+ }
3638
+ }
3639
+
3479
3640
  /**
3480
3641
  * Cleanup preview thumbnail resources
3481
3642
  */