@sangwonl/pocato-core 0.4.4 → 0.4.6

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
@@ -367,6 +367,7 @@ function bootstrapShaders() {
367
367
  // src/renderer/texture-loader.ts
368
368
  import * as THREE2 from "three";
369
369
  var VIDEO_EXTENSIONS = /\.(mp4|webm|mov|ogg)(\?|$)/i;
370
+ var VIDEO_LOAD_TIMEOUT_MS = 15e3;
370
371
  function isVideoSource(layer) {
371
372
  if (layer.type) return layer.type === "video";
372
373
  return VIDEO_EXTENSIONS.test(layer.src);
@@ -457,24 +458,71 @@ function loadVideoTexture(layer, onError) {
457
458
  video.style.zIndex = "-9999";
458
459
  document.body.appendChild(video);
459
460
  pendingVideos.add(video);
461
+ let settled = false;
462
+ const timeoutId = globalThis.setTimeout(() => {
463
+ if (settled) return;
464
+ settled = true;
465
+ pendingVideos.delete(video);
466
+ video.src = "";
467
+ video.remove();
468
+ onError?.(new Error(`Timed out loading video texture: ${layer.src}`));
469
+ resolve({ texture: getTransparentTexture() });
470
+ }, VIDEO_LOAD_TIMEOUT_MS);
460
471
  video.addEventListener("error", () => {
472
+ if (settled) return;
473
+ settled = true;
474
+ globalThis.clearTimeout(timeoutId);
461
475
  pendingVideos.delete(video);
462
476
  const msg = video.error?.message ?? "Unknown video error";
463
477
  onError?.(new Error(`Failed to load video texture: ${layer.src} (${msg})`));
464
478
  video.remove();
465
479
  resolve({ texture: getTransparentTexture() });
466
480
  }, { once: true });
467
- video.src = layer.src;
468
- video.play().then(() => {
469
- pendingVideos.delete(video);
470
- const texture = new THREE2.VideoTexture(video);
471
- resolve({ texture, videoEl: video });
472
- }).catch(() => {
481
+ const resolveWithFrame = () => {
482
+ if (settled) return;
483
+ settled = true;
484
+ globalThis.clearTimeout(timeoutId);
485
+ video.pause();
473
486
  pendingVideos.delete(video);
474
- onError?.(new Error(`Video autoplay blocked: ${layer.src}`));
475
- video.remove();
476
- resolve({ texture: getTransparentTexture() });
477
- });
487
+ const canvas = document.createElement("canvas");
488
+ canvas.width = video.videoWidth || 1;
489
+ canvas.height = video.videoHeight || 1;
490
+ const ctx = canvas.getContext("2d");
491
+ let posterTexture = getTransparentTexture();
492
+ if (ctx) {
493
+ try {
494
+ ctx.drawImage(video, 0, 0);
495
+ posterTexture = new THREE2.CanvasTexture(canvas);
496
+ posterTexture.needsUpdate = true;
497
+ } catch (error) {
498
+ debugLog("video poster drawImage failed", {
499
+ src: layer.src,
500
+ error: error instanceof Error ? error.message : String(error)
501
+ });
502
+ onError?.(error instanceof Error ? error : new Error(String(error)));
503
+ }
504
+ }
505
+ const liveTexture = new THREE2.VideoTexture(video);
506
+ resolve({
507
+ texture: posterTexture,
508
+ liveTexture,
509
+ videoEl: video,
510
+ play: () => {
511
+ video.play().catch(() => {
512
+ onError?.(new Error(`Video autoplay blocked: ${layer.src}`));
513
+ });
514
+ }
515
+ });
516
+ };
517
+ video.addEventListener("loadeddata", () => {
518
+ if ("requestVideoFrameCallback" in video) {
519
+ video.requestVideoFrameCallback(() => resolveWithFrame());
520
+ return;
521
+ }
522
+ window.setTimeout(resolveWithFrame, 0);
523
+ }, { once: true });
524
+ video.src = layer.src;
525
+ video.load();
478
526
  });
479
527
  }
