Bläddra i källkod

Fix a possible distortion of the viewport caused by concurrency issues when changing the orientation of a mobile device. Related to #13

customisations
alemart 3 månader sedan
förälder
incheckning
394e6cda95
1 ändrade filer med 111 tillägg och 51 borttagningar
  1. 111
    51
      src/core/viewport.ts

+ 111
- 51
src/core/viewport.ts Visa fil

30
 import { Resolution } from '../utils/resolution';
30
 import { Resolution } from '../utils/resolution';
31
 import { Nullable } from '../utils/utils';
31
 import { Nullable } from '../utils/utils';
32
 import { Utils } from '../utils/utils';
32
 import { Utils } from '../utils/utils';
33
-import { AREvent, AREventTarget, AREventListener } from '../utils/ar-events';
33
+import { AREvent, AREventTarget } from '../utils/ar-events';
34
 import { IllegalArgumentError, IllegalOperationError, NotSupportedError, AccessDeniedError } from '../utils/errors';
34
 import { IllegalArgumentError, IllegalOperationError, NotSupportedError, AccessDeniedError } from '../utils/errors';
35
 
35
 
36
 
36
 
106
 /** Z-index of the HUD */
106
 /** Z-index of the HUD */
107
 const HUD_ZINDEX = BASE_ZINDEX + 2;
107
 const HUD_ZINDEX = BASE_ZINDEX + 2;
108
 
108
 
109
+/** Time in ms used to throttle the resize callback of the window */
110
+const RESIZE_THROTTLE_DELAY = 100;
111
+
109
 
112
 
110
 
113
 
111
 
114
 
488
     /** the viewport to be resized */
491
     /** the viewport to be resized */
489
     private readonly _viewport: Viewport;
492
     private readonly _viewport: Viewport;
490
 
493
 
491
-    /** a helper */
492
-    private _timeout: Nullable<ReturnType<typeof setTimeout>>;
493
-
494
-    /** bound resize method */
495
-    private readonly _resize: () => void;
496
-
497
-    /** bound event trigger */
498
-    private readonly _triggerResize: () => void;
494
+    /** a timer used for throttling */
495
+    private _throttleTimer: Nullable<ReturnType<typeof setTimeout>>;
499
 
496
 
500
     /** resize strategy */
497
     /** resize strategy */
501
     private _resizeStrategy: ViewportResizeStrategy;
498
     private _resizeStrategy: ViewportResizeStrategy;
510
     constructor(viewport: Viewport)
507
     constructor(viewport: Viewport)
511
     {
508
     {
512
         this._viewport = viewport;
509
         this._viewport = viewport;
513
-        this._timeout = null;
514
-        this._resize = this._onResize.bind(this);
515
-        this._triggerResize = this.triggerResize.bind(this);
510
+        this._throttleTimer = null;
511
+        this._onViewportResize = this._onViewportResize.bind(this);
512
+        this._onWindowResize = this._onWindowResize.bind(this);
513
+        this._onOrientationChange = this._onOrientationChange.bind(this);
516
         this._resizeStrategy = new InlineResizeStrategy();
514
         this._resizeStrategy = new InlineResizeStrategy();
517
 
515
 
518
         // initial setup
516
         // initial setup
519
         // (the size is yet unknown)
517
         // (the size is yet unknown)
520
-        this._viewport.addEventListener('resize', this._resize);
521
-        this.triggerResize(0);
518
+        this._viewport.addEventListener('resize', this._onViewportResize);
519
+        this._triggerResizeEvent();
522
     }
520
     }
523
 
521
 
524
     /**
522
     /**
528
     {
526
     {
529
         // Configure the resize listener. We want the viewport to adjust itself
527
         // Configure the resize listener. We want the viewport to adjust itself
530
         // if the phone/screen is resized or changes orientation
528
         // if the phone/screen is resized or changes orientation
531
-        window.addEventListener('resize', this._triggerResize); // a delay is welcome
529
+        window.addEventListener('resize', this._onWindowResize);
532
 
530
 
533
         // handle changes of orientation
531
         // handle changes of orientation
534
-        // (is this needed? we already listen to resize events)
535
-        if(screen.orientation !== undefined)
536
-            screen.orientation.addEventListener('change', this._triggerResize);
532
+        if(typeof screen.orientation === 'object')
533
+            screen.orientation.addEventListener('change', this._onOrientationChange);
537
         else
534
         else
538
-            window.addEventListener('orientationchange', this._triggerResize); // deprecated
535
+            window.addEventListener('orientationchange', this._onOrientationChange); // deprecated
539
 
536
 
540
         // trigger a resize to setup the sizes / the CSS
537
         // trigger a resize to setup the sizes / the CSS
541
-        this.triggerResize(0);
538
+        this._triggerResizeEvent();
542
     }
539
     }
543
 
540
 
544
     /**
541
     /**
546
      */
543
      */
547
     release(): void
544
     release(): void
548
     {
545
     {
549
-        if(screen.orientation !== undefined)
550
-            screen.orientation.removeEventListener('change', this._triggerResize);
546
+        if(typeof screen.orientation === 'object')
547
+            screen.orientation.removeEventListener('change', this._onOrientationChange);
551
         else
548
         else
552
-            window.removeEventListener('orientationchange', this._triggerResize);
549
+            window.removeEventListener('orientationchange', this._onOrientationChange);
553
 
550
 
554
-        window.removeEventListener('resize', this._triggerResize);
551
+        window.removeEventListener('resize', this._onWindowResize);
555
 
552
 
556
-        this._viewport.removeEventListener('resize', this._resize);
553
+        this._viewport.removeEventListener('resize', this._onViewportResize);
557
         this._resizeStrategy.clear(this._viewport);
554
         this._resizeStrategy.clear(this._viewport);
558
     }
555
     }
