@sangwonl/pocato-core 0.4.12 → 0.4.14

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
@@ -368,7 +368,6 @@ function bootstrapShaders() {
368
368
  import * as THREE2 from "three";
369
369
  var VIDEO_EXTENSIONS = /\.(mp4|webm|mov|ogg)(\?|$)/i;
370
370
  var VIDEO_LOAD_TIMEOUT_MS = 15e3;
371
- var VIDEO_FRAME_CAPTURE_TIMEOUT_MS = 750;
372
371
  function isVideoSource(layer) {
373
372
  if (layer.type) return layer.type === "video";
374
373
  return VIDEO_EXTENSIONS.test(layer.src);
@@ -459,9 +458,13 @@ function loadVideoTexture(layer, options) {
459
458
  const video = document.createElement("video");
460
459
  video.crossOrigin = "anonymous";
461
460
  video.muted = true;
461
+ video.autoplay = true;
462
462
  video.loop = layer.loop ?? true;
463
463
  applyVideoPlaybackOptions(video, layer);
464
464
  video.playsInline = true;
465
+ video.setAttribute("muted", "");
466
+ video.setAttribute("playsinline", "");
467
+ video.setAttribute("webkit-playsinline", "");
465
468
  video.preload = "auto";
466
469
  video.style.position = "fixed";
467
470
  video.style.width = "1px";
@@ -503,7 +506,6 @@ function loadVideoTexture(layer, options) {
503
506
  }, { once: true });
504
507
  const resolveWithFrame = () => {
505
508
  if (settled) return;
506
- video.pause();
507
509
  const canvas = document.createElement("canvas");
508
510
  canvas.width = video.videoWidth || 1;
509
511
  canvas.height = video.videoHeight || 1;
@@ -523,49 +525,29 @@ function loadVideoTexture(layer, options) {
523
525
  }
524
526
  }
525
527
  const liveTexture = new THREE2.VideoTexture(video);
528
+ const play = () => {
529
+ const playPromise = video.play();
530
+ playPromise.catch((error) => {
531
+ const message = error instanceof Error ? error.message : String(error);
532
+ debugLog("video play rejected", { src: layer.src, error: message });
533
+ onError?.(new Error(`Video autoplay blocked: ${layer.src} (${message})`));
534
+ });
535
+ };
536
+ play();
526
537
  finish({
527
538
  texture: posterTexture,
528
539
  liveTexture,
529
540
  videoEl: video,
530
- play: () => {
531
- video.play().catch(() => {
532
- onError?.(new Error(`Video autoplay blocked: ${layer.src}`));
533
- });
534
- }
541
+ play
535
542
  });
536
543
  };
537
544
  video.addEventListener("loadeddata", () => {
538
- waitForDecodedVideoFrame(video, signal).then(() => {
539
- resolveWithFrame();
540
- });
545
+ resolveWithFrame();
541
546
  }, { once: true });
542
547
  video.src = layer.src;
543
548
  video.load();
544
549
  });
545
550
  }
