Bug 1373537 - Remove calculations for clientWidth and instead use predetermined widths for controls, since in practice they don't change anyways. r=jaws draft
authorRay Lin <ralin@mozilla.com>
Fri, 23 Jun 2017 11:47:56 +0800
changeset 602141 758e494d654ade03aac9131c63f1d9baaab42fa8
parent 602051 f3483af8ecf997453064201c49c48a682c7f3c29
child 602142 d98326879d4f55ce45b1fb0d35f37d55628c303b
push id66295
push userbmo:ralin@mozilla.com
push dateThu, 29 Jun 2017 18:24:09 +0000
reviewersjaws
bugs1373537
milestone56.0a1
Bug 1373537 - Remove calculations for clientWidth and instead use predetermined widths for controls, since in practice they don't change anyways. r=jaws MozReview-Commit-ID: 442q7e8dKyZ
toolkit/content/widgets/videocontrols.xml
toolkit/themes/shared/media/videocontrols.css
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -362,50 +362,51 @@
         // Set the current status icon.
         if (this.hasError()) {
           this.clickToPlay.hidden = true;
           this.statusIcon.setAttribute("type", "error");
           this.updateErrorText();
           this.setupStatusFader(true);
         }
 
-        // An event handler for |onresize| should be added when bug 227495 is fixed.
-        this.controlBar.hidden = false;
+        let adjustableControls = [
+          ...this.prioritizedControls,
+          this.controlBar,
+          this.clickToPlay
+        ];
 
