vidply 1.0.29 → 1.0.30

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.
@@ -8,6 +8,7 @@ import {createIconElement} from '../icons/Icons.js';
8
8
  import {i18n} from '../i18n/i18n.js';
9
9
  import {focusElement, focusFirstElement} from '../utils/FocusUtils.js';
10
10
  import {isMobile} from '../utils/PerformanceUtils.js';
11
+ import {captureVideoFrame} from '../utils/VideoFrameCapture.js';
11
12
 
12
13
  export class ControlBar {
13
14
  constructor(player) {
@@ -909,6 +910,21 @@ export class ControlBar {
909
910
  className: `${this.player.options.classPrefix}-progress-tooltip`
910
911
  });
911
912
 
913
+ // Preview thumbnail (for video only)
914
+ this.controls.progressPreview = DOMUtils.createElement('div', {
915
+ className: `${this.player.options.classPrefix}-progress-preview`,
916
+ attributes: {
917
+ 'aria-hidden': 'true'
918
+ }
919
+ });
920
+ this.controls.progressTooltip.appendChild(this.controls.progressPreview);
921
+
922
+ // Time text
923
+ this.controls.progressTooltipTime = DOMUtils.createElement('div', {
924
+ className: `${this.player.options.classPrefix}-progress-tooltip-time`
925
+ });
926
+ this.controls.progressTooltip.appendChild(this.controls.progressTooltipTime);
927
+
912
928
  progressContainer.appendChild(this.controls.buffered);
913
929
  progressContainer.appendChild(this.controls.played);
914
930
  this.controls.played.appendChild(this.controls.progressHandle);
@@ -916,10 +932,139 @@ export class ControlBar {
916
932
 
917
933
  this.controls.progress = progressContainer;
918
934
 
935
+ // Initialize preview functionality
936
+ this.initPreviewThumbnail();
937
+
919
938
  // Progress bar events
920
939
  this.setupProgressBarEvents();
921
940
  }
922
941
 
942
+ /**
943
+ * Initialize preview thumbnail functionality for HTML5 video
944
+ */
945
+ initPreviewThumbnail() {
946
+ this.previewThumbnailCache = new Map();
947
+ this.previewVideo = null;
948
+ this.currentPreviewTime = null;
949
+ this.previewThumbnailTimeout = null;
950
+ this.previewSupported = false;
951
+
952
+ // Check if preview is supported (HTML5 video only)
953
+ // Check if element is a video
954
+ const isVideo = this.player.element && this.player.element.tagName === 'VIDEO';
955
+
956
+ if (!isVideo) {
957
+ return;
958
+ }
959
+
960
+ // Check if renderer supports preview (HTML5Renderer or HLSRenderer with native support)
961
+ // We check if renderer has a media property that is a video element
962
+ const renderer = this.player.renderer;
963
+ 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
+ );
968
+
969
+ this.previewSupported = isHTML5Renderer && hasVideoMedia;
970
+
971
+ if (this.previewSupported) {
972
+ // Create a hidden video element for capturing frames
973
+ this.previewVideo = document.createElement('video');
974
+ this.previewVideo.muted = true;
975
+ this.previewVideo.preload = 'metadata';
976
+ this.previewVideo.style.position = 'absolute';
977
+ this.previewVideo.style.visibility = 'hidden';
978
+ this.previewVideo.style.width = '1px';
979
+ this.previewVideo.style.height = '1px';
980
+ this.previewVideo.style.top = '-9999px';
981
+
982
+ // Copy source from main video
983
+ const mainVideo = renderer.media || this.player.element;
984
+ if (mainVideo.src) {
985
+ this.previewVideo.src = mainVideo.src;
986
+ } else {
987
+ const source = mainVideo.querySelector('source');
988
+ if (source) {
989
+ this.previewVideo.src = source.src;
990
+ }
991
+ }
992
+
993
+ // Handle errors gracefully
994
+ this.previewVideo.addEventListener('error', () => {
995
+ this.player.log('Preview video failed to load', 'warn');
996
+ this.previewSupported = false;
997
+ });
998
+
999
+ // Append to player container (hidden)
1000
+ if (this.player.container) {
1001
+ this.player.container.appendChild(this.previewVideo);
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Generate preview thumbnail for a specific time
1008
+ * @param {number} time - Time in seconds
1009
+ * @returns {Promise<string>} Data URL of the thumbnail
1010
+ */
1011
+ async generatePreviewThumbnail(time) {
1012
+ if (!this.previewSupported || !this.previewVideo) {
1013
+ return null;
1014
+ }
1015
+
1016
+ // Check cache first
1017
+ const cacheKey = Math.floor(time);
1018
+ if (this.previewThumbnailCache.has(cacheKey)) {
1019
+ return this.previewThumbnailCache.get(cacheKey);
1020
+ }
1021
+
1022
+ // Use shared frame capture utility
1023
+ // Don't restore state since preview video is always muted and hidden
1024
+ const dataURL = await captureVideoFrame(this.previewVideo, time, {
1025
+ restoreState: false,
1026
+ quality: 0.8,
1027
+ maxWidth: 160,
1028
+ maxHeight: 90
1029
+ });
1030
+
1031
+ if (dataURL) {
1032
+ // Cache the thumbnail (limit cache size)
1033
+ if (this.previewThumbnailCache.size > 20) {
1034
+ const firstKey = this.previewThumbnailCache.keys().next().value;
1035
+ this.previewThumbnailCache.delete(firstKey);
1036
+ }
1037
+ this.previewThumbnailCache.set(cacheKey, dataURL);
1038
+ }
1039
+
1040
+ return dataURL;
1041
+ }
1042
+
1043
+ /**
1044
+ * Update preview thumbnail display
1045
+ * @param {number} time - Time in seconds
1046
+ */
1047
+ async updatePreviewThumbnail(time) {
1048
+ if (!this.previewSupported) {
1049
+ return;
1050
+ }
1051
+
1052
+ // Clear any pending updates
1053
+ if (this.previewThumbnailTimeout) {
1054
+ clearTimeout(this.previewThumbnailTimeout);
1055
+ }
1056
+
1057
+ // Debounce thumbnail generation to avoid excessive seeking
1058
+ 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';
1063
+ }
1064
+ this.currentPreviewTime = time;
1065
+ }, 100);
1066
+ }
1067
+
923
1068
  setupProgressBarEvents() {
924
1069
  const progress = this.controls.progress;
925
1070
 
@@ -954,15 +1099,26 @@ export class ControlBar {
954
1099
  progress.addEventListener('mousemove', (e) => {
955
1100
  if (!this.isDraggingProgress) {
956
1101
  const {time} = updateProgress(e.clientX);
957
- // Update tooltip text content instead of aria-label (divs shouldn't have aria-label)
958
- this.controls.progressTooltip.textContent = TimeUtils.formatTime(time);
959
- this.controls.progressTooltip.style.left = `${e.clientX - progress.getBoundingClientRect().left}px`;
1102
+ const rect = progress.getBoundingClientRect();
1103
+ const left = e.clientX - rect.left;
1104
+
1105
+ // Update tooltip time text
1106
+ this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
1107
+
1108
+ // Update tooltip position
1109
+ this.controls.progressTooltip.style.left = `${left}px`;
960
1110
  this.controls.progressTooltip.style.display = 'block';
1111
+
1112
+ // Update preview thumbnail
1113
+ this.updatePreviewThumbnail(time);
961
1114
  }
962
1115
  });
963
1116
 
964
1117
  progress.addEventListener('mouseleave', () => {
965
1118
  this.controls.progressTooltip.style.display = 'none';
1119
+ if (this.previewThumbnailTimeout) {
1120
+ clearTimeout(this.previewThumbnailTimeout);
1121
+ }
966
1122
  });
967
1123
 
968
1124
  // Keyboard navigation
@@ -3320,6 +3476,23 @@ export class ControlBar {
3320
3476
  this.element.style.display = 'none';
3321
3477
  }
3322
3478
 
3479
+ /**
3480
+ * Cleanup preview thumbnail resources
3481
+ */
3482
+ cleanupPreviewThumbnail() {
3483
+ if (this.previewThumbnailTimeout) {
3484
+ clearTimeout(this.previewThumbnailTimeout);
3485
+ this.previewThumbnailTimeout = null;
3486
+ }
3487
+
3488
+ if (this.previewVideo && this.previewVideo.parentNode) {
3489
+ this.previewVideo.parentNode.removeChild(this.previewVideo);
3490
+ this.previewVideo = null;
3491
+ }
3492
+
3493
+ this.previewThumbnailCache.clear();
3494
+ }
3495
+
3323
3496
  destroy() {
3324
3497
  if (this.hideTimeout) {
3325
3498
  clearTimeout(this.hideTimeout);
@@ -3329,6 +3502,9 @@ export class ControlBar {
3329
3502
  this.overflowResizeObserver.disconnect();
3330
3503
  }
3331
3504
 
3505
+ // Cleanup preview thumbnail resources
3506
+ this.cleanupPreviewThumbnail();
3507
+
3332
3508
  if (this.element && this.element.parentNode) {
3333
3509
  this.element.parentNode.removeChild(this.element);
3334
3510
  }