@displayxr/inline3d 1.49.2 → 1.49.3

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.
@@ -2565,7 +2565,7 @@ class SelfTile {
2565
2565
 
2566
2566
  /** For `call.diagnostics().self`: what the self view actually shows. */
2567
2567
  diag() {
2568
- if (!this.view) return { route: null, woven: false, reason: 'no-camera', firstWoven: null, layerRetries: 0 };
2568
+ if (!this.view) return { route: null, woven: false, reason: 'no-camera', firstWoven: null, layerRetries: 0, frameAgeMs: null };
2569
2569
  return this.view.weaveState();
2570
2570
  }
2571
2571
 
@@ -564,6 +564,8 @@ class CameraView {
564
564
  this._scratch = null;
565
565
  this._luma = null; // the latest grayscale source copy (auto-convergence and the depth cursor read it)
566
566
  this.disparityPx = null;
567
+ this._lastFrameAt = 0; // performance.now() of the last NEW camera frame (0 = none yet)
568
+ this._lastVideoTime = -1;
567
569
  // `cursor: 'depth'` (ADR-046 on a pair): validated now, its module loaded only on opt-in.
568
570
  this._cursorOpt = resolvePairCursorOption(o.cursor, '@displayxr/inline3d/camera');
569
571
  /** The depth cursor once its module has loaded; null without `cursor: 'depth'`. */
@@ -598,9 +600,28 @@ class CameraView {
598
600
  reason: this.fallbackReason,
599
601
  firstWoven: this.route === 'woven-sbs' ? (this._fw ? { ...this._fw } : 'pending') : null,
600
602
  layerRetries: this._layerFails,
603
+ frameAgeMs: this.frameAgeMs,
601
604
  };
602
605
  }
603
606
 
607
+ /**
608
+ * Milliseconds since the camera last delivered a NEW frame to this view (its video's
609
+ * `currentTime` moved), or null before the first one. A view whose track stalls (a camera taken
610
+ * by another app, a suspended capture) keeps painting its last frame; this is how a page tells.
611
+ */
612
+ get frameAgeMs() {
613
+ return this._lastFrameAt ? Math.round(performance.now() - this._lastFrameAt) : null;
614
+ }
615
+
616
+ /** Note a new camera frame (the video's clock moved since the last paint). */
617
+ _noteFrame(v) {
618
+ if ((v.readyState || 0) < 2) return;
619
+ const t = v.currentTime;
620
+ if (t === this._lastVideoTime) return;
621
+ this._lastVideoTime = t;
622
+ this._lastFrameAt = performance.now();
623
+ }
624
+
604
625
  /** The per-eye convergence shift currently painted, source px. */
605
626
  get convergencePx() {
606
627
  return this.conv.current;
@@ -658,7 +679,14 @@ class CameraView {
658
679
  }
659
680
 
660
681
  _registerWoven() {
661
- if (!this.buffer) this.buffer = document.createElement('canvas');
682
+ if (!this.buffer) {
683
+ // Empty until the camera's first frame is painted into it: the wall does not count an empty
684
+ // buffer as a stereo frame, so firstWoven cannot settle "woven" over a tile that never
685
+ // showed the camera (a track that delivers no frames).
686
+ this.buffer = document.createElement('canvas');
687
+ this.buffer.width = 0;
688
+ this.buffer.height = 0;
689
+ }
662
690
  this._paintWoven(); // the first frame exists before the layer does
663
691
  const handle = this.rectCover === undefined ? this.wall.addImage(this.canvas, this.buffer) : this.wall.addImage(this.canvas, this.buffer, { rectCover: this.rectCover });
664
692
  this.handle = handle;
@@ -737,6 +765,7 @@ class CameraView {
737
765
  if (this.removed) return;
738
766
  const v = this.cam.video;
739
767
  if (!v || this.cam.state === 'closed') return;
768
+ this._noteFrame(v);
740
769
  if (this.route === 'woven-sbs') this._paintWoven();
741
770
  else this._paintFlat(v);
742
771
  }
package/js/inline3d.js CHANGED
@@ -3345,7 +3345,10 @@ class Inline3D {
3345
3345
  this._paint(win, views);
3346
3346
  // An SBS paint with a real source behind it. Before the image has loaded (or while a
3347
3347
  // video has never had a frame) the tile holds nothing worth revealing yet.
3348
- if (win.sbs && (win.kind === 'video' ? ((win.video && win.video.readyState) || 0) >= 2 : !!win.img)) {
3348
+ // A canvas source counts once it has pixels: an empty (0x0) buffer is a page that has not
3349
+ // drawn its first frame yet (a camera view before the camera's first frame), and settling
3350
+ // firstWoven on it reported a tile woven that had never shown anything.
3351
+ if (win.sbs && (win.kind === 'video' ? ((win.video && win.video.readyState) || 0) >= 2 : sourceHasPixels(win.img))) {
3349
3352
  win.fwStereo = true;
3350
3353
  win.lfStereo = true;
3351
3354
  if (win.rw) win.rw.stereo = true;
@@ -3867,6 +3870,18 @@ function firstWovenHold(v) {
3867
3870
  return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : FIRST_WOVEN_HOLD_MS;
3868
3871
  }
3869
3872
 
3873
+ /**
3874
+ * True when an image source has something to draw: a loaded image, a sized canvas or bitmap. A
3875
+ * source that reports no size at all (a test double) keeps the old rule: present = drawable.
3876
+ */
3877
+ function sourceHasPixels(src) {
3878
+ if (!src) return false;
3879
+ const w = src.naturalWidth ?? src.videoWidth ?? src.width;
3880
+ const h = src.naturalHeight ?? src.videoHeight ?? src.height;
3881
+ if (w === undefined && h === undefined) return true;
3882
+ return w > 0 && h > 0;
3883
+ }
3884
+
3870
3885
  function loadImage(source) {
3871
3886
  if (typeof source !== 'string') return Promise.resolve(source); // element/bitmap/canvas
3872
3887
  return new Promise((resolve, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.49.2",
3
+ "version": "1.49.3",
4
4
  "description": "Turn any HTML <canvas> into a 3D window on a spatial display, inside an ordinary web page, for the DisplayXR Browser. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",