vidply 1.0.29 → 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.
- package/dist/dev/vidply.HLSRenderer-UMPUDSYL.js +266 -0
- package/dist/dev/vidply.HLSRenderer-UMPUDSYL.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-FXBZQL6Y.js +12 -0
- package/dist/dev/vidply.HTML5Renderer-FXBZQL6Y.js.map +7 -0
- package/dist/dev/vidply.chunk-W2LSBD6Y.js +251 -0
- package/dist/dev/vidply.chunk-W2LSBD6Y.js.map +7 -0
- package/dist/dev/vidply.esm.js +374 -5
- package/dist/dev/vidply.esm.js.map +3 -3
- package/dist/legacy/vidply.js +378 -3
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +1 -1
- package/dist/legacy/vidply.min.meta.json +25 -7
- package/dist/prod/vidply.HLSRenderer-3CG7BZKA.min.js +6 -0
- package/dist/prod/vidply.HTML5Renderer-KKW3OLHM.min.js +6 -0
- package/dist/prod/vidply.chunk-34RH2THY.min.js +6 -0
- package/dist/prod/vidply.esm.min.js +4 -4
- package/dist/vidply.css +20 -1
- package/dist/vidply.esm.min.meta.json +34 -16
- package/dist/vidply.min.css +1 -1
- package/package.json +2 -2
- package/src/controls/ControlBar.js +325 -3
- package/src/core/Player.js +4868 -4776
- package/src/renderers/HTML5Renderer.js +7 -0
- package/src/styles/vidply.css +20 -1
- package/src/utils/VideoFrameCapture.js +119 -0
|
@@ -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,234 @@ 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
|
+
// Note: Don't rely on constructor.name as it's minified in production builds
|
|
963
|
+
const renderer = this.player.renderer;
|
|
964
|
+
const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === 'VIDEO';
|
|
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';
|
|
975
|
+
|
|
976
|
+
this.previewSupported = isHTML5Renderer && hasVideoMedia;
|
|
977
|
+
|
|
978
|
+
if (this.previewSupported) {
|
|
979
|
+
// Create a hidden video element for capturing frames
|
|
980
|
+
this.previewVideo = document.createElement('video');
|
|
981
|
+
this.previewVideo.muted = true;
|
|
982
|
+
this.previewVideo.preload = 'auto'; // Need more than metadata to capture frames
|
|
983
|
+
this.previewVideo.playsInline = true;
|
|
984
|
+
this.previewVideo.style.position = 'absolute';
|
|
985
|
+
this.previewVideo.style.visibility = 'hidden';
|
|
986
|
+
this.previewVideo.style.width = '1px';
|
|
987
|
+
this.previewVideo.style.height = '1px';
|
|
988
|
+
this.previewVideo.style.top = '-9999px';
|
|
989
|
+
|
|
990
|
+
// Copy source and attributes from main video
|
|
991
|
+
const mainVideo = renderer.media || this.player.element;
|
|
992
|
+
let videoSrc = null;
|
|
993
|
+
|
|
994
|
+
if (mainVideo.src) {
|
|
995
|
+
videoSrc = mainVideo.src;
|
|
996
|
+
} else {
|
|
997
|
+
const source = mainVideo.querySelector('source');
|
|
998
|
+
if (source) {
|
|
999
|
+
videoSrc = source.src;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
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
|
+
|
|
1014
|
+
// Handle errors gracefully
|
|
1015
|
+
this.previewVideo.addEventListener('error', (e) => {
|
|
1016
|
+
this.player.log('Preview video failed to load:', e, 'warn');
|
|
1017
|
+
this.previewSupported = false;
|
|
1018
|
+
});
|
|
1019
|
+
|
|
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
|
|
1026
|
+
if (this.player.container) {
|
|
1027
|
+
this.player.container.appendChild(this.previewVideo);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Set source after appending to DOM
|
|
1031
|
+
this.previewVideo.src = videoSrc;
|
|
1032
|
+
this.previewVideoReady = false;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Generate preview thumbnail for a specific time
|
|
1038
|
+
* @param {number} time - Time in seconds
|
|
1039
|
+
* @returns {Promise<string>} Data URL of the thumbnail
|
|
1040
|
+
*/
|
|
1041
|
+
async generatePreviewThumbnail(time) {
|
|
1042
|
+
if (!this.previewSupported || !this.previewVideo) {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
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
|
+
|
|
1095
|
+
// Check cache first
|
|
1096
|
+
const cacheKey = Math.floor(time);
|
|
1097
|
+
if (this.previewThumbnailCache.has(cacheKey)) {
|
|
1098
|
+
return this.previewThumbnailCache.get(cacheKey);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Use shared frame capture utility
|
|
1102
|
+
// Don't restore state since preview video is always muted and hidden
|
|
1103
|
+
const dataURL = await captureVideoFrame(this.previewVideo, time, {
|
|
1104
|
+
restoreState: false,
|
|
1105
|
+
quality: 0.8,
|
|
1106
|
+
maxWidth: 160,
|
|
1107
|
+
maxHeight: 90
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
if (dataURL) {
|
|
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)
|
|
1114
|
+
const firstKey = this.previewThumbnailCache.keys().next().value;
|
|
1115
|
+
this.previewThumbnailCache.delete(firstKey);
|
|
1116
|
+
}
|
|
1117
|
+
this.previewThumbnailCache.set(cacheKey, dataURL);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
return dataURL;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Update preview thumbnail display
|
|
1125
|
+
* @param {number} time - Time in seconds
|
|
1126
|
+
*/
|
|
1127
|
+
async updatePreviewThumbnail(time) {
|
|
1128
|
+
if (!this.previewSupported || !this.controls.progressPreview) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Clear any pending updates
|
|
1133
|
+
if (this.previewThumbnailTimeout) {
|
|
1134
|
+
clearTimeout(this.previewThumbnailTimeout);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Debounce thumbnail generation to avoid excessive seeking
|
|
1138
|
+
this.previewThumbnailTimeout = setTimeout(async () => {
|
|
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
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}, 100);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
923
1163
|
setupProgressBarEvents() {
|
|
924
1164
|
const progress = this.controls.progress;
|
|
925
1165
|
|
|
@@ -954,15 +1194,28 @@ export class ControlBar {
|
|
|
954
1194
|
progress.addEventListener('mousemove', (e) => {
|
|
955
1195
|
if (!this.isDraggingProgress) {
|
|
956
1196
|
const {time} = updateProgress(e.clientX);
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1197
|
+
const rect = progress.getBoundingClientRect();
|
|
1198
|
+
const left = e.clientX - rect.left;
|
|
1199
|
+
|
|
1200
|
+
// Update tooltip time text
|
|
1201
|
+
this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
|
|
1202
|
+
|
|
1203
|
+
// Update tooltip position
|
|
1204
|
+
this.controls.progressTooltip.style.left = `${left}px`;
|
|
960
1205
|
this.controls.progressTooltip.style.display = 'block';
|
|
1206
|
+
|
|
1207
|
+
// Update preview thumbnail (only if supported)
|
|
1208
|
+
if (this.previewSupported) {
|
|
1209
|
+
this.updatePreviewThumbnail(time);
|
|
1210
|
+
}
|
|
961
1211
|
}
|
|
962
1212
|
});
|
|
963
1213
|
|
|
964
1214
|
progress.addEventListener('mouseleave', () => {
|
|
965
1215
|
this.controls.progressTooltip.style.display = 'none';
|
|
1216
|
+
if (this.previewThumbnailTimeout) {
|
|
1217
|
+
clearTimeout(this.previewThumbnailTimeout);
|
|
1218
|
+
}
|
|
966
1219
|
});
|
|
967
1220
|
|
|
968
1221
|
// Keyboard navigation
|
|
@@ -2659,6 +2912,12 @@ export class ControlBar {
|
|
|
2659
2912
|
this.updateDuration();
|
|
2660
2913
|
this.ensureQualityButton();
|
|
2661
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();
|
|
2662
2921
|
});
|
|
2663
2922
|
this.player.on('volumechange', () => this.updateVolumeDisplay());
|
|
2664
2923
|
this.player.on('progress', () => this.updateBuffered());
|
|
@@ -3320,6 +3579,66 @@ export class ControlBar {
|
|
|
3320
3579
|
this.element.style.display = 'none';
|
|
3321
3580
|
}
|
|
3322
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
|
+
|
|
3625
|
+
/**
|
|
3626
|
+
* Cleanup preview thumbnail resources
|
|
3627
|
+
*/
|
|
3628
|
+
cleanupPreviewThumbnail() {
|
|
3629
|
+
if (this.previewThumbnailTimeout) {
|
|
3630
|
+
clearTimeout(this.previewThumbnailTimeout);
|
|
3631
|
+
this.previewThumbnailTimeout = null;
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
if (this.previewVideo && this.previewVideo.parentNode) {
|
|
3635
|
+
this.previewVideo.parentNode.removeChild(this.previewVideo);
|
|
3636
|
+
this.previewVideo = null;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
this.previewThumbnailCache.clear();
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3323
3642
|
destroy() {
|
|
3324
3643
|
if (this.hideTimeout) {
|
|
3325
3644
|
clearTimeout(this.hideTimeout);
|
|
@@ -3329,6 +3648,9 @@ export class ControlBar {
|
|
|
3329
3648
|
this.overflowResizeObserver.disconnect();
|
|
3330
3649
|
}
|
|
3331
3650
|
|
|
3651
|
+
// Cleanup preview thumbnail resources
|
|
3652
|
+
this.cleanupPreviewThumbnail();
|
|
3653
|
+
|
|
3332
3654
|
if (this.element && this.element.parentNode) {
|
|
3333
3655
|
this.element.parentNode.removeChild(this.element);
|
|
3334
3656
|
}
|