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.
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import {
7
7
  HTML5Renderer
8
- } from "./vidply.chunk-BCOFCT6U.js";
8
+ } from "./vidply.chunk-W2LSBD6Y.js";
9
9
  import {
10
10
  DOMUtils,
11
11
  DraggableResizable,
@@ -91,6 +91,81 @@ function rafWithTimeout(callback, timeout = 100) {
91
91
  setTimeout(execute, timeout);
92
92
  }
93
93
 
94
+ // src/utils/VideoFrameCapture.js
95
+ async function captureVideoFrame(video, time, options = {}) {
96
+ if (!video || video.tagName !== "VIDEO") {
97
+ return null;
98
+ }
99
+ const {
100
+ restoreState = true,
101
+ quality = 0.9,
102
+ maxWidth,
103
+ maxHeight
104
+ } = options;
105
+ const wasPlaying = !video.paused;
106
+ const originalTime = video.currentTime;
107
+ const originalMuted = video.muted;
108
+ if (restoreState) {
109
+ video.muted = true;
110
+ }
111
+ return new Promise((resolve) => {
112
+ const captureFrame = () => {
113
+ try {
114
+ let width = video.videoWidth || 640;
115
+ let height = video.videoHeight || 360;
116
+ if (maxWidth && width > maxWidth) {
117
+ const ratio = maxWidth / width;
118
+ width = maxWidth;
119
+ height = Math.round(height * ratio);
120
+ }
121
+ if (maxHeight && height > maxHeight) {
122
+ const ratio = maxHeight / height;
123
+ height = maxHeight;
124
+ width = Math.round(width * ratio);
125
+ }
126
+ const canvas = document.createElement("canvas");
127
+ canvas.width = width;
128
+ canvas.height = height;
129
+ const ctx = canvas.getContext("2d");
130
+ ctx.drawImage(video, 0, 0, width, height);
131
+ const dataURL = canvas.toDataURL("image/jpeg", quality);
132
+ if (restoreState) {
133
+ video.currentTime = originalTime;
134
+ video.muted = originalMuted;
135
+ if (wasPlaying && !video.paused) {
136
+ video.play().catch(() => {
137
+ });
138
+ }
139
+ }
140
+ resolve(dataURL);
141
+ } catch (error) {
142
+ if (restoreState) {
143
+ video.currentTime = originalTime;
144
+ video.muted = originalMuted;
145
+ if (wasPlaying && !video.paused) {
146
+ video.play().catch(() => {
147
+ });
148
+ }
149
+ }
150
+ resolve(null);
151
+ }
152
+ };
153
+ const onSeeked = () => {
154
+ video.removeEventListener("seeked", onSeeked);
155
+ requestAnimationFrame(() => {
156
+ requestAnimationFrame(captureFrame);
157
+ });
158
+ };
159
+ const timeDiff = Math.abs(video.currentTime - time);
160
+ if (timeDiff < 0.1 && video.readyState >= 2) {
161
+ captureFrame();
162
+ } else {
163
+ video.addEventListener("seeked", onSeeked);
164
+ video.currentTime = time;
165
+ }
166
+ });
167
+ }
168
+
94
169
  // src/controls/ControlBar.js
95
170
  var ControlBar = class {
96
171
  constructor(player) {
@@ -727,13 +802,117 @@ var ControlBar = class {
727
802
  this.controls.progressTooltip = DOMUtils.createElement("div", {
728
803
  className: `${this.player.options.classPrefix}-progress-tooltip`
729
804
  });
805
+ this.controls.progressPreview = DOMUtils.createElement("div", {
806
+ className: `${this.player.options.classPrefix}-progress-preview`,
807
+ attributes: {
808
+ "aria-hidden": "true"
809
+ }
810
+ });
811
+ this.controls.progressTooltip.appendChild(this.controls.progressPreview);
812
+ this.controls.progressTooltipTime = DOMUtils.createElement("div", {
813
+ className: `${this.player.options.classPrefix}-progress-tooltip-time`
814
+ });
815
+ this.controls.progressTooltip.appendChild(this.controls.progressTooltipTime);
730
816
  progressContainer.appendChild(this.controls.buffered);
731
817
  progressContainer.appendChild(this.controls.played);
732
818
  this.controls.played.appendChild(this.controls.progressHandle);
733
819
  progressContainer.appendChild(this.controls.progressTooltip);
734
820
  this.controls.progress = progressContainer;
821
+ this.initPreviewThumbnail();
735
822
  this.setupProgressBarEvents();
736
823
  }
824
+ /**
825
+ * Initialize preview thumbnail functionality for HTML5 video
826
+ */
827
+ initPreviewThumbnail() {
828
+ this.previewThumbnailCache = /* @__PURE__ */ new Map();
829
+ this.previewVideo = null;
830
+ this.currentPreviewTime = null;
831
+ this.previewThumbnailTimeout = null;
832
+ this.previewSupported = false;
833
+ const isVideo = this.player.element && this.player.element.tagName === "VIDEO";
834
+ if (!isVideo) {
835
+ return;
836
+ }
837
+ const renderer = this.player.renderer;
838
+ const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === "VIDEO";
839
+ const isHTML5Renderer = renderer && (renderer.constructor.name === "HTML5Renderer" || renderer.constructor.name === "HLSRenderer" && hasVideoMedia);
840
+ this.previewSupported = isHTML5Renderer && hasVideoMedia;
841
+ if (this.previewSupported) {
842
+ this.previewVideo = document.createElement("video");
843
+ this.previewVideo.muted = true;
844
+ this.previewVideo.preload = "metadata";
845
+ this.previewVideo.style.position = "absolute";
846
+ this.previewVideo.style.visibility = "hidden";
847
+ this.previewVideo.style.width = "1px";
848
+ this.previewVideo.style.height = "1px";
849
+ this.previewVideo.style.top = "-9999px";
850
+ const mainVideo = renderer.media || this.player.element;
851
+ if (mainVideo.src) {
852
+ this.previewVideo.src = mainVideo.src;
853
+ } else {
854
+ const source = mainVideo.querySelector("source");
855
+ if (source) {
856
+ this.previewVideo.src = source.src;
857
+ }
858
+ }
859
+ this.previewVideo.addEventListener("error", () => {
860
+ this.player.log("Preview video failed to load", "warn");
861
+ this.previewSupported = false;
862
+ });
863
+ if (this.player.container) {
864
+ this.player.container.appendChild(this.previewVideo);
865
+ }
866
+ }
867
+ }
868
+ /**
869
+ * Generate preview thumbnail for a specific time
870
+ * @param {number} time - Time in seconds
871
+ * @returns {Promise<string>} Data URL of the thumbnail
872
+ */
873
+ async generatePreviewThumbnail(time) {
874
+ if (!this.previewSupported || !this.previewVideo) {
875
+ return null;
876
+ }
877
+ const cacheKey = Math.floor(time);
878
+ if (this.previewThumbnailCache.has(cacheKey)) {
879
+ return this.previewThumbnailCache.get(cacheKey);
880
+ }
881
+ const dataURL = await captureVideoFrame(this.previewVideo, time, {
882
+ restoreState: false,
883
+ quality: 0.8,
884
+ maxWidth: 160,
885
+ maxHeight: 90
886
+ });
887
+ if (dataURL) {
888
+ if (this.previewThumbnailCache.size > 20) {
889
+ const firstKey = this.previewThumbnailCache.keys().next().value;
890
+ this.previewThumbnailCache.delete(firstKey);
891
+ }
892
+ this.previewThumbnailCache.set(cacheKey, dataURL);
893
+ }
894
+ return dataURL;
895
+ }
896
+ /**
897
+ * Update preview thumbnail display
898
+ * @param {number} time - Time in seconds
899
+ */
900
+ async updatePreviewThumbnail(time) {
901
+ if (!this.previewSupported) {
902
+ return;
903
+ }
904
+ if (this.previewThumbnailTimeout) {
905
+ clearTimeout(this.previewThumbnailTimeout);
906
+ }
907
+ this.previewThumbnailTimeout = setTimeout(async () => {
908
+ const thumbnail = await this.generatePreviewThumbnail(time);
909
+ if (thumbnail && this.controls.progressPreview) {
910
+ this.controls.progressPreview.style.backgroundImage = `url(${thumbnail})`;
911
+ this.controls.progressPreview.style.display = "block";
912
+ }
913
+ this.currentPreviewTime = time;
914
+ }, 100);
915
+ }
737
916
  setupProgressBarEvents() {
738
917
  const progress = this.controls.progress;
739
918
  const updateProgress = (clientX) => {
@@ -760,13 +939,19 @@ var ControlBar = class {
760
939
  progress.addEventListener("mousemove", (e) => {
761
940
  if (!this.isDraggingProgress) {
762
941
  const { time } = updateProgress(e.clientX);
763
- this.controls.progressTooltip.textContent = TimeUtils.formatTime(time);
764
- this.controls.progressTooltip.style.left = `${e.clientX - progress.getBoundingClientRect().left}px`;
942
+ const rect = progress.getBoundingClientRect();
943
+ const left = e.clientX - rect.left;
944
+ this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
945
+ this.controls.progressTooltip.style.left = `${left}px`;
765
946
  this.controls.progressTooltip.style.display = "block";
947
+ this.updatePreviewThumbnail(time);
766
948
  }
767
949
  });
768
950
  progress.addEventListener("mouseleave", () => {
769
951
  this.controls.progressTooltip.style.display = "none";
952
+ if (this.previewThumbnailTimeout) {
953
+ clearTimeout(this.previewThumbnailTimeout);
954
+ }
770
955
  });
771
956
  progress.addEventListener("keydown", (e) => {
772
957
  if (e.key === "ArrowLeft") {
@@ -2513,6 +2698,20 @@ var ControlBar = class {
2513
2698
  hide() {
2514
2699
  this.element.style.display = "none";
2515
2700
  }
2701
+ /**
2702
+ * Cleanup preview thumbnail resources
2703
+ */
2704
+ cleanupPreviewThumbnail() {
2705
+ if (this.previewThumbnailTimeout) {
2706
+ clearTimeout(this.previewThumbnailTimeout);
2707
+ this.previewThumbnailTimeout = null;
2708
+ }
2709
+ if (this.previewVideo && this.previewVideo.parentNode) {
2710
+ this.previewVideo.parentNode.removeChild(this.previewVideo);
2711
+ this.previewVideo = null;
2712
+ }
2713
+ this.previewThumbnailCache.clear();
2714
+ }
2516
2715
  destroy() {
2517
2716
  if (this.hideTimeout) {
2518
2717
  clearTimeout(this.hideTimeout);
@@ -2520,6 +2719,7 @@ var ControlBar = class {
2520
2719
  if (this.overflowResizeObserver) {
2521
2720
  this.overflowResizeObserver.disconnect();
2522
2721
  }
2722
+ this.cleanupPreviewThumbnail();
2523
2723
  if (this.element && this.element.parentNode) {
2524
2724
  this.element.parentNode.removeChild(this.element);
2525
2725
  }
@@ -5021,7 +5221,7 @@ var Player = class _Player extends EventEmitter {
5021
5221
  const module = await import("./vidply.VimeoRenderer-VPH4RNES.js");
5022
5222
  rendererClass = module.VimeoRenderer || module.default;
5023
5223
  } else if (src.includes(".m3u8")) {
5024
- const module = await import("./vidply.HLSRenderer-ENLZE4QS.js");
5224
+ const module = await import("./vidply.HLSRenderer-UMPUDSYL.js");
5025
5225
  rendererClass = module.HLSRenderer || module.default;
5026
5226
  } else if (src.includes("soundcloud.com") || src.includes("api.soundcloud.com")) {
5027
5227
  const module = await import("./vidply.SoundCloudRenderer-CD7VJKNS.js");
@@ -5126,6 +5326,64 @@ var Player = class _Player extends EventEmitter {
5126
5326
  return posterPath;
5127
5327
  }
5128
5328
  }
5329
+ /**
5330
+ * Generate a poster image from video frame at specified time
5331
+ * @param {number} time - Time in seconds (default: 10)
5332
+ * @returns {Promise<string|null>} Data URL of the poster image or null if failed
5333
+ */
5334
+ async generatePosterFromVideo(time = 10) {
5335
+ if (this.element.tagName !== "VIDEO") {
5336
+ return null;
5337
+ }
5338
+ const renderer = this.renderer;
5339
+ if (!renderer || !renderer.media || renderer.media.tagName !== "VIDEO") {
5340
+ return null;
5341
+ }
5342
+ const video = renderer.media;
5343
+ if (!video.duration || video.duration < time) {
5344
+ time = Math.min(time, Math.max(1, video.duration * 0.1));
5345
+ }
5346
+ let videoToUse = video;
5347
+ if (this.controlBar && this.controlBar.previewVideo && this.controlBar.previewSupported) {
5348
+ videoToUse = this.controlBar.previewVideo;
5349
+ }
5350
+ const restoreState = videoToUse === video;
5351
+ return await captureVideoFrame(videoToUse, time, {
5352
+ restoreState,
5353
+ quality: 0.9
5354
+ });
5355
+ }
5356
+ /**
5357
+ * Auto-generate poster from video if none is provided
5358
+ */
5359
+ async autoGeneratePoster() {
5360
+ const hasPoster = this.element.getAttribute("poster") || this.element.poster || this.options.poster;
5361
+ if (hasPoster) {
5362
+ return;
5363
+ }
5364
+ if (this.element.tagName !== "VIDEO") {
5365
+ return;
5366
+ }
5367
+ if (!this.state.duration || this.state.duration === 0) {
5368
+ await new Promise((resolve) => {
5369
+ const onLoadedMetadata = () => {
5370
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
5371
+ resolve();
5372
+ };
5373
+ if (this.element.readyState >= 1) {
5374
+ resolve();
5375
+ } else {
5376
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
5377
+ }
5378
+ });
5379
+ }
5380
+ const posterDataURL = await this.generatePosterFromVideo(10);
5381
+ if (posterDataURL) {
5382
+ this.element.poster = posterDataURL;
5383
+ this.log("Auto-generated poster from video frame at 10 seconds", "info");
5384
+ this.showPosterOverlay();
5385
+ }
5386
+ }
5129
5387
  showPosterOverlay() {
5130
5388
  if (!this.videoWrapper || this.element.tagName !== "VIDEO") {
5131
5389
  return;
@@ -5134,7 +5392,7 @@ var Player = class _Player extends EventEmitter {
5134
5392
  if (!poster) {
5135
5393
  return;
5136
5394
  }
5137
- const resolvedPoster = this.resolvePosterPath(poster);
5395
+ const resolvedPoster = poster.startsWith("data:") ? poster : this.resolvePosterPath(poster);
5138
5396
  this.videoWrapper.style.setProperty("--vidply-poster-image", `url("${resolvedPoster}")`);
5139
5397
  this.videoWrapper.classList.add("vidply-forced-poster");
5140
5398
  if (this._isAudioContent && this.container) {