@sangwonl/pocato-core 0.4.6 → 0.4.8

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,
@@ -515,18 +526,19 @@ function loadVideoTexture(layer, onError) {
515
526
  });
516
527
  };
517
528
  video.addEventListener("loadeddata", () => {
518
- if ("requestVideoFrameCallback" in video) {
519
- video.requestVideoFrameCallback(() => resolveWithFrame());
520
- return;
521
- }
522
- window.setTimeout(resolveWithFrame, 0);
529
+ resolveWithFrame();
523
530
  }, { once: true });
524
531
  video.src = layer.src;
525
532
  video.load();
526
533
  });
527
534
  }
528
- function loadFrozenVideoTexture(layer, freezeTime, onError) {
535
+ function loadFrozenVideoTexture(layer, freezeTime, options) {
529
536
  return new Promise((resolve) => {
537
+ const { onError, signal } = options;
538
+ if (signal?.aborted) {
539
+ resolve({ texture: getTransparentTexture() });
540
+ return;
541
+ }
530
542
  debugLog("freeze load start", {
531
543
  src: layer.src,
532
544
  freezeTime,
@@ -545,13 +557,27 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
545
557
  video.style.pointerEvents = "none";
546
558
  video.style.zIndex = "-9999";
547
559
  document.body.appendChild(video);
548
- pendingVideos.add(video);
549
560
  let settled = false;
550
561
  let targetTime = 0;
551
562
  let retrySeekRegistered = false;
552
- const captureFrame = () => {
563
+ const cleanupVideo = () => {
564
+ video.pause();
565
+ video.src = "";
566
+ video.remove();
567
+ };
568
+ const finish = (loaded) => {
553
569
  if (settled) return;
554
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;
555
581
  video.pause();
556
582
  debugLog("freeze capture frame", {
557
583
  src: layer.src,
@@ -566,10 +592,8 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
566
592
  canvas.height = video.videoHeight || 1;
567
593
  const ctx = canvas.getContext("2d");
568
594
  if (!ctx) {
569
- pendingVideos.delete(video);
570
- video.src = "";
571
- video.remove();
572
- resolve({ texture: getTransparentTexture() });
595
+ cleanupVideo();
596
+ finish({ texture: getTransparentTexture() });
573
597
  return;
574
598
  }
575
599
  try {
@@ -579,11 +603,9 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
579
603
  src: layer.src,
580
604
  error: error instanceof Error ? error.message : String(error)
581
605
  });
582
- pendingVideos.delete(video);
583
- video.src = "";
584
- video.remove();
606
+ cleanupVideo();
585
607
  onError?.(error instanceof Error ? error : new Error(String(error)));
586
- resolve({ texture: getTransparentTexture() });
608
+ finish({ texture: getTransparentTexture() });
587
609
  return;
588
610
  }
589
611
  const texture = new THREE2.CanvasTexture(canvas);
@@ -593,10 +615,8 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
593
615
  width: canvas.width,
594
616
  height: canvas.height
595
617
  });
596
- pendingVideos.delete(video);
597
- video.src = "";
598
- video.remove();
599
- resolve({ texture });
618
+ cleanupVideo();
619
+ finish({ texture });
600
620
  };
601
621
  const captureReadyFrame = () => {
602
622
  debugLog("freeze wait decoded frame", {
@@ -637,11 +657,10 @@ function loadFrozenVideoTexture(layer, freezeTime, onError) {
637
657
  video.addEventListener("loadeddata", retry, { once: true });
638
658
  };
639
659
  video.addEventListener("error", () => {
640
- pendingVideos.delete(video);
641
660
  const msg = video.error?.message ?? "Unknown video error";
642
661
  onError?.(new Error(`Failed to load video texture: ${layer.src} (${msg})`));
643
- video.remove();
644
- resolve({ texture: getTransparentTexture() });
662
+ cleanupVideo();
663
+ finish({ texture: getTransparentTexture() });
645
664
  }, { once: true });
646
665
  video.addEventListener("loadedmetadata", () => {
647
666
  const time = getFreezeTargetTime(freezeTime, video.duration, layer.freezeSeed);
@@ -693,14 +712,6 @@ function disposeLoadedLayer(loaded) {
693
712
  loaded.liveTexture.dispose();
694
713
  }
695
714
  }
696
- function cleanupPendingVideos() {
697
- for (const video of pendingVideos) {
698
- video.pause();
699
- video.src = "";
700
- video.remove();
701
- }
702
- pendingVideos.clear();
703
- }
704
715
 
705
716
  // src/shaders/common.vert.ts
706
717
  var common_vert_default = glsl`
@@ -2015,6 +2026,7 @@ var FaceRenderer = class {
2015
2026
  constructor(shader, width, height, effectUniforms) {
2016
2027
  this.loadedLayers = [];
2017
2028
  this.layerLoadVersion = 0;
2029
+ this.layerLoadAbort = null;
2018
2030
  bootstrapShaders();
2019
2031
  this.scene = new THREE3.Scene();
2020
2032
  this.camera = new THREE3.Camera();
@@ -2065,19 +2077,25 @@ var FaceRenderer = class {
2065
2077
  }
2066
2078
  async loadLayers(layers, onError) {
2067
2079
  const version = ++this.layerLoadVersion;
2080
+ this.layerLoadAbort?.abort();
2081
+ const abort = new AbortController();
2082
+ this.layerLoadAbort = abort;
2068
2083
  this.disposeLayers();
2069
2084
  const count = Math.min(layers.length, MAX_LAYERS);
2070
2085
  this.material.uniforms.uLayerCount.value = count;
2071
2086
  const promises = layers.slice(0, MAX_LAYERS).map(
2072
- (layer) => loadLayerTexture(layer, onError)
2087
+ (layer) => loadLayerTexture(layer, { onError, signal: abort.signal })
2073
2088
  );
2074
2089
  const results = await Promise.all(promises);
2075
- if (version !== this.layerLoadVersion) {
2090
+ if (version !== this.layerLoadVersion || abort.signal.aborted) {
2076
2091
  for (const loaded of results) {
2077
2092
  disposeLoadedLayer(loaded);
2078
2093
  }
2079
2094
  return;
2080
2095
  }
2096
+ if (this.layerLoadAbort === abort) {
2097
+ this.layerLoadAbort = null;
2098
+ }
2081
2099
  this.loadedLayers = results;
2082
2100
  for (let i = 0; i < results.length; i++) {
2083
2101
  this.material.uniforms[`uLayer${i}`].value = results[i].texture;
@@ -2123,6 +2141,8 @@ var FaceRenderer = class {
2123
2141
  }
2124
2142
  destroy() {
2125
2143
  this.layerLoadVersion++;
2144
+ this.layerLoadAbort?.abort();
2145
+ this.layerLoadAbort = null;
2126
2146
  this.disposeLayers();
2127
2147
  this.mesh.geometry.dispose();
2128
2148
  this.material.dispose();
@@ -2597,7 +2617,6 @@ var Renderer = class {
2597
2617
  this.stopRenderLoop();
2598
2618
  this.resizeObserver?.disconnect();
2599
2619
  this.intersectionObserver?.disconnect();
2600
- cleanupPendingVideos();
2601
2620
  this.frontFace.destroy();
2602
2621
  if (this.frontRenderer) {
2603
2622
  this.disposeWebGLRenderer(this.frontRenderer);