Bug 1449532 - Part II, Use Web Animation API to animate video control transition draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 30 Mar 2018 00:21:03 +0800
changeset 777043 41ec315236cf27b84b59b821af81fe1d3f0093f2
parent 777001 b8073a256ce9cfeec8da125c3b23ab9c9a069b6b
child 777044 bd34dfc95c73c05f0c9ca97de0867bf6725b111e
push id105061
push usertimdream@gmail.com
push dateWed, 04 Apr 2018 05:03:48 +0000
bugs1449532, 1374007, 1319301
milestone61.0a1
Bug 1449532 - Part II, Use Web Animation API to animate video control transition Web Animation API should give us deterministic timing when the transition ends or aborts. Additional clean-ups: - Make sure hidden status is always set/get from the hidden property, instead of the hidden attribute. - Remove the unused isControlBarHidden property. - controlsSpacer no longer has a background color (removed in bug 1374007), therefore it no longer needs a transition and there is no need to test its state with the test added in bug 1319301. - Fix a logic error at hideByAdjustment property, revealed by the changed transition timing, in which adjustControlSize() would show the controlBar set hidden by the transition. MozReview-Commit-ID: DB2cgQcUEXi
toolkit/content/moz.build
toolkit/content/tests/widgets/mochitest.ini
toolkit/content/tests/widgets/test_bug1319301.html
toolkit/content/tests/widgets/test_videocontrols_vtt.html
toolkit/content/widgets/videocontrols.xml
toolkit/themes/shared/media/videocontrols.css
--- a/toolkit/content/moz.build
+++ b/toolkit/content/moz.build
@@ -177,18 +177,16 @@ with Files('tests/reftests/*videocontrol
     BUG_COMPONENT = ('Toolkit', 'Video/Audio Controls')
 
 with Files('tests/unit/**'):
     BUG_COMPONENT = ('Toolkit', 'General')
 
 
 with Files('tests/widgets/*audiocontrols*'):
     BUG_COMPONENT = ('Toolkit', 'Video/Audio Controls')
-with Files('tests/widgets/*1319301*'):
-    BUG_COMPONENT = ('Toolkit', 'Video/Audio Controls')
 with Files('tests/widgets/*898940*'):
     BUG_COMPONENT = ('Toolkit', 'Video/Audio Controls')
 
 with Files('tests/widgets/*contextmenu*'):
     BUG_COMPONENT = ('Firefox', 'Menus')
 
 with Files('tests/widgets/*editor*'):
     BUG_COMPONENT = ('Core', 'XP Toolkit/Widgets: XUL')
--- a/toolkit/content/tests/widgets/mochitest.ini
+++ b/toolkit/content/tests/widgets/mochitest.ini
@@ -36,13 +36,12 @@ skip-if = toolkit == 'android'
 [test_videocontrols_audio_direction.html]
 [test_videocontrols_jsdisabled.html]
 skip-if = toolkit == 'android' # bug 1272646
 [test_videocontrols_standalone.html]
 skip-if = toolkit == 'android' # bug 1075573
 [test_videocontrols_video_direction.html]
 skip-if = os == 'win'
 [test_videocontrols_video_noaudio.html]
-[test_bug1319301.html]
 [test_bug898940.html]
 [test_videocontrols_error.html]
 [test_videocontrols_orientation.html]
 run-if = toolkit == 'android'
deleted file mode 100644
--- a/toolkit/content/tests/widgets/test_bug1319301.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>Video controls test - bug 1319301</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-<body>
-<p id="display"></p>
-
-<div id="content">
-  <video id="video" controls preload="auto"></video>
-</div>
-
-<pre id="test">
-<script clas="testbody" type="application/javascript">
-  const video = document.getElementById("video");
-  const controlsSpacer = getAnonElementWithinVideoByAttribute(video, "anonid", "controlsSpacer");
-
-  add_task(async function setup() {
-    await new Promise(resolve => window.addEventListener("load", resolve));
-    await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]});
-  });
-
-  add_task(async function play_fadeout() {
-    isnot(controlsSpacer.getAttribute("fadeout"), "true", "controlsSpacer should not fadeout before playing");
-
-    await new Promise(resolve => {
-      video.addEventListener("canplaythrough", video.play);
-      video.addEventListener("play", () => SimpleTest.executeSoon(resolve));
-      video.src = "seek_with_sound.ogg";
-    });
-
-    is(controlsSpacer.getAttribute("fadeout"), "true", "controlsSpacer should fadeout once video starts playing");
-  });
-</script>
-</pre>
-</body>
-</html>
--- a/toolkit/content/tests/widgets/test_videocontrols_vtt.html
+++ b/toolkit/content/tests/widgets/test_videocontrols_vtt.html
@@ -28,83 +28,83 @@
     await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]});
     await new Promise(resolve => {
       video.src = "seek_with_sound.ogg";
       video.addEventListener("loadedmetadata", resolve);
     });
   });
 
   add_task(async function check_inital_state() {
-    is(ccBtn.getAttribute("hidden"), "true", "CC button should hide");
+    ok(ccBtn.hidden, "CC button should hide");
   });
 
   add_task(async function check_unsupported_type_added() {
     video.addTextTrack("descriptions", "English", "en");
     video.addTextTrack("chapters", "English", "en");
     video.addTextTrack("metadata", "English", "en");
 
     await new Promise(SimpleTest.executeSoon);
-    is(ccBtn.getAttribute("hidden"), "true", "CC button should hide if no supported tracks provided");
+    ok(ccBtn.hidden, "CC button should hide if no supported tracks provided");
   });
 
   add_task(async function check_cc_button_present() {
     const sub = video.addTextTrack("subtitles", "English", "en");
     sub.mode = "disabled";
 
     await new Promise(SimpleTest.executeSoon);
-    is(ccBtn.hasAttribute("hidden"), false, "CC button should show");
+    ok(!ccBtn.hidden, "CC button should show");
     is(ccBtn.hasAttribute("enabled"), false, "CC button should be disabled");
   });
 
   add_task(async function check_cc_button_be_enabled() {
     const subtitle = video.addTextTrack("subtitles", "English", "en");
     subtitle.mode = "showing";
 
     await new Promise(SimpleTest.executeSoon);
-    is(ccBtn.getAttribute("enabled"), "true", "CC button should be enabled");
+    ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled");
     subtitle.mode = "disabled";
   });
 
   add_task(async function check_cpations_type() {
     const caption = video.addTextTrack("captions", "English", "en");
     caption.mode = "showing";
 
     await new Promise(SimpleTest.executeSoon);
-    is(ccBtn.getAttribute("enabled"), "true", "CC button should be enabled");
+    ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled");
   });
 
   add_task(async function check_track_ui_state() {
     synthesizeMouseAtCenter(ccBtn, {});
 
     await new Promise(SimpleTest.executeSoon);
-    is(ttList.hasAttribute("hidden"), false, "Texttrack menu should show up");
-    is(ttList.lastChild.getAttribute("on"), "true", "The last added item should be highlighted");
+    ok(!ttList.hidden, "Texttrack menu should show up");
+    ok(ttList.lastChild.hasAttribute("on"), "The last added item should be highlighted");
   });
 
   add_task(async function check_select_texttrack() {
     const tt = ttList.children[1];
 
-    isnot(tt.getAttribute("on"), "true", "Item should be off before click");
+    ok(!tt.hasAttribute("on"), "Item should be off before click");
     synthesizeMouseAtCenter(tt, {});
 
     await new Promise(SimpleTest.executeSoon);
-    is(tt.getAttribute("on"), "true", "Selected item should be enabled");
-    is(ttList.getAttribute("hidden"), "true", "Should hide texttrack menu once clicked on an item");
+    ok(tt.hasAttribute("on"), "Selected item should be enabled");
+    ok(ttList.hidden, "Should hide texttrack menu once clicked on an item");
   });
 
   add_task(async function check_change_texttrack_mode() {
     const tts = [...video.textTracks];
 
     tts.forEach(tt => tt.mode = "hidden");
     await new Promise(SimpleTest.executeSoon);
-    is(ccBtn.hasAttribute("enabled"), false, "CC button should be disabled");
+    ok(!ccBtn.hasAttribute("enabled"), "CC button should be disabled");
 
     // enable the last text track.
     tts[tts.length - 1].mode = "showing";
     await new Promise(SimpleTest.executeSoon);
-    is(ccBtn.hasAttribute("enabled"), true, "CC button should be enabled");
-    is(ttList.lastChild.getAttribute("on"), "true", "The last item should be highlighted");
+    ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled");
+    ok(ttList.lastChild.hasAttribute("on"), "The last item should be highlighted");
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -133,22 +133,16 @@
           this.video.style.height = this.controlBarMinHeight + "px";
           this.video.style.width = "66%";
         } else {
           this.video.style.removeProperty("height");
           this.video.style.removeProperty("width");
         }
       },
 
