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.
- package/dist/dev/vidply.esm.js +126 -15
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/legacy/vidply.js +127 -15
- package/dist/legacy/vidply.js.map +2 -2
- package/dist/legacy/vidply.min.js +1 -1
- package/dist/legacy/vidply.min.meta.json +5 -5
- package/dist/prod/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +5 -5
- package/package.json +1 -1
- package/src/controls/ControlBar.js +167 -21
- package/src/utils/VideoFrameCapture.js +11 -2
|
@@ -104,12 +104,12 @@
|
|
|
104
104
|
"format": "esm"
|
|
105
105
|
},
|
|
106
106
|
"src/utils/VideoFrameCapture.js": {
|
|
107
|
-
"bytes":
|
|
107
|
+
"bytes": 4640,
|
|
108
108
|
"imports": [],
|
|
109
109
|
"format": "esm"
|
|
110
110
|
},
|
|
111
111
|
"src/controls/ControlBar.js": {
|
|
112
|
-
"bytes":
|
|
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":
|
|
609
|
+
"bytesInOutput": 1009
|
|
610
610
|
},
|
|
611
611
|
"src/controls/ControlBar.js": {
|
|
612
|
-
"bytesInOutput":
|
|
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":
|
|
636
|
+
"bytes": 203700
|
|
637
637
|
},
|
|
638
638
|
"dist/prod/vidply.de-FR3XX54P.min.js": {
|
|
639
639
|
"imports": [],
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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 = '
|
|
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
|
-
|
|
995
|
+
videoSrc = mainVideo.src;
|
|
986
996
|
} else {
|
|
987
997
|
const source = mainVideo.querySelector('source');
|
|
988
998
|
if (source) {
|
|
989
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
this.controls.progressPreview
|
|
1062
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
}
|