546
- function waitForDecodedVideoFrame(video, signal) {
547
- return new Promise((resolve) => {
548
- if (signal?.aborted || !("requestVideoFrameCallback" in video)) {
549
- resolve();
550
- return;
551
- }
552
- let settled = false;
553
- let frameId = null;
554
- const finish = () => {
555
- if (settled) return;
556
- settled = true;
557
- globalThis.clearTimeout(timeoutId);
558
- if (frameId != null && "cancelVideoFrameCallback" in video) {
559
- video.cancelVideoFrameCallback(frameId);
560
- }
561
- signal?.removeEventListener("abort", finish);
562
- resolve();
563
- };
564
- const timeoutId = globalThis.setTimeout(finish, VIDEO_FRAME_CAPTURE_TIMEOUT_MS);
565
- signal?.addEventListener("abort", finish, { once: true });
566
- frameId = video.requestVideoFrameCallback(finish);
567
- });
568
- }
569
551
  function loadFrozenVideoTexture(layer, freezeTime, options) {
570
552
  return new Promise((resolve) => {
571
553
  const { onError, signal } = options;
@@ -2056,7 +2038,18 @@ var FRAG_SHADERS = {
2056
2038
  lenticular: lenticular_frag_default
2057
2039
  };
2058
2040
  var MAX_LAYERS = 8;
2059
- var VIDEO_FRAME_WAIT_TIMEOUT_MS = 750;
2041
+ function waitForDecodedFrame(video) {
2042
+ if (!video || !("requestVideoFrameCallback" in video)) {
2043
+ return Promise.resolve();
2044
+ }
2045
+ return new Promise((resolve) => {
2046
+ const timeout = globalThis.setTimeout(resolve, 750);
2047
+ video.requestVideoFrameCallback(() => {
2048
+ globalThis.clearTimeout(timeout);
2049
+ resolve();
2050
+ });
2051
+ });
2052
+ }
2060
2053
  var FaceRenderer = class {
2061
2054
  constructor(shader, width, height, effectUniforms) {
2062
2055
  this.loadedLayers = [];
@@ -2158,24 +2151,17 @@ var FaceRenderer = class {
2158
2151
  }
2159
2152
  async playLoadedVideos() {
2160
2153
  const activationVersion = ++this.videoActivationVersion;
2161
- this.videoActivationAbort?.abort();
2162
- const activationAbort = new AbortController();
2163
- this.videoActivationAbort = activationAbort;
2164
- await Promise.all(this.loadedLayers.map(async (loaded, i) => {
2165
- if (this.destroyed || activationAbort.signal.aborted || activationVersion !== this.videoActivationVersion) return;
2154
+ const tasks2 = this.loadedLayers.map(async (loaded, i) => {
2155
+ if (this.destroyed || activationVersion !== this.videoActivationVersion) return;
2166
2156
  if (!loaded.liveTexture) {
2167
2157
  loaded.play?.();
2168
2158
  return;
2169
2159
  }
2160
+ this.material.uniforms[`uLayer${i}`].value = loaded.liveTexture;
2170
2161
  loaded.play?.();
2171
- const hasFrame = await waitForVideoFrame(loaded.videoEl, activationAbort.signal);
2172
- if (hasFrame && !this.destroyed && !activationAbort.signal.aborted && activationVersion === this.videoActivationVersion) {
2173
- this.material.uniforms[`uLayer${i}`].value = loaded.liveTexture;
2174
- }
2175
- }));
2176
- if (this.videoActivationAbort === activationAbort) {
2177
- this.videoActivationAbort = null;
2178
- }
2162
+ await waitForDecodedFrame(loaded.videoEl);
2163
+ });
2164
+ await Promise.all(tasks2);
2179
2165
  }
2180
2166
  updateShader(fragmentShader) {
2181
2167
  this.material.fragmentShader = resolveIncludes(fragmentShader);
@@ -2205,39 +2191,6 @@ var FaceRenderer = class {
2205
2191
  this.material.dispose();
2206
2192
  }
2207
2193
  };
2208
- function waitForVideoFrame(video, signal) {
2209
- return new Promise((resolve) => {
2210
- if (signal?.aborted) {
2211
- resolve(false);
2212
- return;
2213
- }
2214
- let settled = false;
2215
- let timeoutId = null;
2216
- let videoFrameId = null;
2217
- const finish = (hasFrame) => {
2218
- if (settled) return;
2219
- settled = true;
2220
- if (timeoutId != null) globalThis.clearTimeout(timeoutId);
2221
- if (video && videoFrameId != null && "cancelVideoFrameCallback" in video) {
2222
- video.cancelVideoFrameCallback(videoFrameId);
2223
- }
2224
- signal?.removeEventListener("abort", abort);
2225
- resolve(hasFrame);
2226
- };
2227
- const abort = () => finish(false);
2228
- if (video && "requestVideoFrameCallback" in video) {
2229
- timeoutId = globalThis.setTimeout(() => finish(false), VIDEO_FRAME_WAIT_TIMEOUT_MS);
2230
- signal?.addEventListener("abort", abort, { once: true });
2231
- videoFrameId = video.requestVideoFrameCallback(() => finish(true));
2232
- return;
2233
- }
2234
- const rafId2 = requestAnimationFrame(() => finish(true));
2235
- signal?.addEventListener("abort", () => {
2236
- cancelAnimationFrame(rafId2);
2237
- finish(false);
2238
- }, { once: true });
2239
- });
2240
- }
2241
2194
  function toThreeUniformValue(value) {
2242
2195
  if (typeof value === "string") {
2243
2196
  return value.startsWith("#") ? new THREE3.Color(value) : value;
@@ -2251,6 +2204,7 @@ function toThreeUniformValue(value) {
2251
2204
  }
2252
2205
 
2253
2206
  // src/renderer/index.ts
2207
+ var READY_REVEAL_TRANSITION_MS = 500;
2254
2208
  var Renderer = class {
2255
2209
  constructor(container, options, onError, onReady, onFirstFrame) {
2256
2210
  this.container = container;
@@ -2273,6 +2227,7 @@ var Renderer = class {
2273
2227
  this.firstFrameScheduled = false;
2274
2228
  this.mediaActivated = false;
2275
2229
  this.mediaActivationPromise = null;
2230
+ this.readyRevealTimeoutId = null;
2276
2231
  if (!options.front?.layers?.length) {
2277
2232
  console.warn("[pocato] front.layers must have at least 1 element");
2278
2233
  }
@@ -2432,6 +2387,19 @@ var Renderer = class {
2432
2387
  this.layersReady = true;
2433
2388
  this.onReady?.();
2434
2389
  }
2390
+ resetReadyState() {
2391
+ this.layersReady = false;
2392
+ this.firstFrameEmitted = false;
2393
+ this.firstFrameScheduled = false;
2394
+ this.mediaActivated = false;
2395
+ this.mediaActivationPromise = null;
2396
+ if (this.readyRevealTimeoutId != null) {
2397
+ window.clearTimeout(this.readyRevealTimeoutId);
2398
+ this.readyRevealTimeoutId = null;
2399
+ }
2400
+ this.cardEl.classList.add("pocato-loading");
2401
+ this.cardEl.classList.remove("pocato-ready");
2402
+ }
2435
2403
  scheduleFirstFrame() {
2436
2404
  if (!this.layersReady || this.firstFrameEmitted || this.firstFrameScheduled) return;
2437
2405
  if (!this.mediaActivated) {
@@ -2447,10 +2415,14 @@ var Renderer = class {
2447
2415
  if (this.mediaActivationPromise) return;
2448
2416
  requestAnimationFrame(() => {
2449
2417
  if (this.destroyed || this.firstFrameEmitted) return;
2450
- this.firstFrameEmitted = true;
2451
2418
  this.cardEl.classList.remove("pocato-loading");
2452
2419
  this.cardEl.classList.add("pocato-ready");
2453
- this.onFirstFrame?.();
2420
+ this.readyRevealTimeoutId = window.setTimeout(() => {
2421
+ this.readyRevealTimeoutId = null;
2422
+ if (this.destroyed || this.firstFrameEmitted) return;
2423
+ this.firstFrameEmitted = true;
2424
+ this.onFirstFrame?.();
2425
+ }, READY_REVEAL_TRANSITION_MS);
2454
2426
  });
2455
2427
  this.firstFrameScheduled = true;
2456
2428
  }
@@ -2692,6 +2664,7 @@ var Renderer = class {
2692
2664
  face.updateResolution(canvas.width, canvas.height);
2693
2665
  }
2694
2666
  async updateLayers(frontLayers, backLayers) {
2667
+ this.resetReadyState();
2695
2668
  const promises = [];
2696
2669
  if (frontLayers) {
2697
2670
  promises.push(this.frontFace.loadLayers(frontLayers, this.onError));
@@ -2700,6 +2673,9 @@ var Renderer = class {
2700
2673
  promises.push(this.backFace.loadLayers(backLayers, this.onError));
2701
2674
  }
2702
2675
  await Promise.all(promises);
2676
+ if (this.destroyed) return;
2677
+ this.layersReady = true;
2678
+ this.onReady?.();
2703
2679
  }
2704
2680
  getRotatorEl() {
2705
2681
  return this.rotatorEl;
@@ -2713,6 +2689,10 @@ var Renderer = class {
2713
2689
  destroy() {
2714
2690
  this.destroyed = true;
2715
2691
  this.stopRenderLoop();
2692
+ if (this.readyRevealTimeoutId != null) {
2693
+ window.clearTimeout(this.readyRevealTimeoutId);
2694
+ this.readyRevealTimeoutId = null;
2695
+ }
2716
2696
  this.resizeObserver?.disconnect();
2717
2697
  this.intersectionObserver?.disconnect();
2718
2698
  this.frontFace.destroy();