-      get isControlBarHidden() {
-        return this.controlBar.hidden ||
-               this.controlBar.hideByAdjustment ||
-               this.controlBar.getAttribute("fadeout") === "true";
-      },
-
       suppressError: false,
 
       setupStatusFader(immediate) {
         // Since the play button will be showing, we don't want to
         // show the throbber behind it. The throbber here will
         // only show if needed after the play button has been pressed.
         if (!this.clickToPlay.hidden) {
           this.startFadeOut(this.statusOverlay, true);
@@ -293,31 +287,46 @@
             modifier: {
               value: "",
               writable: true
             },
             isWanted: {
               value: true,
               writable: true
             },
-            hideByAdjustment: {
+            hidden: {
               set: (v) => {
-                if (v) {
-                  control.setAttribute("hidden", "true");
-                } else {
-                  control.removeAttribute("hidden");
-                }
-
+                control._isHiddenExplicitly = v;
+                control._updateHiddenAttribute();
+              },
+              get: () => control.hasAttribute("hidden")
+            },
+            hiddenByAdjustment: {
+              set: (v) => {
                 control._isHiddenByAdjustment = v;
+                control._updateHiddenAttribute();
               },
               get: () => control._isHiddenByAdjustment
             },
             _isHiddenByAdjustment: {
               value: false,
               writable: true
+            },
+            _isHiddenExplicitly: {
+              value: false,
+              writable: true
+            },
+            _updateHiddenAttribute: {
+              value: () => {
+                if (control._isHiddenExplicitly || control._isHiddenByAdjustment) {
+                  control.setAttribute("hidden", "");
+                } else {
+                  control.removeAttribute("hidden");
+                }
+              }
             }
           });
         }
         this.adjustControlSize();
 
         // Can only update the volume controls once we've computed
         // _volumeControlWidth, since the volume slider implementation
         // depends on it.
