@sangwonl/pocato-core 0.4.7 → 0.4.9

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/index.js CHANGED
@@ -381,7 +381,6 @@ function getTransparentTexture() {
381
381
  }
382
382
  return transparentTexture;
383
383
  }
384
- var pendingVideos = /* @__PURE__ */ new Set();
385
384
  function debugLog(message, data) {
386
385
  try {
387
386
  if (globalThis.localStorage?.getItem("pocatoDebug") === "1") {
@@ -408,14 +407,14 @@ function getFreezeTargetTime(freezeTime, duration, seed) {
408
407
  function isAtTargetTime(video, targetTime) {
409
408
  return Math.abs(video.currentTime - targetTime) < 0.05;
410
409
  }
411
- function loadLayerTexture(layer, onError) {
410
+ function loadLayerTexture(layer, options = {}) {
412
411
  if (isVideoSource(layer)) {
413
412
  if (layer.freeze != null) {
414
- return loadFrozenVideoTexture(layer, layer.freeze === "random" ? -1 : layer.freeze, onError);
413
+ return loadFrozenVideoTexture(layer, layer.freeze === "random" ? -1 : layer.freeze, options);
415
414
  }
416
- return loadVideoTexture(layer, onError);
415
+ return loadVideoTexture(layer, options);
417
416
  }
418
- return loadImageTexture(layer.src, onError);
417
+ return loadImageTexture(layer.src, options.onError);
419
418
  }
420
419
  var inflight = /* @__PURE__ */ new Map();
421
420
  function loadImageTexture(src, onError) {
@@ -441,8 +440,13 @@ function loadImageTexture(src, onError) {
441
440
  }
442
441
  return pending.then((texture) => ({ texture }));
443
442
  }
444
- function loadVideoTexture(layer, onError) {
443
+ function loadVideoTexture(layer, options) {
445
444
  return new Promise((resolve) => {
445
+ const { onError, signal } = options;
446
+ if (signal?.aborted) {
447
+ resolve({ texture: getTransparentTexture() });
448
+ return;
449
+ }
446
450
  const video = document.createElement("video");
447
451
  video.crossOrigin = "anonymous";
448
452
  video.muted = true;
@@ -457,33 +461,40 @@ function loadVideoTexture(layer, onError) {
457
461
  video.style.pointerEvents = "none";
458
462
  video.style.zIndex = "-9999";
459
463
  document.body.appendChild(video);
460
- pendingVideos.add(video);
461
464
  let settled = false;
462
- const timeoutId = globalThis.setTimeout(() => {
463
- if (settled) return;
464
- settled = true;
465
- pendingVideos.delete(video);
465
+ const cleanupVideo = () => {
466
+ video.pause();
466
467
  video.src = "";
467
468
  video.remove();
469
+ };
470
+ const finish = (loaded) => {
471
+ if (settled) return;
472
+ settled = true;
473
+ globalThis.clearTimeout(timeoutId);
474
+ signal?.removeEventListener("abort", abort);
475
+ resolve(loaded);
476
+ };
477
+ const abort = () => {
478
+ cleanupVideo();
479
+ finish({ texture: getTransparentTexture() });
480
+ };
481
+ const timeoutId = globalThis.setTimeout(() => {
482
+ if (settled) return;
483
+ cleanupVideo();
468
484
  onError?.(new Error(`Timed out loading video texture: ${layer.src}`));
469
- resolve({ texture: getTransparentTexture() });
485
+ finish({ texture: getTransparentTexture() });
470
486
  }, VIDEO_LOAD_TIMEOUT_MS);
487
+ signal?.addEventListener("abort", abort, { once: true });
471
488
  video.addEventListener("error", () => {
472
489
  if (settled) return;
473
- settled = true;
474
- globalThis.clearTimeout(timeoutId);
475
- pendingVideos.delete(video);
476
490
  const msg = video.error?.message ?? "Unknown video error";
477
491
  onError?.(new Error(`Failed to load video texture: ${layer.src} (${msg})`));
478
- video.remove();
479
- resolve({ texture: getTransparentTexture() });
492
+ cleanupVideo();
493
+ finish({ texture: getTransparentTexture() });
480
494
  }, { once: true });
481
495
  const resolveWithFrame = () => {
482
496
  if (settled) return;
483
- settled = true;
484
- globalThis.clearTimeout(timeoutId);
485
497
  video.pause();
486
- pendingVideos.delete(video);
487
498
  const canvas = document.createElement("canvas");
488
499
  canvas.width = video.videoWidth || 1;
489
500
  canvas.height = video.videoHeight || 1;
@@ -503,7 +514,7 @@ function loadVideoTexture(layer, onError) {
503
514
  }
504
515
  }
505
516
  const liveTexture = new THREE2.VideoTexture(video);
506
- resolve({
517
+ finish({
507
518
  texture: posterTexture,
508
519
  liveTexture,
509
520
  videoEl: video,
@@ -521,8 +532,13 @@ function loadVideoTexture(layer, onError) {
521
532
  video.load();
522
533
  });
523
534
  }
524
- function loadFrozenVideoTexture(layer, freezeTime, onError) {
535
+ function loadFrozenVideoTexture(layer, freezeTime, options) {
525
536
  return new Promise((resolve) => {
537
+ const { onError, signal } = options;
538
+ if (signal?.aborted) {
539
+ resolve({ texture: getTransparentTexture() });
540
+ return;
541
+ }
526
542
  debugLog("freeze load start", {
527
543
  src: layer.src,
528
544
  freezeTime,
@@ -541,13 +557,27 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
541
557
  video.style.pointerEvents = "none";
542
558
  video.style.zIndex = "-9999";
543
559
  document.body.appendChild(video);
544
- pendingVideos.add(video);
545
560
  let settled = false;
546
561
  let targetTime = 0;
547
562
  let retrySeekRegistered = false;
548
- const captureFrame = () => {
563
+ const cleanupVideo = () => {
564
+ video.pause();
565
+ video.src = "";
566
+ video.remove();
567
+ };
568
+ const finish = (loaded) => {
549
569
  if (settled) return;
550
570
  settled = true;
571
+ signal?.removeEventListener("abort", abort);
572
+ resolve(loaded);
573
+ };
574
+ const abort = () => {
575
+ cleanupVideo();
576
+ finish({ texture: getTransparentTexture() });
577
+ };
578
+ signal?.addEventListener("abort", abort, { once: true });
579
+ const captureFrame = () => {
580
+ if (settled) return;
551
581
  video.pause();
552
582
  debugLog("freeze capture frame", {
553
583
  src: layer.src,
@@ -562,10 +592,8 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
562
592
  canvas.height = video.videoHeight || 1;
563
593
  const ctx = canvas.getContext("2d");
564
594
  if (!ctx) {
565
- pendingVideos.delete(video);
566
- video.src = "";
567
- video.remove();
568
- resolve({ texture: getTransparentTexture() });
595
+ cleanupVideo();
596
+ finish({ texture: getTransparentTexture() });
569
597
  return;
570
598
  }
571
599
  try {
@@ -575,11 +603,9 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
575
603
  src: layer.src,
576
604
  error: error instanceof Error ? error.message : String(error)
577
605
  });
578
- pendingVideos.delete(video);
579
- video.src = "";
580
- video.remove();
606
+ cleanupVideo();
581
607
  onError?.(error instanceof Error ? error : new Error(String(error)));
582
- resolve({ texture: getTransparentTexture() });
608
+ finish({ texture: getTransparentTexture() });
583
609
  return;
584
610
  }
585
611
  const texture = new THREE2.CanvasTexture(canvas);
@@ -589,10 +615,8 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
589
615
  width: canvas.width,
590
616
  height: canvas.height
591
617
  });
592
- pendingVideos.delete(video);
593
- video.src = "";
594
- video.remove();
595
- resolve({ texture });
618
+ cleanupVideo();
619
+ finish({ texture });
596
620
  };
597
621
  const captureReadyFrame = () => {
598
622
  debugLog("freeze wait decoded frame", {
@@ -633,11 +657,10 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
633
657
  video.addEventListener("loadeddata", retry, { once: true });
634
658
  };
635
659
  video.addEventListener("error", () => {
636
- pendingVideos.delete(video);
637
660
  const msg = video.error?.message ?? "Unknown video error";
638
661
  onError?.(new Error(`Failed to load video texture: ${layer.src} (${msg})`));
639
- video.remove();
640
- resolve({ texture: getTransparentTexture() });
662
+ cleanupVideo();
663
+ finish({ texture: getTransparentTexture() });
641
664
  }, { once: true });
642
665
  video.addEventListener("loadedmetadata", () => {
643
666
  const time = getFreezeTargetTime(freezeTime, video.duration, layer.freezeSeed);
@@ -689,14 +712,6 @@ function disposeLoadedLayer(loaded) {
689
712
  loaded.liveTexture.dispose();
690
713
  }
691
714
  }
692
- function cleanupPendingVideos() {
693
- for (const video of pendingVideos) {
694
- video.pause();
695
- video.src = "";
696
- video.remove();
697
- }
698
- pendingVideos.clear();
699
- }
700
715
 
701
716
  // src/shaders/common.vert.ts
702
717
  var common_vert_default = glsl`
@@ -2011,6 +2026,7 @@ var FaceRenderer = class {
2011
2026
  constructor(shader, width, height, effectUniforms) {
2012
2027
  this.loadedLayers = [];
2013
2028
  this.layerLoadVersion = 0;
2029
+ this.layerLoadAbort = null;
2014
2030
  bootstrapShaders();
2015
2031
  this.scene = new THREE3.Scene();
2016
2032
  this.camera = new THREE3.Camera();
@@ -2061,19 +2077,25 @@ var FaceRenderer = class {
2061
2077
  }
2062
2078
  async loadLayers(layers, onError) {
2063
2079
  const version = ++this.layerLoadVersion;
2080
+ this.layerLoadAbort?.abort();
2081
+ const abort = new AbortController();
2082
+ this.layerLoadAbort = abort;
2064
2083
  this.disposeLayers();
2065
2084
  const count = Math.min(layers.length, MAX_LAYERS);
2066
2085
  this.material.uniforms.uLayerCount.value = count;
2067
2086
  const promises = layers.slice(0, MAX_LAYERS).map(
2068
- (layer) => loadLayerTexture(layer, onError)
2087
+ (layer) => loadLayerTexture(layer, { onError, signal: abort.signal })
2069
2088
  );
2070
2089
  const results = await Promise.all(promises);
2071
- if (version !== this.layerLoadVersion) {
2090
+ if (version !== this.layerLoadVersion || abort.signal.aborted) {
2072
2091
  for (const loaded of results) {
2073
2092
  disposeLoadedLayer(loaded);
2074
2093
  }
2075
2094
  return;
2076
2095
  }
2096
+ if (this.layerLoadAbort === abort) {
2097
+ this.layerLoadAbort = null;
2098
+ }
2077
2099
  this.loadedLayers = results;
2078
2100
  for (let i = 0; i < results.length; i++) {
2079
2101
  this.material.uniforms[`uLayer${i}`].value = results[i].texture;
@@ -2119,6 +2141,8 @@ var FaceRenderer = class {
2119
2141
  }
2120
2142
  destroy() {
2121
2143
  this.layerLoadVersion++;
2144
+ this.layerLoadAbort?.abort();
2145
+ this.layerLoadAbort = null;
2122
2146
  this.disposeLayers();
2123
2147
  this.mesh.geometry.dispose();
2124
2148
  this.material.dispose();
@@ -2157,6 +2181,7 @@ var Renderer = class {
2157
2181
  this.layersReady = false;
2158
2182
  this.firstFrameEmitted = false;
2159
2183
  this.firstFrameScheduled = false;
2184
+ this.mediaActivated = false;
2160
2185
  if (!options.front?.layers?.length) {
2161
2186
  console.warn("[pocato] front.layers must have at least 1 element");
2162
2187
  }
@@ -2318,19 +2343,20 @@ var Renderer = class {
2318
2343
  }
2319
2344
  scheduleFirstFrame() {
2320
2345
  if (!this.layersReady || this.firstFrameEmitted || this.firstFrameScheduled) return;
2321
- this.firstFrameScheduled = true;
2346
+ if (!this.mediaActivated) {
2347
+ this.mediaActivated = true;
2348
+ this.frontFace.playLoadedVideos();
2349
+ this.backFace?.playLoadedVideos();
2350
+ return;
2351
+ }
2322
2352
  requestAnimationFrame(() => {
2323
2353
  if (this.destroyed || this.firstFrameEmitted) return;
2324
2354
  this.firstFrameEmitted = true;
2325
2355
  this.cardEl.classList.remove("pocato-loading");
2326
2356
  this.cardEl.classList.add("pocato-ready");
2327
2357
  this.onFirstFrame?.();
2328
- requestAnimationFrame(() => {
2329
- if (this.destroyed) return;
2330
- this.frontFace.playLoadedVideos();
2331
- this.backFace?.playLoadedVideos();
2332
- });
2333
2358
  });
2359
+ this.firstFrameScheduled = true;
2334
2360
  }
2335
2361
  injectStyles() {
2336
2362
  const id = "pocato-styles";
@@ -2593,7 +2619,6 @@ var Renderer = class {
2593
2619
  this.stopRenderLoop();
2594
2620
  this.resizeObserver?.disconnect();
2595
2621
  this.intersectionObserver?.disconnect();
2596
- cleanupPendingVideos();
2597
2622
  this.frontFace.destroy();
2598
2623
  if (this.frontRenderer) {
2599
2624
  this.disposeWebGLRenderer(this.frontRenderer);