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.
@@ -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,88 @@
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 if (video.readyState >= 1) {
5651
+ video.addEventListener("seeked", onSeeked);
5652
+ video.currentTime = time;
5653
+ } else {
5654
+ const onLoadedMetadata = () => {
5655
+ video.removeEventListener("loadedmetadata", onLoadedMetadata);
5656
+ video.addEventListener("seeked", onSeeked);
5657
+ video.currentTime = time;
5658
+ };
5659
+ video.addEventListener("loadedmetadata", onLoadedMetadata);
5660
+ }
5661
+ });
5662
+ }
5663
+
5577
5664
  // src/controls/ControlBar.js
5578
5665
  var ControlBar = class {
5579
5666
  constructor(player) {
@@ -6211,13 +6298,184 @@
6211
6298
  this.controls.progressTooltip = DOMUtils.createElement("div", {
6212
6299
  className: "".concat(this.player.options.classPrefix, "-progress-tooltip")
6213
6300
  });
6301
+ this.controls.progressPreview = DOMUtils.createElement("div", {
6302
+ className: "".concat(this.player.options.classPrefix, "-progress-preview"),
6303
+ attributes: {
6304
+ "aria-hidden": "true"
6305
+ }
6306
+ });
6307
+ this.controls.progressTooltip.appendChild(this.controls.progressPreview);
6308
+ this.controls.progressTooltipTime = DOMUtils.createElement("div", {
6309
+ className: "".concat(this.player.options.classPrefix, "-progress-tooltip-time")
6310
+ });
6311
+ this.controls.progressTooltip.appendChild(this.controls.progressTooltipTime);
6214
6312
  progressContainer.appendChild(this.controls.buffered);
6215
6313
  progressContainer.appendChild(this.controls.played);
6216
6314
  this.controls.played.appendChild(this.controls.progressHandle);
6217
6315
  progressContainer.appendChild(this.controls.progressTooltip);
6218
6316
  this.controls.progress = progressContainer;
6317
+ this.initPreviewThumbnail();
6219
6318
  this.setupProgressBarEvents();
6220
6319
  }
6320
+ /**
6321
+ * Initialize preview thumbnail functionality for HTML5 video
6322
+ */
6323
+ initPreviewThumbnail() {
6324
+ this.previewThumbnailCache = /* @__PURE__ */ new Map();
6325
+ this.previewVideo = null;
6326
+ this.currentPreviewTime = null;
6327
+ this.previewThumbnailTimeout = null;
6328
+ this.previewSupported = false;
6329
+ const isVideo = this.player.element && this.player.element.tagName === "VIDEO";
6330
+ if (!isVideo) {
6331
+ return;
6332
+ }
6333
+ const renderer = this.player.renderer;
6334
+ const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === "VIDEO";
6335
+ const isHTML5Renderer = hasVideoMedia && renderer.media === this.player.element && !renderer.hls && typeof renderer.seek === "function";
6336
+ this.previewSupported = isHTML5Renderer && hasVideoMedia;
6337
+ if (this.previewSupported) {
6338
+ this.previewVideo = document.createElement("video");
6339
+ this.previewVideo.muted = true;
6340
+ this.previewVideo.preload = "auto";
6341
+ this.previewVideo.playsInline = true;
6342
+ this.previewVideo.style.position = "absolute";
6343
+ this.previewVideo.style.visibility = "hidden";
6344
+ this.previewVideo.style.width = "1px";
6345
+ this.previewVideo.style.height = "1px";
6346
+ this.previewVideo.style.top = "-9999px";
6347
+ const mainVideo = renderer.media || this.player.element;
6348
+ let videoSrc = null;
6349
+ if (mainVideo.src) {
6350
+ videoSrc = mainVideo.src;
6351
+ } else {
6352
+ const source = mainVideo.querySelector("source");
6353
+ if (source) {
6354
+ videoSrc = source.src;
6355
+ }
6356
+ }
6357
+ if (!videoSrc) {
6358
+ this.player.log("No video source found for preview", "warn");
6359
+ this.previewSupported = false;
6360
+ return;
6361
+ }
6362
+ if (mainVideo.crossOrigin) {
6363
+ this.previewVideo.crossOrigin = mainVideo.crossOrigin;
6364
+ }
6365
+ this.previewVideo.addEventListener("error", (e) => {
6366
+ this.player.log("Preview video failed to load:", e, "warn");
6367
+ this.previewSupported = false;
6368
+ });
6369
+ this.previewVideo.addEventListener("loadedmetadata", () => {
6370
+ this.previewVideoReady = true;
6371
+ }, { once: true });
6372
+ if (this.player.container) {
6373
+ this.player.container.appendChild(this.previewVideo);
6374
+ }
6375
+ this.previewVideo.src = videoSrc;
6376
+ this.previewVideoReady = false;
6377
+ }
6378
+ }
6379
+ /**
6380
+ * Generate preview thumbnail for a specific time
6381
+ * @param {number} time - Time in seconds
6382
+ * @returns {Promise<string>} Data URL of the thumbnail
6383
+ */
6384
+ async generatePreviewThumbnail(time) {
6385
+ if (!this.previewSupported || !this.previewVideo) {
6386
+ return null;
6387
+ }
6388
+ if (!this.previewVideoReady) {
6389
+ if (this.previewVideo.readyState < 2) {
6390
+ await new Promise((resolve, reject) => {
6391
+ const timeout = setTimeout(() => {
6392
+ reject(new Error("Preview video data load timeout"));
6393
+ }, 1e4);
6394
+ const cleanup = () => {
6395
+ clearTimeout(timeout);
6396
+ this.previewVideo.removeEventListener("loadeddata", checkReady);
6397
+ this.previewVideo.removeEventListener("canplay", checkReady);
6398
+ this.previewVideo.removeEventListener("error", onError);
6399
+ };
6400
+ const checkReady = () => {
6401
+ if (this.previewVideo.readyState >= 2) {
6402
+ cleanup();
6403
+ this.previewVideoReady = true;
6404
+ resolve();
6405
+ }
6406
+ };
6407
+ const onError = () => {
6408
+ cleanup();
6409
+ reject(new Error("Preview video failed to load"));
6410
+ };
6411
+ if (this.previewVideo.readyState >= 1) {
6412
+ this.previewVideo.addEventListener("loadeddata", checkReady);
6413
+ }
6414
+ this.previewVideo.addEventListener("canplay", checkReady);
6415
+ this.previewVideo.addEventListener("error", onError);
6416
+ if (this.previewVideo.readyState >= 2) {
6417
+ checkReady();
6418
+ }
6419
+ }).catch(() => {
6420
+ this.previewSupported = false;
6421
+ return null;
6422
+ });
6423
+ } else {
6424
+ this.previewVideoReady = true;
6425
+ }
6426
+ }
6427
+ const cacheKey = Math.floor(time);
6428
+ if (this.previewThumbnailCache.has(cacheKey)) {
6429
+ return this.previewThumbnailCache.get(cacheKey);
6430
+ }
6431
+ const dataURL = await captureVideoFrame(this.previewVideo, time, {
6432
+ restoreState: false,
6433
+ quality: 0.8,
6434
+ maxWidth: 160,
6435
+ maxHeight: 90
6436
+ });
6437
+ if (dataURL) {
6438
+ if (this.previewThumbnailCache.size >= 20) {
6439
+ const firstKey = this.previewThumbnailCache.keys().next().value;
6440
+ this.previewThumbnailCache.delete(firstKey);
6441
+ }
6442
+ this.previewThumbnailCache.set(cacheKey, dataURL);
6443
+ }
6444
+ return dataURL;
6445
+ }
6446
+ /**
6447
+ * Update preview thumbnail display
6448
+ * @param {number} time - Time in seconds
6449
+ */
6450
+ async updatePreviewThumbnail(time) {
6451
+ if (!this.previewSupported || !this.controls.progressPreview) {
6452
+ return;
6453
+ }
6454
+ if (this.previewThumbnailTimeout) {
6455
+ clearTimeout(this.previewThumbnailTimeout);
6456
+ }
6457
+ this.previewThumbnailTimeout = setTimeout(async () => {
6458
+ try {
6459
+ const thumbnail = await this.generatePreviewThumbnail(time);
6460
+ if (thumbnail && this.controls.progressPreview) {
6461
+ this.controls.progressPreview.style.backgroundImage = 'url("'.concat(thumbnail, '")');
6462
+ this.controls.progressPreview.style.display = "block";
6463
+ this.controls.progressPreview.style.backgroundRepeat = "no-repeat";
6464
+ this.controls.progressPreview.style.backgroundPosition = "center";
6465
+ } else {
6466
+ if (this.controls.progressPreview) {
6467
+ this.controls.progressPreview.style.display = "none";
6468
+ }
6469
+ }
6470
+ this.currentPreviewTime = time;
6471
+ } catch (error) {
6472
+ this.player.log("Preview thumbnail update failed:", error, "warn");
6473
+ if (this.controls.progressPreview) {
6474
+ this.controls.progressPreview.style.display = "none";
6475
+ }
6476
+ }
6477
+ }, 100);
6478
+ }
6221
6479
  setupProgressBarEvents() {
6222
6480
  const progress = this.controls.progress;
6223
6481
  const updateProgress = (clientX) => {
@@ -6244,13 +6502,21 @@
6244
6502
  progress.addEventListener("mousemove", (e) => {
6245
6503
  if (!this.isDraggingProgress) {
6246
6504
  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");
6505
+ const rect = progress.getBoundingClientRect();
6506
+ const left = e.clientX - rect.left;
6507
+ this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
6508
+ this.controls.progressTooltip.style.left = "".concat(left, "px");
6249
6509
  this.controls.progressTooltip.style.display = "block";
6510
+ if (this.previewSupported) {
6511
+ this.updatePreviewThumbnail(time);
6512
+ }
6250
6513
  }
6251
6514
  });
6252
6515
  progress.addEventListener("mouseleave", () => {
6253
6516
  this.controls.progressTooltip.style.display = "none";
6517
+ if (this.previewThumbnailTimeout) {
6518
+ clearTimeout(this.previewThumbnailTimeout);
6519
+ }
6254
6520
  });
6255
6521
  progress.addEventListener("keydown", (e) => {
6256
6522
  if (e.key === "ArrowLeft") {
@@ -7530,6 +7796,10 @@
7530
7796
  this.updateDuration();
7531
7797
  this.ensureQualityButton();
7532
7798
  this.updateQualityIndicator();
7799
+ this.updatePreviewVideoSource();
7800
+ });
7801
+ this.player.on("sourcechange", () => {
7802
+ this.updatePreviewVideoSource();
7533
7803
  });
7534
7804
  this.player.on("volumechange", () => this.updateVolumeDisplay());
7535
7805
  this.player.on("progress", () => this.updateBuffered());
@@ -7997,6 +8267,52 @@
7997
8267
  hide() {
7998
8268
  this.element.style.display = "none";
7999
8269
  }
8270
+ /**
8271
+ * Update preview video source when player source changes (for playlists)
8272
+ * Also re-initializes if preview wasn't set up initially
8273
+ */
8274
+ updatePreviewVideoSource() {
8275
+ var _a;
8276
+ const renderer = this.player.renderer;
8277
+ if (!renderer || !renderer.media || renderer.media.tagName !== "VIDEO") {
8278
+ return;
8279
+ }
8280
+ if (!this.previewSupported && !this.previewVideo) {
8281
+ this.initPreviewThumbnail();
8282
+ }
8283
+ if (!this.previewSupported || !this.previewVideo) {
8284
+ return;
8285
+ }
8286
+ const mainVideo = renderer.media;
8287
+ const newSrc = mainVideo.src || ((_a = mainVideo.querySelector("source")) == null ? void 0 : _a.src);
8288
+ if (newSrc && this.previewVideo.src !== newSrc) {
8289
+ this.previewThumbnailCache.clear();
8290
+ this.previewVideoReady = false;
8291
+ this.previewVideo.src = newSrc;
8292
+ if (mainVideo.crossOrigin) {
8293
+ this.previewVideo.crossOrigin = mainVideo.crossOrigin;
8294
+ }
8295
+ this.previewVideo.addEventListener("loadedmetadata", () => {
8296
+ this.previewVideoReady = true;
8297
+ }, { once: true });
8298
+ } else if (newSrc && !this.previewVideoReady && this.previewVideo.readyState >= 1) {
8299
+ this.previewVideoReady = true;
8300
+ }
8301
+ }
8302
+ /**
8303
+ * Cleanup preview thumbnail resources
8304
+ */
8305
+ cleanupPreviewThumbnail() {
8306
+ if (this.previewThumbnailTimeout) {
8307
+ clearTimeout(this.previewThumbnailTimeout);
8308
+ this.previewThumbnailTimeout = null;
8309
+ }
8310
+ if (this.previewVideo && this.previewVideo.parentNode) {
8311
+ this.previewVideo.parentNode.removeChild(this.previewVideo);
8312
+ this.previewVideo = null;
8313
+ }
8314
+ this.previewThumbnailCache.clear();
8315
+ }
8000
8316
  destroy() {
8001
8317
  if (this.hideTimeout) {
8002
8318
  clearTimeout(this.hideTimeout);
@@ -8004,6 +8320,7 @@
8004
8320
  if (this.overflowResizeObserver) {
8005
8321
  this.overflowResizeObserver.disconnect();
8006
8322
  }
8323
+ this.cleanupPreviewThumbnail();
8007
8324
  if (this.element && this.element.parentNode) {
8008
8325
  this.element.parentNode.removeChild(this.element);
8009
8326
  }
@@ -10635,6 +10952,64 @@
10635
10952
  return posterPath;
10636
10953
  }
10637
10954
  }
10955
+ /**
10956
+ * Generate a poster image from video frame at specified time
10957
+ * @param {number} time - Time in seconds (default: 10)
10958
+ * @returns {Promise<string|null>} Data URL of the poster image or null if failed
10959
+ */
10960
+ async generatePosterFromVideo(time = 10) {
10961
+ if (this.element.tagName !== "VIDEO") {
10962
+ return null;
10963
+ }
10964
+ const renderer = this.renderer;
10965
+ if (!renderer || !renderer.media || renderer.media.tagName !== "VIDEO") {
10966
+ return null;
10967
+ }
10968
+ const video = renderer.media;
10969
+ if (!video.duration || video.duration < time) {
10970
+ time = Math.min(time, Math.max(1, video.duration * 0.1));
10971
+ }
10972
+ let videoToUse = video;
10973
+ if (this.controlBar && this.controlBar.previewVideo && this.controlBar.previewSupported) {
10974
+ videoToUse = this.controlBar.previewVideo;
10975
+ }
10976
+ const restoreState = videoToUse === video;
10977
+ return await captureVideoFrame(videoToUse, time, {
10978
+ restoreState,
10979
+ quality: 0.9
10980
+ });
10981
+ }
10982
+ /**
10983
+ * Auto-generate poster from video if none is provided
10984
+ */
10985
+ async autoGeneratePoster() {
10986
+ const hasPoster = this.element.getAttribute("poster") || this.element.poster || this.options.poster;
10987
+ if (hasPoster) {
10988
+ return;
10989
+ }
10990
+ if (this.element.tagName !== "VIDEO") {
10991
+ return;
10992
+ }
10993
+ if (!this.state.duration || this.state.duration === 0) {
10994
+ await new Promise((resolve) => {
10995
+ const onLoadedMetadata = () => {
10996
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
10997
+ resolve();
10998
+ };
10999
+ if (this.element.readyState >= 1) {
11000
+ resolve();
11001
+ } else {
11002
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
11003
+ }
11004
+ });
11005
+ }
11006
+ const posterDataURL = await this.generatePosterFromVideo(10);
11007
+ if (posterDataURL) {
11008
+ this.element.poster = posterDataURL;
11009
+ this.log("Auto-generated poster from video frame at 10 seconds", "info");
11010
+ this.showPosterOverlay();
11011
+ }
11012
+ }
10638
11013
  showPosterOverlay() {
10639
11014
  if (!this.videoWrapper || this.element.tagName !== "VIDEO") {
10640
11015
  return;
@@ -10643,7 +11018,7 @@
10643
11018
  if (!poster) {
10644
11019
  return;
10645
11020
  }
10646
- const resolvedPoster = this.resolvePosterPath(poster);
11021
+ const resolvedPoster = poster.startsWith("data:") ? poster : this.resolvePosterPath(poster);
10647
11022
  this.videoWrapper.style.setProperty("--vidply-poster-image", 'url("'.concat(resolvedPoster, '")'));
10648
11023
  this.videoWrapper.classList.add("vidply-forced-poster");
10649
11024
  if (this._isAudioContent && this.container) {