@@ -337,17 +346,16 @@
         // (Note: the |controls| attribute is already handled via layout/style/html.css)
         var shouldShow = !this.dynamicControls ||
                          (this.video.paused &&
                          !this.video.autoplay);
         // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
         let shouldClickToPlayShow = shouldShow && !this.isAudioOnly &&
                                     this.video.currentTime == 0 && !this.hasError();
         this.startFade(this.clickToPlay, shouldClickToPlayShow, true);
-        this.startFade(this.controlsSpacer, shouldClickToPlayShow, true);
         this.startFade(this.controlBar, shouldShow, true);
       },
 
       get dynamicControls() {
         // Don't fade controls for <audio> elements.
         var enabled = !this.isAudioOnly;
 
         // Allow tests to explicitly suppress the fading of controls.
@@ -393,18 +401,18 @@
        * is completed.
        */
       SHOW_THROBBER_TIMEOUT_MS: 250,
       _showThrobberTimer: null,
       _delayShowThrobberWhileResumingVideoDecoder() {
         this._showThrobberTimer = setTimeout(() => {
           this.statusIcon.setAttribute("type", "throbber");
           // Show the throbber immediately since we have waited for SHOW_THROBBER_TIMEOUT_MS.
-          // We don't want to wait for another transition-delay(750ms) and the
-          // transition-duration(300ms).
+          // We don't want to wait for another animation delay(750ms) and the
+          // animation duration(300ms).
           this.setupStatusFader(true);
         }, this.SHOW_THROBBER_TIMEOUT_MS);
       },
       _cancelShowThrobberWhileResumingVideoDecoder() {
         if (this._showThrobberTimer) {
           clearTimeout(this._showThrobberTimer);
           this._showThrobberTimer = null;
         }
@@ -424,17 +432,16 @@
           case "play":
             this.setPlayButtonState(false);
             this.setupStatusFader();
             if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControls) {
               this.startFadeOut(this.controlBar);
             }
             if (!this._triggeredByControls) {
               this.clickToPlay.hidden = true;
-              this.controlsSpacer.setAttribute("fadeout", "true");
             }
             this._triggeredByControls = false;
             break;
           case "pause":
             // Little white lie: if we've internally paused the video
             // while dragging the scrubber, don't change the button state.
             if (!this.scrubber.isDragging) {
               this.setPlayButtonState(true);
@@ -964,81 +971,131 @@
           this.adjustControlSize();
 
           // Keep the controls visible if the click-to-play is visible.
           if (!this.clickToPlay.hidden) {
             return;
           }
 
           this.startFadeOut(this.controlBar, false);
-          this.textTrackList.setAttribute("hidden", "true");
+          this.textTrackList.hidden = true;
           clearTimeout(this._showControlsTimeout);
           Utils._controlsHiddenByTimeout = false;
         }
       },
 
       startFadeIn(element, immediate) {
         this.startFade(element, true, immediate);
       },
 
       startFadeOut(element, immediate) {
         this.startFade(element, false, immediate);
       },
 
