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.
@@ -1741,6 +1741,11 @@
1741
1741
  this.media.addEventListener("loadedmetadata", () => {
1742
1742
  this.player.state.duration = this.media.duration;
1743
1743
  this.player.emit("loadedmetadata");
1744
+ if (this.media.tagName === "VIDEO") {
1745
+ this.player.autoGeneratePoster().catch((error) => {
1746
+ this.player.log("Failed to auto-generate poster:", error, "warn");
1747
+ });
1748
+ }
1744
1749
  });
1745
1750
  this.media.addEventListener("play", () => {
1746
1751
  this.player.state.playing = true;
@@ -5574,6 +5579,81 @@
5574
5579
  setTimeout(execute, timeout);
5575
5580
  }
5576
5581
 
5582
+ // src/utils/VideoFrameCapture.js
5583
+ async function captureVideoFrame(video, time, options = {}) {
5584
+ if (!video || video.tagName !== "VIDEO") {
5585
+ return null;
5586
+ }
5587
+ const {
5588
+ restoreState = true,
5589
+ quality = 0.9,
5590
+ maxWidth,
5591
+ maxHeight
5592
+ } = options;
5593
+ const wasPlaying = !video.paused;
5594
+ const originalTime = video.currentTime;
5595
+ const originalMuted = video.muted;
5596
+ if (restoreState) {
5597
+ video.muted = true;
5598
+ }
5599
+ return new Promise((resolve) => {
5600
+ const captureFrame = () => {
5601
+ try {
5602
+ let width = video.videoWidth || 640;
5603
+ let height = video.videoHeight || 360;
5604
+ if (maxWidth && width > maxWidth) {
5605
+ const ratio = maxWidth / width;
5606
+ width = maxWidth;
5607
+ height = Math.round(height * ratio);
5608
+ }
5609
+ if (maxHeight && height > maxHeight) {
5610
+ const ratio = maxHeight / height;
5611
+ height = maxHeight;
5612
+ width = Math.round(width * ratio);
5613
+ }
5614
+ const canvas = document.createElement("canvas");
5615
+ canvas.width = width;
5616
+ canvas.height = height;
5617
+ const ctx = canvas.getContext("2d");
5618
+ ctx.drawImage(video, 0, 0, width, height);
5619
+ const dataURL = canvas.toDataURL("image/jpeg", quality);
5620
+ if (restoreState) {
5621
+ video.currentTime = originalTime;
5622
+ video.muted = originalMuted;
5623
+ if (wasPlaying && !video.paused) {
5624
+ video.play().catch(() => {
5625
+ });
5626
+ }
5627
+ }
5628
+ resolve(dataURL);
5629
+ } catch (error) {
5630
+ if (restoreState) {
5631
+ video.currentTime = originalTime;
5632
+ video.muted = originalMuted;
5633
+ if (wasPlaying && !video.paused) {
5634
+ video.play().catch(() => {
5635
+ });
5636
+ }
5637
+ }
5638
+ resolve(null);
5639
+ }
5640
+ };
5641
+ const onSeeked = () => {
5642
+ video.removeEventListener("seeked", onSeeked);
5643
+ requestAnimationFrame(() => {
5644
+ requestAnimationFrame(captureFrame);
5645
+ });
5646
+ };
5647
+ const timeDiff = Math.abs(video.currentTime - time);
5648
+ if (timeDiff < 0.1 && video.readyState >= 2) {
5649
+ captureFrame();
5650
+ } else {
5651
+ video.addEventListener("seeked", onSeeked);
5652
+ video.currentTime = time;
5653
+ }
5654
+ });
5655
+ }
5656
+
5577
5657
  // src/controls/ControlBar.js
5578
5658
  var ControlBar = class {
5579
5659
  constructor(player) {
@@ -6211,13 +6291,117 @@
6211
6291
  this.controls.progressTooltip = DOMUtils.createElement("div", {
6212
6292
  className: "".concat(this.player.options.classPrefix, "-progress-tooltip")
6213
6293
  });
6294
+ this.controls.progressPreview = DOMUtils.createElement("div", {
6295
+ className: "".concat(this.player.options.classPrefix, "-progress-preview"),
6296
+ attributes: {
6297
+ "aria-hidden": "true"
6298
+ }
6299
+ });
6300
+ this.controls.progressTooltip.appendChild(this.controls.progressPreview);
6301
+ this.controls.progressTooltipTime = DOMUtils.createElement("div", {
6302
+ className: "".concat(this.player.options.classPrefix, "-progress-tooltip-time")
6303
+ });
6304
+ this.controls.progressTooltip.appendChild(this.controls.progressTooltipTime);
6214
6305
  progressContainer.appendChild(this.controls.buffered);
6215
6306
  progressContainer.appendChild(this.controls.played);
6216
6307
  this.controls.played.appendChild(this.controls.progressHandle);
6217
6308
  progressContainer.appendChild(this.controls.progressTooltip);
6218
6309
  this.controls.progress = progressContainer;
6310
+ this.initPreviewThumbnail();
6219
6311
  this.setupProgressBarEvents();
6220
6312
  }
6313
+ /**
6314
+ * Initialize preview thumbnail functionality for HTML5 video
6315
+ */
6316
+ initPreviewThumbnail() {
6317
+ this.previewThumbnailCache = /* @__PURE__ */ new Map();
6318
+ this.previewVideo = null;
6319
+ this.currentPreviewTime = null;
6320
+ this.previewThumbnailTimeout = null;
6321
+ this.previewSupported = false;
6322
+ const isVideo = this.player.element && this.player.element.tagName === "VIDEO";
6323
+ if (!isVideo) {
6324
+ return;
6325
+ }
6326
+ const renderer = this.player.renderer;
6327
+ const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === "VIDEO";
6328
+ const isHTML5Renderer = renderer && (renderer.constructor.name === "HTML5Renderer" || renderer.constructor.name === "HLSRenderer" && hasVideoMedia);
6329
+ this.previewSupported = isHTML5Renderer && hasVideoMedia;
6330
+ if (this.previewSupported) {
6331
+ this.previewVideo = document.createElement("video");
6332
+ this.previewVideo.muted = true;
6333
+ this.previewVideo.preload = "metadata";
6334
+ this.previewVideo.style.position = "absolute";
6335
+ this.previewVideo.style.visibility = "hidden";
6336
+ this.previewVideo.style.width = "1px";
6337
+ this.previewVideo.style.height = "1px";
6338
+ this.previewVideo.style.top = "-9999px";
6339
+ const mainVideo = renderer.media || this.player.element;
6340
+ if (mainVideo.src) {
6341
+ this.previewVideo.src = mainVideo.src;
6342
+ } else {
6343
+ const source = mainVideo.querySelector("source");
6344
+ if (source) {
6345
+ this.previewVideo.src = source.src;
6346
+ }
6347
+ }
6348
+ this.previewVideo.addEventListener("error", () => {
6349
+ this.player.log("Preview video failed to load", "warn");
6350
+ this.previewSupported = false;
6351
+ });
6352
+ if (this.player.container) {
6353
+ this.player.container.appendChild(this.previewVideo);
6354
+ }
6355
+ }
6356
+ }
6357
+ /**
6358
+ * Generate preview thumbnail for a specific time
6359
+ * @param {number} time - Time in seconds
6360
+ * @returns {Promise<string>} Data URL of the thumbnail
6361
+ */
6362
+ async generatePreviewThumbnail(time) {
6363
+ if (!this.previewSupported || !this.previewVideo) {
6364
+ return null;
6365
+ }
6366
+ const cacheKey = Math.floor(time);
6367
+ if (this.previewThumbnailCache.has(cacheKey)) {
6368
+ return this.previewThumbnailCache.get(cacheKey);
6369
+ }
6370
+ const dataURL = await captureVideoFrame(this.previewVideo, time, {
6371
+ restoreState: false,
6372
+ quality: 0.8,
6373
+ maxWidth: 160,
6374
+ maxHeight: 90
6375
+ });
6376
+ if (dataURL) {
6377
+ if (this.previewThumbnailCache.size > 20) {
6378
+ const firstKey = this.previewThumbnailCache.keys().next().value;
6379
+ this.previewThumbnailCache.delete(firstKey);
6380
+ }
6381
+ this.previewThumbnailCache.set(cacheKey, dataURL);
6382
+ }
6383
+ return dataURL;
6384
+ }
6385
+ /**
6386
+ * Update preview thumbnail display
6387
+ * @param {number} time - Time in seconds
6388
+ */
6389
+ async updatePreviewThumbnail(time) {
6390
+ if (!this.previewSupported) {
6391
+ return;
6392
+ }
6393
+ if (this.previewThumbnailTimeout) {
6394
+ clearTimeout(this.previewThumbnailTimeout);
6395
+ }
6396
+ this.previewThumbnailTimeout = setTimeout(async () => {
6397
+ const thumbnail = await this.generatePreviewThumbnail(time);
6398
+ if (thumbnail && this.controls.progressPreview) {
6399
+ this.controls.progressPreview.style.backgroundImage = "url(".concat(thumbnail, ")");
6400
+ this.controls.progressPreview.style.display = "block";
6401
+ }
6402
+ this.currentPreviewTime = time;
6403
+ }, 100);
6404
+ }
6221
6405
  setupProgressBarEvents() {
6222
6406
  const progress = this.controls.progress;
6223
6407
  const updateProgress = (clientX) => {
@@ -6244,13 +6428,19 @@
6244
6428
  progress.addEventListener("mousemove", (e) => {
6245
6429
  if (!this.isDraggingProgress) {
6246
6430
  const { time } = updateProgress(e.clientX);
6247
- this.controls.progressTooltip.textContent = TimeUtils.formatTime(time);
6248
- this.controls.progressTooltip.style.left = "".concat(e.clientX - progress.getBoundingClientRect().left, "px");
6431
+ const rect = progress.getBoundingClientRect();
6432
+ const left = e.clientX - rect.left;
6433
+ this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
6434
+ this.controls.progressTooltip.style.left = "".concat(left, "px");
6249
6435
  this.controls.progressTooltip.style.display = "block";
6436
+ this.updatePreviewThumbnail(time);
6250
6437
  }
6251
6438
  });
6252
6439
  progress.addEventListener("mouseleave", () => {
6253
6440
  this.controls.progressTooltip.style.display = "none";
6441
+ if (this.previewThumbnailTimeout) {
6442
+ clearTimeout(this.previewThumbnailTimeout);
6443
+ }
6254
6444
  });
6255
6445
  progress.addEventListener("keydown", (e) => {
6256
6446
  if (e.key === "ArrowLeft") {
@@ -7997,6 +8187,20 @@
7997
8187
  hide() {
7998
8188
  this.element.style.display = "none";
7999
8189
  }
8190
+ /**
8191
+ * Cleanup preview thumbnail resources
8192
+ */
8193
+ cleanupPreviewThumbnail() {
8194
+ if (this.previewThumbnailTimeout) {
8195
+ clearTimeout(this.previewThumbnailTimeout);
8196
+ this.previewThumbnailTimeout = null;
8197
+ }
8198
+ if (this.previewVideo && this.previewVideo.parentNode) {
8199
+ this.previewVideo.parentNode.removeChild(this.previewVideo);
8200
+ this.previewVideo = null;
8201
+ }
8202
+ this.previewThumbnailCache.clear();
8203
+ }
8000
8204
  destroy() {
8001
8205
  if (this.hideTimeout) {
8002
8206
  clearTimeout(this.hideTimeout);
@@ -8004,6 +8208,7 @@
8004
8208
  if (this.overflowResizeObserver) {
8005
8209
  this.overflowResizeObserver.disconnect();
8006
8210
  }
8211
+ this.cleanupPreviewThumbnail();
8007
8212
  if (this.element && this.element.parentNode) {
8008
8213
  this.element.parentNode.removeChild(this.element);
8009
8214
  }
@@ -10635,6 +10840,64 @@
10635
10840
  return posterPath;
10636
10841
  }
10637
10842
  }
10843
+ /**
10844
+ * Generate a poster image from video frame at specified time
10845
+ * @param {number} time - Time in seconds (default: 10)
10846
+ * @returns {Promise<string|null>} Data URL of the poster image or null if failed
10847
+ */
10848
+ async generatePosterFromVideo(time = 10) {
10849
+ if (this.element.tagName !== "VIDEO") {
10850
+ return null;
10851
+ }
10852
+ const renderer = this.renderer;
10853
+ if (!renderer || !renderer.media || renderer.media.tagName !== "VIDEO") {
10854
+ return null;
10855
+ }
10856
+ const video = renderer.media;
10857
+ if (!video.duration || video.duration < time) {
10858
+ time = Math.min(time, Math.max(1, video.duration * 0.1));
10859
+ }
10860
+ let videoToUse = video;
10861
+ if (this.controlBar && this.controlBar.previewVideo && this.controlBar.previewSupported) {
10862
+ videoToUse = this.controlBar.previewVideo;
10863
+ }
10864
+ const restoreState = videoToUse === video;
10865
+ return await captureVideoFrame(videoToUse, time, {
10866
+ restoreState,
10867
+ quality: 0.9
10868
+ });
10869
+ }
10870
+ /**
10871
+ * Auto-generate poster from video if none is provided
10872
+ */
10873
+ async autoGeneratePoster() {
10874
+ const hasPoster = this.element.getAttribute("poster") || this.element.poster || this.options.poster;
10875
+ if (hasPoster) {
10876
+ return;
10877
+ }
10878
+ if (this.element.tagName !== "VIDEO") {
10879
+ return;
10880
+ }
10881
+ if (!this.state.duration || this.state.duration === 0) {
10882
+ await new Promise((resolve) => {
10883
+ const onLoadedMetadata = () => {
10884
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
10885
+ resolve();
10886
+ };
10887
+ if (this.element.readyState >= 1) {
10888
+ resolve();
10889
+ } else {
10890
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
10891
+ }
10892
+ });
10893
+ }
10894
+ const posterDataURL = await this.generatePosterFromVideo(10);
10895
+ if (posterDataURL) {
10896
+ this.element.poster = posterDataURL;
10897
+ this.log("Auto-generated poster from video frame at 10 seconds", "info");
10898
+ this.showPosterOverlay();
10899
+ }
10900
+ }
10638
10901
  showPosterOverlay() {
10639
10902
  if (!this.videoWrapper || this.element.tagName !== "VIDEO") {
10640
10903
  return;
@@ -10643,7 +10906,7 @@
10643
10906
  if (!poster) {
10644
10907
  return;
10645
10908
  }
10646
- const resolvedPoster = this.resolvePosterPath(poster);
10909
+ const resolvedPoster = poster.startsWith("data:") ? poster : this.resolvePosterPath(poster);
10647
10910
  this.videoWrapper.style.setProperty("--vidply-poster-image", 'url("'.concat(resolvedPoster, '")'));
10648
10911
  this.videoWrapper.classList.add("vidply-forced-poster");
10649
10912
  if (this._isAudioContent && this.container) {