-        for (let control of this.layoutControls) {
+        for (let control of adjustableControls) {
           if (!control) {
             break;
           }
 
           Object.defineProperties(control, {
+            // We should directly access CSSOM to get pre-defined style instead of
+            // retrieving computed dimensions from layout.
             minWidth: {
-              value: control.clientWidth,
-              writable: true
+              get: () => {
+                let controlAnonId = control.getAttribute("anonid");
+                let propertyName = `--${controlAnonId}-width`;
+                if (control.modifier) {
+                  propertyName += "-" + control.modifier;
+                }
+                let preDefinedSize = this.controlBarComputedStyles.getPropertyValue(propertyName);
+
+                return parseInt(preDefinedSize, 10);
+              }
             },
             isAdjustableControl: {
               value: true
             },
-            isWanted: {
-              value: true,
-              writable: true
-            },
-            resized: {
-              value: false,
+            modifier: {
+              value: "",
               writable: true
             },
-            resizedHandler: {
-              value: () => {
-                let width = control.clientWidth;
-
-                if (width === 0) {
-                  return;
-                }
-
-                control.minWidth = width;
-              },
+            isWanted: {
+              value: true,
               writable: true
             },
             hideByAdjustment: {
               set: (v) => {
                 if (v) {
                   control.setAttribute("hidden", "true");
                 } else {
                   control.removeAttribute("hidden");
@@ -416,34 +417,17 @@
               get: () => control._isHiddenByAdjustment
             },
             _isHiddenByAdjustment: {
               value: false,
               writable: true
             }
           });
         }
-        // Cannot get minimal width of flexible scrubber and clickToPlay.
-        // Rewrite to empirical value for now.
-        this.scrubberStack.minWidth = 64;
-        this.volumeStack.minWidth = 48;
-        this.clickToPlay.minWidth = 48;
-
-        if (this.positionDurationBox) {
-          this.positionDurationBox.resizedHandler = () => {
-            let durationWidth = this.durationSpan.hideByAdjustment ? 0 : this.durationSpan.clientWidth;
-
-            this.positionDurationBox.minWidth = this.positionDurationBox.clientWidth - durationWidth;
-          };
-
-          this.positionDurationBox.resized = true;
-        }
-
         this.adjustControlSize();
-        this.controlBar.hidden = true;
 
         // Can only update the volume controls once we've computed
         // _volumeControlWidth, since the volume slider implementation
         // depends on it.
         this.updateVolumeControls();
       },
 
       setupNewLoadState() {
@@ -589,20 +573,16 @@
             if (this.video instanceof HTMLVideoElement &&
                 (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
               this.isAudioOnly = true;
               this.clickToPlay.hidden = true;
               this.startFadeIn(this.controlBar);
               this.setFullscreenButtonState();
             }
             this.showPosition(Math.round(this.video.currentTime * 1000), Math.round(this.video.duration * 1000));
-            if (this.positionDurationBox) {
-              this.positionDurationBox.resized = true;
-              this.durationSpan.resized = true;
-            }
             if (!this.isAudioOnly && !this.video.mozHasAudio) {
               this.muteButton.setAttribute("noAudio", "true");
               this.muteButton.setAttribute("disabled", "true");
             }
             this.adjustControlSize();
             break;
           case "loadeddata":
             this.firstFrameShown = true;
@@ -833,16 +813,17 @@
         const positionTextNode = Array.prototype.find.call(
           this.positionDurationBox.childNodes, (n) => !!~n.textContent.search("#1"));
         const durationSpan = this.durationSpan;
         const durationFormat = durationSpan.textContent;
         const positionFormat = positionTextNode.textContent;
 
         durationSpan.classList.add("duration");
         durationSpan.setAttribute("role", "none");
+        durationSpan.setAttribute("anonid", "durationSpan");
 
         Object.defineProperties(this.positionDurationBox, {
           durationSpan: {
             value: durationSpan
           },
           position: {
             set: (v) => {
               positionTextNode.textContent = positionFormat.replace("#1", v);
@@ -859,31 +840,36 @@
       showDuration(duration) {
         let isInfinite = (duration == Infinity);
         this.log("Duration is " + duration + "ms.\n");
 
         if (isNaN(duration) || isInfinite) {
           duration = this.maxCurrentTimeSeen;
         }
 
+        // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
+        this.showHours = (duration >= 3600000);
+
         // Format the duration as "h:mm:ss" or "m:ss"
         let timeString = isInfinite ? "" : this.formatTime(duration);
         if (this.videocontrols.isTouchControls) {
           this.durationLabel.setAttribute("value", timeString);
         } else {
           this.positionDurationBox.duration = timeString;
+
+          if (this.showHours) {
+            this.positionDurationBox.modifier = "long";
+            this.durationSpan.modifier = "long";
+          }
         }
 
         // "durationValue" property is used by scale binding to
         // generate accessible name.
         this.scrubber.durationValue = timeString;
 
-        // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
-        this.showHours = (duration >= 3600000);
-
         this.scrubber.max = duration;
         // XXX Can't set increment here, due to bug 473103. Also, doing so causes
         // snapping when dragging with the mouse, so we can't just set a value for
         // the arrow-keys.
         this.scrubber.pageIncrement = Math.round(duration / 10);
       },
 
       pauseVideoDuringDragging() {
@@ -1172,36 +1158,16 @@
         }
 
         if (this.videocontrols.isTouchControls) {
           this.scrubber.dragStateChanged(false);
         }
         element.hidden = true;
       },
 
-      onVideoControlsResized() {
-        // Do not adjust again if resize event is caused by adjustControls(). Audio
-        // might be resized by adjustControls(), so we should skip this handler if
-        // current size is the same as the adjusted size.
-        let {width, height} = this.video.getBoundingClientRect();
-        if (width === this.adjustedVideoSize.width && height === this.adjustedVideoSize.height) {
-          return;
-        }
-
-        // For the controls which haven't got correct computed size yet, force
-        // them to re-cache their minWidth when the media is resized (reflow).
-        this.layoutControls.forEach(control => {
-          if (control) {
-            control.resized = control.resized || (control.minWidth === 0);
-          }
-        });
-
-        this.adjustControlSize();
-      },
-
       _triggeredByControls: false,
 
       startPlay() {
         this._triggeredByControls = true;
         this.hideClickToPlay();
         this.video.play();
       },
 
@@ -1490,22 +1456,16 @@
             return true;
           }
         }
 
         return false;
       },
 
       setClosedCaptionButtonState() {
-        this.adjustControlSize();
-
-        if (!this.isClosedCaptionAvailable) {
-          return;
-        }
-
         if (this.isClosedCaptionOn()) {
           this.closedCaptionButton.setAttribute("enabled", "true");
         } else {
           this.closedCaptionButton.removeAttribute("enabled");
         }
 
         let ttItems = this.textTrackList.childNodes;
 
@@ -1513,16 +1473,18 @@
           const idx = +tti.getAttribute("index");
 
           if (idx == this.currentTextTrackIndex) {
             tti.setAttribute("on", "true");
           } else {
             tti.removeAttribute("on");
           }
         }
+
+        this.adjustControlSize();
       },
 
       addNewTextTrack(tt) {
         if (!this.isSupportedTextTrack(tt)) {
           return;
         }
 
         if (tt.index && tt.index < this.textTracksCount) {
@@ -1661,119 +1623,99 @@
 
       get isTopLevelSyntheticDocument() {
         let doc = this.video.ownerDocument;
         let win = doc.defaultView;
         return doc.mozSyntheticDocument && win === win.top;
       },
 
       controlBarMinHeight: 40,
-      adjustedVideoSize: {},
+      controlBarMinVisibleHeight: 28,
       adjustControlSize() {
         if (this.videocontrols.isTouchControls) {
           return;
         }
 
-        let controlHidden = this.isControlBarHidden;
-
-        if (this.layoutControls.some(control => control.resized)) {
-          this.controlBar.hidden = false;
-
-          for (let control of this.layoutControls) {
-            if (control.resized && !control.hideByAdjustment) {
-              control.resizedHandler();
-              control.resized = false;
-            }
-          }
-
-          this.controlBar.hidden = controlHidden;
-        }
-
-        // Check minWidth of controlBar before adjusting. If the layout information
-        // isn't ready yet, the minWidth of controlBar would be undefined or 0, so early
-        // return to avoid invalid adjustment.
-        if (!this.controlBar.minWidth) {
-          return;
-        }
-
-        let videoWidth = this.video.clientWidth;
-        let videoHeight = this.video.clientHeight;
         const minControlBarPaddingWidth = 18;
 
-        // Hide and show control in order.
-        const prioritizedControls = [
-          this.playButton,
-          this.muteButton,
-          this.fullscreenButton,
-          this.closedCaptionButton,
-          this.positionDurationBox,
-          this.scrubberStack,
-          this.durationSpan,
-          this.volumeStack
-        ];
-
         this.fullscreenButton.isWanted = !this.controlBar.hasAttribute("fullscreen-unavailable");
         this.closedCaptionButton.isWanted = this.isClosedCaptionAvailable;
         this.volumeStack.isWanted = !this.muteButton.hasAttribute("noAudio");
 
+        let minRequiredWidth = this.prioritizedControls
+          .filter(control => control && control.isWanted)
+          .reduce((accWidth, cc) => accWidth + cc.minWidth, minControlBarPaddingWidth);
+        // Skip the adjustment in case the stylesheets haven't been loaded yet.
+        if (!minRequiredWidth) {
+          return;
+        }
+
+        let givenHeight = this.video.clientHeight;
+        let videoWidth = this.video.clientWidth || minRequiredWidth;
+        let videoHeight = this.isAudioOnly ? this.controlBarMinHeight : givenHeight;
+
         let widthUsed = minControlBarPaddingWidth;
         let preventAppendControl = false;
 
-        for (let control of prioritizedControls) {
+        for (let control of this.prioritizedControls) {
           if (!control.isWanted) {
             control.hideByAdjustment = true;
             continue;
           }
 
           control.hideByAdjustment = preventAppendControl ||
           widthUsed + control.minWidth > videoWidth;
 
           if (control.hideByAdjustment) {
             preventAppendControl = true;
           } else {
             widthUsed += control.minWidth;
           }
         }
 
-        if (this.durationSpan.hideByAdjustment) {
-          this.positionDurationBox.resized = true;
+        // Use flexible spacer to separate controls when scrubber is hidden.
+        // As long as muteButton hidden, which means only play button presents,
+        // hide spacer and make playButton centered.
+        this.controlBarSpacer.hidden = !this.scrubberStack.hidden || this.muteButton.hidden;
+
+        // Since the size of videocontrols is expanded with controlBar in <audio>, we
+        // should fix the dimensions in order not to recursively trigger reflow afterwards.
+        if (this.isAudioOnly) {
+          if (givenHeight) {
+            this.controlBar.style.height = `${Math.max(givenHeight, this.controlBarMinVisibleHeight)}px`;
+          }
+          this.controlBar.style.width = `${videoWidth - minControlBarPaddingWidth}px`;
+          return;
         }
 
         if (videoHeight < this.controlBarMinHeight ||
             widthUsed === minControlBarPaddingWidth) {
           this.controlBar.setAttribute("size", "hidden");
           this.controlBar.hideByAdjustment = true;
         } else {
           this.controlBar.removeAttribute("size");
           this.controlBar.hideByAdjustment = false;
         }
 
-        // Use flexible spacer to separate controls when scrubber is hidden.
-        // As long as muteButton hidden, which means only play button presents,
-        // hide spacer and make playButton centered.
-        this.controlBarSpacer.hidden = !this.scrubberStack.hidden || this.muteButton.hidden;
-
         // Adjust clickToPlayButton size.
         const minVideoSideLength = Math.min(videoWidth, videoHeight);
         const clickToPlayViewRatio = 0.15;
         const clickToPlayScaledSize = Math.max(
         this.clickToPlay.minWidth, minVideoSideLength * clickToPlayViewRatio);
 
         if (clickToPlayScaledSize >= videoWidth ||
            (clickToPlayScaledSize + this.controlBarMinHeight / 2 >= videoHeight / 2 )) {
           this.clickToPlay.hideByAdjustment = true;
         } else {
           if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
             this.clickToPlay.hideByAdjustment = false;
           }
           this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
           this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
         }
-        // Record new size after adjustment
-        this.adjustedVideoSize = this.video.getBoundingClientRect();
       },
 
       init(binding) {
         this.video = binding.parentNode;
         this.videocontrols = binding;
 
         this.controlsContainer    = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
         this.statusIcon    = document.getAnonymousElementByAttribute(binding, "anonid", "statusIcon");
@@ -1797,24 +1739,30 @@
         this.fullscreenButton   = document.getAnonymousElementByAttribute(binding, "anonid", "fullscreenButton");
         this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "anonid", "closedCaptionButton");
         this.textTrackList = document.getAnonymousElementByAttribute(binding, "anonid", "textTrackList");
 
         if (this.positionDurationBox) {
           this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
         }
 
-        this.layoutControls = [
-          ...this.controlBar.children,
+        this.controlBarComputedStyles = getComputedStyle(this.controlBar);
+
+        // Hide and show control in certain order.
+        this.prioritizedControls = [
+          this.playButton,
+          this.muteButton,
+          this.fullscreenButton,
+          this.closedCaptionButton,
+          this.positionDurationBox,
+          this.scrubberStack,
           this.durationSpan,
-          this.controlBar,
-          this.clickToPlay
+          this.volumeStack
         ];
 
-
         // XXX controlsContainer is a desktop only element. To determine whether
         // isTouchControls or not during the whole initialization process, get
         // this state overridden here.
         this.videocontrols.isTouchControls = !this.controlsContainer;
         this.isAudioOnly = (this.video instanceof HTMLAudioElement);
         this.setupInitialState();
         this.setupNewLoadState();
         this.initTextTracks();
@@ -1846,17 +1794,17 @@
         addListener(this.muteButton, "click", this.toggleMute);
         addListener(this.closedCaptionButton, "click", this.toggleClosedCaption);
         addListener(this.fullscreenButton, "click", this.toggleFullscreen);
         addListener(this.playButton, "click", this.clickToPlayClickHandler);
         addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
         addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
         addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
 
-        addListener(this.videocontrols, "resizevideocontrols", this.onVideoControlsResized);
+        addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
         addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
         addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
         addListener(this.controlBar, "transitionend", this.onControlBarTransitioned);
         addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
         addListener(this.video, "keypress", this.keyHandler, true);
 
         addListener(this.videocontrols, "dragstart", function(event) {
           event.preventDefault(); // prevent dragging of controls image (bug 517114)
--- a/toolkit/themes/shared/media/videocontrols.css
+++ b/toolkit/themes/shared/media/videocontrols.css
@@ -6,16 +6,34 @@
 @namespace url("http://www.w3.org/1999/xhtml");
 
 video > xul|videocontrols,
 audio > xul|videocontrols {
   writing-mode: horizontal-tb;
   width: 100%;
   height: 100%;
   display: inline-block;
+  overflow: hidden;
+}
+
+.controlBar {
+  --clickToPlay-width: 48px;
+  --clickToPlay-height: var(--clickToPlay-width);
+  --playButton-width: 30px;
+  --playButton-height: var(--playButton-width);
+  --scrubberStack-width: 64px;
+  --muteButton-width: 30px;
+  --volumeStack-width: 48px;
+  --closedCaptionButton-width: 30px;
+  --fullscreenButton-width: 30px;
+
+  --positionDurationBox-width: 40px;
+  --durationSpan-width: 40px;
+  --positionDurationBox-width-long: 60px;
+  --durationSpan-width-long: 60px;
 }
 
 .controlsContainer [hidden="true"],
 .controlBar[hidden] {
   display: none;
 }
 
 .controlBar[size="hidden"] {
@@ -78,18 +96,18 @@ audio > xul|videocontrols {
   background-color: rgba(26,26,26,.8);
 }
 
 .playButton,
 .muteButton,
 .closedCaptionButton,
 .fullscreenButton {
   height: 100%;
-  min-height: 30px;
-  min-width: 30px;
+  min-width: var(--playButton-width);
+  min-height: var(--playButton-height);
   padding: 6px;
   border: 0;
   margin: 0;
   background-color: transparent;
   background-repeat: no-repeat;
   background-position: center;
   background-origin: content-box;
   background-clip: content-box;
@@ -176,26 +194,27 @@ audio > xul|videocontrols {
 
 .progressStack {
   position: relative;
   width: 100%;
   height: 5px;
 }
 
 .scrubberStack {
-  min-width: 48px;
-  flex-basis: 48px;
+  /* minus margin to get basis of required width */
+  min-width: calc(var(--scrubberStack-width) - 18px);
+  flex-basis: calc(var(--scrubberStack-width) - 18px);
   flex-grow: 2;
   flex-shrink: 0;
   margin: 0 9px;
 }
 
 .volumeStack {
   max-width: 60px;
-  min-width: 48px;
+  min-width: var(--volumeStack-width);
   flex-grow: 1;
   flex-shrink: 0;
   margin-right: 6px;
   margin-left: 4px;
 }
 
 .bufferBar,
 .progressBar,
@@ -354,18 +373,18 @@ audio > xul|videocontrols {
   min-width: 70px;
   min-height: 60px;
   background: url(chrome://global/skin/media/error.png) no-repeat center;
   background-size: contain;
 }
 
 /* Overlay Play button */
 .clickToPlay {
-  min-width: 48px;
-  min-height: 48px;
+  min-width: var(--clickToPlay-width);
+  min-height: var(--clickToPlay-height);
   border-radius: 50%;
   background-image: url(chrome://global/skin/media/playButton.svg);
   background-repeat: no-repeat;
   background-position: 54% 50%;
   background-size: 40% 40%;
   background-color: #1a1a1a;
   -moz-context-properties: fill;
   fill: #ffffff;