-      startFade(element, fadeIn, immediate) {
-        if (element.classList.contains("controlBar") && fadeIn) {
-          // Bug 493523, the scrubber doesn't call valueChanged while hidden,
-          // so our dependent state (eg, timestamp in the thumb) will be stale.
-          // As a workaround, update it manually when it first becomes unhidden.
-          if (element.hidden) {
-            this.scrubber.value = this.video.currentTime * 1000;
+      animationMap: new WeakMap(),
+
+      animationProps: {
+        clickToPlay: {
+          keyframes: [
+            { transform: "scale(3)", opacity: 0 },
+            { transform: "scale(1)", opacity: 0.55 }
+          ],
+          options: {
+            duration: 400
+          }
+        },
+        controlBar: {
+          keyframes: [
+            { opacity: 0 },
+            { opacity: 1 }
+          ],
+          options: {
+            duration: 200
+          }
+        },
+        statusOverlay: {
+          keyframes: [
+            { opacity: 0 },
+            { opacity: 0, offset: .72 }, // ~750ms into animation
+            { opacity: 1 }
+          ],
+          options: {
+            duration: 1050
           }
         }
+      },
 
-        if (immediate) {
-          element.setAttribute("immediate", true);
-        } else {
-          element.removeAttribute("immediate");
+      startFade(element, fadeIn, immediate = false) {
+        // Bug 493523, the scrubber doesn't call valueChanged while hidden,
+        // so our dependent state (eg, timestamp in the thumb) will be stale.
+        // As a workaround, update it manually when it first becomes unhidden.
+        if (element == this.controlBar && fadeIn && element.hidden) {
+          this.scrubber.value = this.video.currentTime * 1000;
+        }
+
+        let animationProp =
+          this.animationProps[element.getAttribute("anonid")];
+        if (!animationProp) {
+          throw new Error("Element " + element.getAttribute("anonid") +
+            " has no transition. Toggle the hidden property directly.");
+        }
+
+        let animation = this.animationMap.get(element);
+        if (!animation) {
+          animation = new Animation(new KeyframeEffect(
+            element, animationProp.keyframes, animationProp.options));
+
+          this.animationMap.set(element, animation);
         }
 
         if (fadeIn) {
           // hidden state should be controlled by adjustControlSize
-          if (!(element.isAdjustableControl && element.hideByAdjustment)) {
-            element.hidden = false;
+          if (element.isAdjustableControl && element.hiddenByAdjustment) {
+            return;
+          }
+
+          // No need to fade in again if the element is visible and not fading out
+          if (!element.hidden && !element.classList.contains("fadeout")) {
+            return;
           }
-          // force style resolution, so that transition begins
-          // when we remove the attribute.
-          element.clientTop;
-          element.removeAttribute("fadeout");
-          if (element.classList.contains("controlBar")) {
-            this.controlsSpacer.removeAttribute("hideCursor");
+
+          // Unhide
+          element.hidden = false;
+        } else {
+          // No need to fade out if the element is already no visible.
+          if (element.hidden) {
+            return;
           }
-        } else {
-          element.setAttribute("fadeout", true);
-          if (element.classList.contains("controlBar") && !this.hasError() &&
+
+          if (element == this.controlBar && !this.hasError() &&
               document.mozFullScreenElement == this.video) {
             this.controlsSpacer.setAttribute("hideCursor", true);
           }
         }
-      },
 
-      onTransitionEnd(event) {
-        // Ignore events for things other than opacity changes.
-        if (event.propertyName != "opacity") {
-          return;
+        element.classList.toggle("fadeout", !fadeIn);
+        element.classList.toggle("fadein", fadeIn);
+        let finishedPromise;
+        if (!immediate) {
+          animation.playbackRate = fadeIn ? 1 : -1;
+          animation.play();
+          finishedPromise = animation.finished;
+        } else {
+          animation.cancel();
+          finishedPromise = Promise.resolve();
         }
-
-        var element = event.originalTarget;
-
-        // Nothing to do when a fade *in* finishes.
-        if (!element.hasAttribute("fadeout")) {
-          return;
-        }
-
-        element.hidden = true;
+        finishedPromise.then(() => {
+          if (element == this.controlBar) {
+            this.onControlBarAnimationFinished();
+          }
+          element.classList.remove(fadeIn ? "fadein" : "fadeout");
+          if (!fadeIn) {
+            element.hidden = true;
+          }
+        }, () => { /* Do nothing on rejection */ });
       },
 
       _triggeredByControls: false,
 
       startPlay() {
         this._triggeredByControls = true;
         this.hideClickToPlay();
         this.video.play();
@@ -1186,25 +1243,19 @@
         let videoWidth = this.video.clientWidth;
 
         // The play button will animate to 3x its size. This
         // shows the animation unless the video is too small
         // to show 2/3 of the animation.
         let animationScale = 2;
         let animationMinSize = this.clickToPlay.minWidth * animationScale;
 
-        if (animationMinSize > videoWidth ||
-            animationMinSize > (videoHeight - this.controlBarMinHeight)) {
-          this.clickToPlay.setAttribute("immediate", "true");
-          this.clickToPlay.hidden = true;
-        } else {
-          this.clickToPlay.removeAttribute("immediate");
-        }
-        this.clickToPlay.setAttribute("fadeout", "true");
-        this.controlsSpacer.setAttribute("fadeout", "true");
+        let immediate = (animationMinSize > videoWidth ||
+            animationMinSize > (videoHeight - this.controlBarMinHeight));
+        this.startFadeOut(this.clickToPlay, immediate);
       },
 
       setPlayButtonState(aPaused) {
         if (aPaused) {
           this.playButton.setAttribute("paused", "true");
         } else {
           this.playButton.removeAttribute("paused");
         }
@@ -1481,34 +1532,34 @@
         for (let tt of this.overlayableTextTracks) {
           if (tt.index === index) {
             tt.mode = "showing";
           } else {
             tt.mode = "disabled";
           }
         }
 
-        this.textTrackList.setAttribute("hidden", "true");
+        this.textTrackList.hidden = true;
       },
 
-      onControlBarTransitioned() {
-        this.textTrackList.setAttribute("hidden", "true");
+      onControlBarAnimationFinished() {
+        this.textTrackList.hidden = true;
         this.video.dispatchEvent(new CustomEvent("controlbarchange"));
         this.adjustControlSize();
       },
 
       toggleCasting() {
         this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
       },
 
       toggleClosedCaption() {
-        if (this.textTrackList.hasAttribute("hidden")) {
-          this.textTrackList.removeAttribute("hidden");
+        if (this.textTrackList.hidden) {
+          this.textTrackList.hidden = false;
         } else {
-          this.textTrackList.setAttribute("hidden", "true");
+          this.textTrackList.hidden = true;
         }
       },
 
       onTextTrackAdd(trackEvent) {
         this.addNewTextTrack(trackEvent.track);
         this.setClosedCaptionButtonState();
       },
 
@@ -1600,24 +1651,24 @@
         let videoHeight = this.isAudioOnly ? this.controlBarMinHeight : givenHeight;
         let videocontrolsWidth = this.videocontrols.clientWidth;
 
         let widthUsed = minControlBarPaddingWidth;
         let preventAppendControl = false;
 
         for (let control of this.prioritizedControls) {
           if (!control.isWanted) {
-            control.hideByAdjustment = true;
+            control.hiddenByAdjustment = true;
             continue;
           }
 
-          control.hideByAdjustment = preventAppendControl ||
+          control.hiddenByAdjustment = preventAppendControl ||
           widthUsed + control.minWidth > videoWidth;
 
-          if (control.hideByAdjustment) {
+          if (control.hiddenByAdjustment) {
             preventAppendControl = true;
           } else {
             widthUsed += control.minWidth;
           }
         }
 
         // Use flexible spacer to separate controls when scrubber is hidden.
         // As long as muteButton hidden, which means only play button presents,
@@ -1642,34 +1693,34 @@
             this.controlBar.style.width = `${videoWidth}px`;
           }
           return;
         }
 
         if (videoHeight < this.controlBarMinHeight ||
             widthUsed === minControlBarPaddingWidth) {
           this.controlBar.setAttribute("size", "hidden");
-          this.controlBar.hideByAdjustment = true;
+          this.controlBar.hiddenByAdjustment = true;
         } else {
           this.controlBar.removeAttribute("size");
-          this.controlBar.hideByAdjustment = false;
+          this.controlBar.hiddenByAdjustment = false;
         }
 
         // 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;
+          this.clickToPlay.hiddenByAdjustment = true;
         } else {
           if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
-            this.clickToPlay.hideByAdjustment = false;
+            this.clickToPlay.hiddenByAdjustment = false;
           }
           this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
           this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
         }
       },
 
       init(binding) {
         this.video = binding.parentNode;
@@ -1774,18 +1825,16 @@
         // On touch videocontrols, tapping controlsSpacer should show/hide
         // the control bar, instead of playing the video or toggle fullscreen.
         if (!this.videocontrols.isTouchControls) {
           addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
           addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
         }
 
         addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
-        addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
-        addListener(this.controlBar, "transitionend", this.onControlBarTransitioned);
         // See comment at onFullscreenChange on bug 718107.
         // addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
         addListener(this.video, "keypress", this.keyHandler, {capture: true});
         // Prevent any click event within media controls from dispatching through to video.
         addListener(this.videocontrols, "click", function(event) {
           event.stopPropagation();
         }, {mozSystemGroup: false});
         addListener(this.videocontrols, "dragstart", function(event) {
@@ -1818,17 +1867,17 @@
       controlsTimeout: 5000,
 
       get Utils() {
         return this.videocontrols.Utils;
       },
 
       get visible() {
         return !this.Utils.controlBar.hasAttribute("fadeout") &&
-               !(this.Utils.controlBar.getAttribute("hidden") == "true");
+               !(this.Utils.controlBar.hidden);
       },
 
       _firstShow: false,
       get firstShow() { return this._firstShow; },
       set firstShow(val) {
         this._firstShow = val;
         this.Utils.controlBar.setAttribute("firstshow", val);
       },
--- a/toolkit/themes/shared/media/videocontrols.css
+++ b/toolkit/themes/shared/media/videocontrols.css
@@ -60,17 +60,17 @@ audio > xul|videocontrols {
 
 .touch .controlBar {
   /* Do not delete: these variables are accessed by JavaScript directly.
      see videocontrols.xml and search for |-width|. */
   --scrubberStack-width: 84px;
   --volumeStack-width: 64px;
 }
 
-.controlsContainer [hidden="true"],
+.controlsContainer [hidden],
 .controlBar[hidden] {
   display: none;
 }
 
 .controlBar[size="hidden"] {
   display: none;
 }
 
@@ -444,51 +444,16 @@ audio > xul|videocontrols {
 .controlsSpacerStack:hover > .clickToPlay[fadeout] {
   opacity: 0;
 }
 
 .controlBar[fullscreen-unavailable] .fullscreenButton {
   display: none;
 }
 
-/* CSS Transitions */
-.clickToPlay {
-  transition-property: transform, opacity;
-  transition-duration: 400ms, 400ms;
-}
-
-.controlsSpacer[fadeout] {
-  opacity: 0;
-}
-
-.clickToPlay[fadeout] {
-  transform: scale(3);
-  opacity: 0;
-}
-
-.clickToPlay[fadeout][immediate] {
-  transition-property: opacity, background-size;
-  transition-duration: 0s, 0s;
-}
-.controlBar:not([immediate]) {
-  transition-property: opacity;
-  transition-duration: 200ms;
-}
-.controlBar[fadeout] {
-  opacity: 0;
-}
-.volumeStack:not([immediate]) {
-  transition-property: opacity, margin-top;
-  transition-duration: 200ms, 200ms;
-}
-.statusOverlay:not([immediate]) {
-  transition-property: opacity;
-  transition-duration: 300ms;
-  transition-delay: 750ms;
-}
 .statusOverlay[fadeout],
 .statusOverlay[error] + .controlsOverlay > .controlsSpacerStack {
   opacity: 0;
 }
 
 /* Error description formatting */
 .errorLabel {
   padding: 0 10px;