559
 
556
 
560
     /**
557
     /**
561
-     * Trigger a resize event after a delay
562
-     * @param delay in milliseconds
563
-     */
564
-    triggerResize(delay: number = 100): void
565
-    {
566
-        const event = new ViewportEvent('resize');
567
-
568
-        if(delay <= 0) {
569
-            this._viewport.dispatchEvent(event);
570
-            return;
571
-        }
572
-
573
-        if(this._timeout !== null)
574
-            clearTimeout(this._timeout);
575
-
576
-        this._timeout = setTimeout(() => {
577
-            this._timeout = null;
578
-            this._viewport.dispatchEvent(event);
579
-        }, delay);
580
-    }
581
-
582
-    /**
583
      * Change the resize strategy
558
      * Change the resize strategy
584
      * @param strategy new strategy
559
      * @param strategy new strategy
585
      */
560
      */
587
     {
562
     {
588
         this._resizeStrategy.clear(this._viewport);
563
         this._resizeStrategy.clear(this._viewport);
589
         this._resizeStrategy = strategy;
564
         this._resizeStrategy = strategy;
590
-        this.triggerResize(0);
565
+        this._triggerResizeEvent();
591
     }
566
     }
592
 
567
 
593
     /**
568
     /**
615
     }
590
     }
616
 
591
 
617
     /**
592
     /**
618
-     * Resize callback
593
+     * Trigger a resize event
594
+     */
595
+    private _triggerResizeEvent(): void
596
+    {
597
+        const event = new ViewportEvent('resize');
598
+        this._viewport.dispatchEvent(event);
599
+    }
600
+
601
+    /**
602
+     * Called when the window receives a 'resize' event
603
+     */
604
+    private _onWindowResize(): void
605
+    {
606
+        // throttle the resize callback
607
+        if(this._throttleTimer !== null)
608
+            clearTimeout(this._throttleTimer);
609
+
610
+        this._throttleTimer = setTimeout(() => {
611
+            this._throttleTimer = null;
612
+            this._triggerResizeEvent();
613
+        }, RESIZE_THROTTLE_DELAY);
614
+    }
615
+
616
+    /**
617
+     * Called when a change of screen orientation is detected
618
+     */
619
+    private async _onOrientationChange(): Promise<void>
620
+    {
621
+        /*
622
+
623
+        When changing the orientation of a mobile device, sometimes there will
624
+        be a mismatch between the viewport and the screen: one will be in
625
+        landscape mode and the other in portrait mode.
626
+
627
+        Ultimately, the size of the viewport is dependent on the size of the
628
+        underlying media (typically a camera feed). When the device is rotated,
629
+        the browser should rotate the camera feed automatically*. In addition,
630
+        it will trigger a resize event on the window, which will, in time,
631
+        resize the viewport. If the order of events is such that the viewport
632
+        is resized before the camera feed is rotated, then we will observe a
633
+        distorted video.
634
+
635
+        Let's correct the issue by first detecting the situation and then by
636
+        triggering a new resize event.
637
+
638
+        (*) at the time of this writing, when testing on Android devices, I see
639
+            that Chrome does it. Firefox 139 does it in one device but doesn't
640
+            do it in another (it's a browser bug - see below). Safari/iOS does
641
+            it (tested on the cloud).
642
+
643
+            https://bugzilla.mozilla.org/show_bug.cgi?id=1802849
644
+
645
+        */
646
+
647
+        const MAX_ATTEMPTS = 3;
648
+        const canvas = this._viewport._backgroundCanvas;
649
+
650
+        for(let i = 0; i < MAX_ATTEMPTS; i++) {
651
+
652
+            await Utils.wait(RESIZE_THROTTLE_DELAY);
653
+
654
+            // After one delay, this._viewport.aspectRatio will likely be
655
+            // correct, but the canvas will not reflect that if, instants ago,
656
+            // it was resized using the previous aspect ratio of the media.
657
+            const a = canvas.width / canvas.height;
658
+            const b = screen.width / screen.height;
659
+
660
+            // if there is a mismatch between the aspect ratios, then the last
661
+            // resize of the viewport took place with the previous aspect ratio
662
+            // of the media. This means that canvas is now incorrectly sized,
663
+            // and that the previous viewport resize event was ineffective.
664
+            if((a-1) * (b-1) < 0) { //if((a < 1 && b > 1) || (a > 1 && b < 1))
665
+                this._triggerResizeEvent();
666
+                break;
667
+            }
668
+
669
+            // Note: when using a canvas or a video file as a source of data,
670
+            // its aspect ratio is not expected to change. We will trigger an
671
+            // additional resize event on mobile in this case. Unlikely to be
672
+            // an issue.
673
+
674
+        }
675
+    }
676
+
677
+    /**
678
+     * Called when the viewport receives a 'resize' event
619
      */
679
      */
620
-    private _onResize(): void
680
+    private _onViewportResize(): void
621
     {
681
     {
622
         const viewport = this._viewport;
682
         const viewport = this._viewport;
623
 
683
 
760
 
820
 
761
 /**
821
 /**
762
  * Immersive viewport with stretch style: it occupies the entire page and
822
  * Immersive viewport with stretch style: it occupies the entire page and
763
- * fully stretches the media
823
+ * fully stretches the media, possibly distorting it
764
  */
824
  */
765
 class StretchResizeStrategy extends ImmersiveResizeStrategy
825
 class StretchResizeStrategy extends ImmersiveResizeStrategy
766
 {
826
 {

Laddar…
Avbryt
Spara