480
528
  function loadFrozenVideoTexture(layer, freezeTime, onError) {
@@ -641,6 +689,9 @@ function disposeLoadedLayer(loaded) {
641
689
  if (loaded.texture !== getTransparentTexture()) {
642
690
  loaded.texture.dispose();
643
691
  }
692
+ if (loaded.liveTexture && loaded.liveTexture !== loaded.texture && loaded.liveTexture !== getTransparentTexture()) {
693
+ loaded.liveTexture.dispose();
694
+ }
644
695
  }
645
696
  function cleanupPendingVideos() {
646
697
  for (const video of pendingVideos) {
@@ -2046,6 +2097,15 @@ var FaceRenderer = class {
2046
2097
  updateResolution(width, height) {
2047
2098
  this.material.uniforms.uResolution.value.set(width, height);
2048
2099
  }
2100
+ playLoadedVideos() {
2101
+ for (let i = 0; i < this.loadedLayers.length; i++) {
2102
+ const loaded = this.loadedLayers[i];
2103
+ if (loaded.liveTexture) {
2104
+ this.material.uniforms[`uLayer${i}`].value = loaded.liveTexture;
2105
+ }
2106
+ loaded.play?.();
2107
+ }
2108
+ }
2049
2109
  updateShader(fragmentShader) {
2050
2110
  this.material.fragmentShader = resolveIncludes(fragmentShader);
2051
2111
  this.material.needsUpdate = true;
@@ -2082,11 +2142,12 @@ function toThreeUniformValue(value) {
2082
2142
 
2083
2143
  // src/renderer/index.ts
2084
2144
  var Renderer = class {
2085
- constructor(container, options, onError, onReady) {
2145
+ constructor(container, options, onError, onReady, onFirstFrame) {
2086
2146
  this.container = container;
2087
2147
  this.options = options;
2088
2148
  this.onError = onError;
2089
2149
  this.onReady = onReady;
2150
+ this.onFirstFrame = onFirstFrame;
2090
2151
  this.backFace = null;
2091
2152
  this.frontRenderer = null;
2092
2153
  this.backRenderer = null;
@@ -2097,6 +2158,9 @@ var Renderer = class {
2097
2158
  this.intersectionObserver = null;
2098
2159
  this.visible = false;
2099
2160
  this.destroyed = false;
2161
+ this.layersReady = false;
2162
+ this.firstFrameEmitted = false;
2163
+ this.firstFrameScheduled = false;
2100
2164
  if (!options.front?.layers?.length) {
2101
2165
  console.warn("[pocato] front.layers must have at least 1 element");
2102
2166
  }
@@ -2253,11 +2317,23 @@ var Renderer = class {
2253
2317
  }
2254
2318
  await Promise.all(promises);
2255
2319
  if (this.destroyed) return;
2320
+ this.layersReady = true;
2321
+ this.onReady?.();
2322
+ }
2323
+ scheduleFirstFrame() {
2324
+ if (!this.layersReady || this.firstFrameEmitted || this.firstFrameScheduled) return;
2325
+ this.firstFrameScheduled = true;
2256
2326
  requestAnimationFrame(() => {
2257
- if (this.destroyed) return;
2327
+ if (this.destroyed || this.firstFrameEmitted) return;
2328
+ this.firstFrameEmitted = true;
2258
2329
  this.cardEl.classList.remove("pocato-loading");
2259
2330
  this.cardEl.classList.add("pocato-ready");
2260
- this.onReady?.();
2331
+ this.onFirstFrame?.();
2332
+ requestAnimationFrame(() => {
2333
+ if (this.destroyed) return;
2334
+ this.frontFace.playLoadedVideos();
2335
+ this.backFace?.playLoadedVideos();
2336
+ });
2261
2337
  });
2262
2338
  }
2263
2339
  injectStyles() {
@@ -2429,6 +2505,7 @@ var Renderer = class {
2429
2505
  this.backFace.material.uniforms.uTime.value += delta;
2430
2506
  this.backRenderer.render(this.backFace.scene, this.backFace.camera);
2431
2507
  }
2508
+ this.scheduleFirstFrame();
2432
2509
  };
2433
2510
  this.rafId = requestAnimationFrame(animate);
2434
2511
  }
@@ -3117,7 +3194,8 @@ var PocaCard = class extends EventEmitter {
3117
3194
  preventTouchScroll: options.preventTouchScroll
3118
3195
  },
3119
3196
  (error) => this.emit("error", error),
3120
- () => this.emit("ready")
3197
+ () => this.emit("ready"),
3198
+ () => this.emit("firstFrame")
3121
3199
  );
3122
3200
  this.interaction = new InteractionHandler(
3123
3201
  this.renderer.getRotatorEl(),