Bug 1431255 - Part IV, Load videocontrols.js, migrated from videoControls binding r=jaws
authorTimothy Guan-tin Chien <timdream@gmail.com>
Wed, 27 Jun 2018 11:55:38 -0700
changeset 486754 4ddca5eb06c25aeeeae1f9776b81fe971a4f75cd
parent 486753 a0200438265ba27626915031f3dd2f91f95969b6
child 486755 74adb43ed11299a58f455aa7db6d8f89cd885e93
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1431255
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1431255 - Part IV, Load videocontrols.js, migrated from videoControls binding r=jaws videocontrols.js handles the controls attribute with a callback named "onattributechange" called by UAWidgets, replaces the CSS selectors. MozReview-Commit-ID: 8rrw0Pbu8Dj
toolkit/content/jar.mn
toolkit/content/tests/widgets/test_audiocontrols_dimensions.html
toolkit/content/widgets/videocontrols.js
toolkit/themes/shared/media/videocontrols.css
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -98,13 +98,14 @@ toolkit.jar:
    content/global/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
    content/global/bindings/tree.xml            (widgets/tree.xml)
    content/global/bindings/videocontrols.xml   (widgets/videocontrols.xml)
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
    content/global/elements/editor.js           (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
+   content/global/elements/videocontrols.js    (widgets/videocontrols.js)
 #ifdef XP_MACOSX
    content/global/macWindowMenu.js
 #endif
    content/global/gmp-sources/openh264.json    (gmp-sources/openh264.json)
    content/global/gmp-sources/widevinecdm.json (gmp-sources/widevinecdm.json)
--- a/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html
+++ b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html
@@ -37,12 +37,31 @@
     isnot(originalControlBarWidth, 400, "the default audio width is not 400px");
 
     audio.style.width = "400px";
     audio.offsetWidth; // force reflow
 
     isnot(controlBar.clientWidth, originalControlBarWidth, "new width should differ from the origianl width");
     is(controlBar.clientWidth, 400, "controlbar's width should grow with audio width");
   });
+
+  add_task(function check_audio_height_construction_sync() {
+    let el = new Audio();
+    el.src = "audio.wav";
+    el.controls = true;
+    document.body.appendChild(el);
+    is(el.clientHeight, 40, "Height of audio element with controls");
+    document.body.removeChild(el);
+  });
+
+  add_task(function check_audio_height_add_control_sync() {
+    let el = new Audio();
+    el.src = "audio.wav";
+    document.body.appendChild(el);
+    is(el.clientHeight, 0, "Height of audio element without controls");
+    el.controls = true;
+    is(el.clientHeight, 40, "Height of audio element with controls");
+    document.body.removeChild(el);
+  });
 </script>
 </pre>
 </body>
 </html>
copy from toolkit/content/widgets/videocontrols.xml
copy to toolkit/content/widgets/videocontrols.js
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.js
@@ -1,93 +1,88 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-<!DOCTYPE bindings [
-<!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
-%videocontrolsDTD;
-]>
+"use strict";
 
-<bindings id="videoControlBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl"
-          xmlns:svg="http://www.w3.org/2000/svg"
-          xmlns:html="http://www.w3.org/1999/xhtml">
+// This is a page widget. It runs in per-origin UA widget scope,
+// to be loaded by UAWidgetsChild.jsm.
 
-<binding id="videoControls">
-  <resources>
-    <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
-  </resources>
+/*
+ * This is the class of entry. It will construct the actual implementation
+ * according to the value of the "controls" property.
+ */
+this.VideoControlsPageWidget = class {
+  constructor(shadowRoot) {
+    this.shadowRoot = shadowRoot;
+    this.element = shadowRoot.host;
+    this.document = this.element.ownerDocument;
+    this.window = this.document.defaultView;
 
-  <xbl:content xmlns="http://www.w3.org/1999/xhtml" class="mediaControlsFrame">
-    <div anonid="controlsContainer" class="controlsContainer" role="none">
-      <div anonid="statusOverlay" class="statusOverlay stackItem" hidden="true">
-        <div anonid="statusIcon" class="statusIcon"></div>
-        <span class="errorLabel" anonid="errorAborted">&error.aborted;</span>
-        <span class="errorLabel" anonid="errorNetwork">&error.network;</span>
-        <span class="errorLabel" anonid="errorDecode">&error.decode;</span>
-        <span class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</span>
-        <span class="errorLabel" anonid="errorNoSource">&error.noSource2;</span>
-        <span class="errorLabel" anonid="errorGeneric">&error.generic;</span>
-      </div>
+    this.isMobile = this.window.navigator.appVersion.includes("Android");
+
+    this.switchImpl();
+  }
+
+  /*
+   * Callback called by UAWidgets when the "controls" property changes.
+   */
+  onattributechange() {
+    this.switchImpl();
+  }
 
-      <div anonid="controlsOverlay" class="controlsOverlay stackItem">
-        <div class="controlsSpacerStack">
-          <div anonid="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
-          <div anonid="clickToPlay" class="clickToPlay" hidden="true"></div>
-        </div>
-        <div anonid="controlBar" class="controlBar" hidden="true">
-          <button anonid="playButton"
-                  class="button playButton"
-                  playlabel="&playButton.playLabel;"
-                  pauselabel="&playButton.pauseLabel;"
-                  tabindex="-1"/>
-          <div anonid="scrubberStack" class="scrubberStack progressContainer" role="none">
-            <div class="progressBackgroundBar stackItem" role="none">
-              <div class="progressStack" role="none">
-                <progress anonid="bufferBar" class="bufferBar" value="0" max="100" tabindex="-1"></progress>
-                <progress anonid="progressBar" class="progressBar" value="0" max="100" tabindex="-1"></progress>
-              </div>
-            </div>
-            <input type="range" anonid="scrubber" class="scrubber" tabindex="-1"/>
-          </div>
-          <span anonid="positionLabel" class="positionLabel" role="presentation"></span>
-          <span anonid="durationLabel" class="durationLabel" role="presentation"></span>
-          <span anonid="positionDurationBox" class="positionDurationBox" aria-hidden="true">
-            &positionAndDuration.nameFormat;
-          </span>
-          <div anonid="controlBarSpacer" class="controlBarSpacer" hidden="true" role="none"></div>
-          <button anonid="muteButton"
-                  class="button muteButton"
-                  mutelabel="&muteButton.muteLabel;"
-                  unmutelabel="&muteButton.unmuteLabel;"
-                  tabindex="-1"/>
-          <div anonid="volumeStack" class="volumeStack progressContainer" role="none">
-            <input type="range" anonid="volumeControl" class="volumeControl" min="0" max="100" step="1" tabindex="-1"/>
-          </div>
-          <button anonid="castingButton" class="button castingButton"
-                  aria-label="&castingButton.castingLabel;"/>
-          <button anonid="closedCaptionButton" class="button closedCaptionButton"/>
-          <button anonid="fullscreenButton"
-                  class="button fullscreenButton"
-                  enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
-                  exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
-        </div>
-        <div anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
-      </div>
-    </div>
-  </xbl:content>
+  /*
+   * Actually switch the implementation.
+   * - With "controls" set, the VideoControlsImplPageWidget controls should load.
+   * - Without it, on mobile, the NoControlsImplPageWidget should load, so
+   *   the user could see the click-to-play button when the video/audio is blocked.
+   */
+  switchImpl() {
+    let newImpl;
+    if (this.element.controls) {
+      newImpl = VideoControlsImplPageWidget;
+    } else if (this.isMobile) {
+      newImpl = NoControlsImplPageWidget;
+    }
+    // Skip if we are asked to load the same implementation.
+    // This can happen if the property is set again w/o value change.
+    if (this.impl && this.impl.constructor == newImpl) {
+      return;
+    }
+    if (this.impl) {
+      this.impl.destructor();
+      this.shadowRoot.firstChild.remove();
+    }
+    if (newImpl) {
+      this.impl = new newImpl(this.shadowRoot);
+    } else {
+      this.impl = undefined;
+    }
+  }
 
-  <implementation>
+  destructor() {
+    if (!this.impl) {
+      return;
+    }
+    this.impl.destructor();
+    this.shadowRoot.firstChild.remove();
+    delete this.impl;
+  }
+};
 
-  <constructor>
-    <![CDATA[
+this.VideoControlsImplPageWidget = class {
+  constructor(shadowRoot) {
+    this.shadowRoot = shadowRoot;
+    this.element = shadowRoot.host;
+    this.document = this.element.ownerDocument;
+    this.window = this.document.defaultView;
+
+    this.generateContent();
+
     this.randomID = 0;
 
     this.Utils = {
       debug: false,
       video: null,
       videocontrols: null,
       controlBar: null,
       playButton: null,
@@ -204,17 +199,17 @@
         // to determine if the media source changed while we were detached.
         this.initPositionDurationBox();
         this.maxCurrentTimeSeen = currentTime;
         this.showPosition(currentTime, duration);
 
         // If we have metadata, check if this is a <video> without
         // video data, or a video with no audio track.
         if (this.video.readyState >= this.video.HAVE_METADATA) {
-          if (this.video instanceof HTMLVideoElement &&
+          if (this.video instanceof this.window.HTMLVideoElement &&
               (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
             this.isAudioOnly = true;
           }
 
           // We have to check again if the media has audio here,
           // because of bug 718107: switching to fullscreen may
           // cause the bindings to detach and reattach, hence
           // unsetting the attribute.
@@ -266,18 +261,18 @@
             break;
           }
 
           Object.defineProperties(control, {
             // We should directly access CSSOM to get pre-defined style instead of
             // retrieving computed dimensions from layout.
             minWidth: {
               get: () => {
-                let controlAnonId = control.getAttribute("anonid");
-                let propertyName = `--${controlAnonId}-width`;
+                let controlId = control.id;
+                let propertyName = `--${controlId}-width`;
                 if (control.modifier) {
                   propertyName += "-" + control.modifier;
                 }
                 let preDefinedSize = this.controlBarComputedStyles.getPropertyValue(propertyName);
 
                 return parseInt(preDefinedSize, 10);
               }
             },
@@ -398,27 +393,27 @@
        *
        * Once the queued seek operation is done, we dispatch a
        * "canplay" event which indicates that the resuming operation
        * is completed.
        */
       SHOW_THROBBER_TIMEOUT_MS: 250,
       _showThrobberTimer: null,
       _delayShowThrobberWhileResumingVideoDecoder() {
-        this._showThrobberTimer = setTimeout(() => {
+        this._showThrobberTimer = this.window.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 animation delay(750ms) and the
           // animation duration(300ms).
           this.setupStatusFader(true);
         }, this.SHOW_THROBBER_TIMEOUT_MS);
       },
       _cancelShowThrobberWhileResumingVideoDecoder() {
         if (this._showThrobberTimer) {
-          clearTimeout(this._showThrobberTimer);
+          this.window.clearTimeout(this._showThrobberTimer);
           this._showThrobberTimer = null;
         }
       },
 
       handleEvent(aEvent) {
         if (!aEvent.isTrusted) {
           this.log("Drop untrusted event ----> " + aEvent.type);
           return;
@@ -472,25 +467,27 @@
             break;
           case "volumechange":
             this.updateVolumeControls();
             // Show the controls to highlight the changing volume,
             // but only if the click-to-play overlay has already
             // been hidden (we don't hide controls when the overlay is visible).
             if (this.clickToPlay.hidden && !this.isAudioOnly) {
               this.startFadeIn(this.controlBar);
-              clearTimeout(this._hideControlsTimeout);
-              this._hideControlsTimeout =
-                setTimeout(() => this._hideControlsFn(), this.HIDE_CONTROLS_TIMEOUT_MS);
+              this.window.clearTimeout(this._hideControlsTimeout);
+              this._hideControlsTimeout = this.window.setTimeout(
+                () => this._hideControlsFn(),
+                this.HIDE_CONTROLS_TIMEOUT_MS
+              );
             }
             break;
           case "loadedmetadata":
             // If a <video> doesn't have any video data, treat it as <audio>
             // and show the controls (they won't fade back out)
-            if (this.video instanceof HTMLVideoElement &&
+            if (this.video instanceof this.window.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.isAudioOnly && !this.video.mozHasAudio) {
@@ -503,17 +500,17 @@
             this.firstFrameShown = true;
             this.setupStatusFader();
             break;
           case "loadstart":
             this.maxCurrentTimeSeen = 0;
             this.controlsSpacer.removeAttribute("aria-label");
             this.statusOverlay.removeAttribute("error");
             this.statusIcon.setAttribute("type", "throbber");
-            this.isAudioOnly = (this.video instanceof HTMLAudioElement);
+            this.isAudioOnly = this.video instanceof this.window.HTMLAudioElement;
             this.setPlayButtonState(true);
             this.setupNewLoadState();
             this.setupStatusFader();
             break;
           case "progress":
             this.statusIcon.removeAttribute("stalled");
             this.showBuffered();
             this.setupStatusFader();
@@ -711,18 +708,18 @@
         }
 
         try {
           for (let { el, type, capture = false } of this.controlsEvents) {
             el.removeEventListener(type, this, { mozSystemGroup: true, capture });
           }
         } catch (ex) {}
 
-        clearTimeout(this._showControlsTimeout);
-        clearTimeout(this._hideControlsTimeout);
+        this.window.clearTimeout(this._showControlsTimeout);
+        this.window.clearTimeout(this._hideControlsTimeout);
         this._cancelShowThrobberWhileResumingVideoDecoder();
 
         this.log("--- videocontrols terminated ---");
       },
 
       hasError() {
         // We either have an explicit error, or the resource selection
         // algorithm is running and we've tried to load something and failed.
@@ -739,17 +736,17 @@
       hasSources() {
         if (this.video.hasAttribute("src") &&
             this.video.getAttribute("src") !== "") {
           return true;
         }
         for (var child = this.video.firstChild;
              child !== null;
              child = child.nextElementSibling) {
-          if (child instanceof HTMLSourceElement) {
+          if (child instanceof this.window.HTMLSourceElement) {
             return true;
           }
         }
         return false;
       },
 
       updateErrorText() {
         let error;
@@ -778,17 +775,17 @@
               break;
           }
         } else if (v.networkState == v.NETWORK_NO_SOURCE) {
           error = "errorNoSource";
         } else {
           return; // No error found.
         }
 
-        let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
+        let label = this.shadowRoot.getElementById(error);
         this.controlsSpacer.setAttribute("aria-label", label.textContent);
         this.statusOverlay.setAttribute("error", error);
       },
 
       formatTime(aTime, showHours = false) {
         // Format the duration as "h:mm:ss" or "m:ss"
         aTime = Math.round(aTime / 1000);
         let hours = Math.floor(aTime / 3600);
@@ -813,17 +810,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");
+        durationSpan.id = "durationSpan";
 
         Object.defineProperties(this.positionDurationBox, {
           durationSpan: {
             value: durationSpan
           },
           position: {
             set: (v) => {
               positionTextNode.textContent = positionFormat.replace("#1", v);
@@ -1005,50 +1002,54 @@
       },
       HIDE_CONTROLS_TIMEOUT_MS: 2000,
       onMouseMove(event) {
         // If the controls are static, don't change anything.
         if (!this.dynamicControls) {
           return;
         }
 
-        clearTimeout(this._hideControlsTimeout);
+        this.window.clearTimeout(this._hideControlsTimeout);
 
         // Suppress fading out the controls until the video has rendered
         // its first frame. But since autoplay videos start off with no
         // controls, let them fade-out so the controls don't get stuck on.
         if (!this.firstFrameShown &&
             !this.video.autoplay) {
           return;
         }
 
         if (this._controlsHiddenByTimeout) {
-          this._showControlsTimeout =
-            setTimeout(() => this._showControlsFn(), this.SHOW_CONTROLS_TIMEOUT_MS);
+          this._showControlsTimeout = this.window.setTimeout(
+            () => this._showControlsFn(),
+            this.SHOW_CONTROLS_TIMEOUT_MS
+          );
         } else {
           this.startFade(this.controlBar, true);
         }
 
         // Hide the controls if the mouse cursor is left on top of the video
         // but above the control bar and if the click-to-play overlay is hidden.
         if ((this._controlsHiddenByTimeout ||
             event.clientY < this.controlBar.getBoundingClientRect().top) &&
             this.clickToPlay.hidden) {
-          this._hideControlsTimeout =
-            setTimeout(() => this._hideControlsFn(), this.HIDE_CONTROLS_TIMEOUT_MS);
+          this._hideControlsTimeout = this.window.setTimeout(
+            () => this._hideControlsFn(),
+            this.HIDE_CONTROLS_TIMEOUT_MS
+          );
         }
       },
 
       onMouseInOut(event) {
         // If the controls are static, don't change anything.
         if (!this.dynamicControls) {
           return;
         }
 
-        clearTimeout(this._hideControlsTimeout);
+        this.window.clearTimeout(this._hideControlsTimeout);
 
         // Ignore events caused by transitions between child nodes.
         // Note that the videocontrols element is the same
         // size as the *content area* of the video element,
         // but this is not the same as the video element's
         // border area if the video has border or padding.
         if (this.checkEventWithin(event, this.videocontrols)) {
           return;
@@ -1075,17 +1076,17 @@
 
           // Keep the controls visible if the click-to-play is visible.
           if (!this.clickToPlay.hidden) {
             return;
           }
 
           this.startFadeOut(this.controlBar, false);
           this.textTrackList.hidden = true;
-          clearTimeout(this._showControlsTimeout);
+          this.window.clearTimeout(this._showControlsTimeout);
           this._controlsHiddenByTimeout = false;
         }
       },
 
       startFadeIn(element, immediate) {
         this.startFade(element, true, immediate);
       },
 
@@ -1144,43 +1145,43 @@
           },
           then(fn) {
             this.fn = fn;
           }
         };
         // Note that handler is not a real Promise.
         // All it offered is a then() method to register a callback
         // to be triggered at the right time.
-        animation.finished = handler;
+        Object.defineProperty(animation, "finished", { value: handler, configurable: true });
         animation.addEventListener("finish", handler);
         animation.addEventListener("cancel", handler);
       },
 
       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")];
+          this.animationProps[element.id];
         if (!animationProp) {
-          throw new Error("Element " + element.getAttribute("anonid") +
+          throw new Error("Element " + element.id +
             " has no transition. Toggle the hidden property directly.");
         }
 
         let animation = this.animationMap.get(element);
         if (!animation) {
           // Create the animation object but don't start it.
           // To be replaced with the following when the constructors ship
           // (currently behind dom.animations-api.core.enabled)
           /*
-          animation = new Animation(new KeyframeEffect(
+          animation = new this.window.Animation(new this.window.KeyframeEffect(
             element, animationProp.keyframes, animationProp.options));
           */
           animation = element.animate(animationProp.keyframes, animationProp.options);
           animation.cancel();
 
           this.animationMap.set(element, animation);
         }
 
@@ -1203,17 +1204,17 @@
           element.hidden = false;
         } else {
           // No need to fade out if the element is already no visible.
           if (element.hidden) {
             return;
           }
 
           if (element == this.controlBar && !this.hasError() &&
-              document.mozFullScreenElement == this.video) {
+              this.document.mozFullScreenElement == this.video) {
             this.controlsSpacer.setAttribute("hideCursor", true);
           }
         }
 
         element.classList.toggle("fadeout", !fadeIn);
         element.classList.toggle("fadein", fadeIn);
         let finishedPromise;
         if (!immediate) {
@@ -1272,27 +1273,27 @@
         }
 
         // We'll handle style changes in the event listener for
         // the "volumechange" event, same as if content script was
         // controlling volume.
       },
 
       get isVideoInFullScreen() {
-        return document.mozFullScreenElement == this.video;
+        return this.document.mozFullScreenElement == this.video;
       },
 
       toggleFullscreen() {
         this.isVideoInFullScreen ?
-          document.mozCancelFullScreen() :
+          this.document.mozCancelFullScreen() :
           this.video.mozRequestFullScreen();
       },
 
       setFullscreenButtonState() {
-        if (this.isAudioOnly || !document.mozFullScreenEnabled) {
+        if (this.isAudioOnly || !this.document.mozFullScreenEnabled) {
           this.controlBar.setAttribute("fullscreen-unavailable", true);
           this.adjustControlSize();
           return;
         }
         this.controlBar.removeAttribute("fullscreen-unavailable");
         this.adjustControlSize();
 
         var attrName = this.isVideoInFullScreen ? "exitfullscreenlabel" : "enterfullscreenlabel";
@@ -1321,17 +1322,17 @@
 
         // This is already broken by bug 718107 (controls will be hidden
         // as soon as the video enters fullscreen).
         // We can think about restoring the behavior here once the bug is
         // fixed, or we could simply acknowledge the current behavior
         // after-the-fact and try not to fix this.
         if (this.isVideoInFullScreen) {
           this._hideControlsTimeout =
-            setTimeout(() => this._hideControlsFn(), this.HIDE_CONTROLS_TIMEOUT_MS);
+            this.window.setTimeout(() => this._hideControlsFn(), this.HIDE_CONTROLS_TIMEOUT_MS);
         }
 
         // Constructor will handle this correctly on the new DOM content in
         // the new binding.
         this.setFullscreenButtonState();
       },
       */
 
@@ -1340,27 +1341,27 @@
           return;
         }
         if (lock) {
           if (this.video.mozIsOrientationLocked) {
             return;
           }
           let dimenDiff = this.video.videoWidth - this.video.videoHeight;
           if (dimenDiff > 0) {
-            this.video.mozIsOrientationLocked = window.screen.mozLockOrientation("landscape");
+            this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation("landscape");
           } else if (dimenDiff < 0) {
-            this.video.mozIsOrientationLocked = window.screen.mozLockOrientation("portrait");
+            this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation("portrait");
           } else {
-            this.video.mozIsOrientationLocked = window.screen.mozLockOrientation(window.screen.orientation);
+            this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation(this.window.screen.orientation);
           }
         } else {
           if (!this.video.mozIsOrientationLocked) {
             return;
           }
-          window.screen.mozUnlockOrientation();
+          this.window.screen.mozUnlockOrientation();
           this.video.mozIsOrientationLocked = false;
         }
       },
 
       clickToPlayClickHandler(e) {
         if (e.button != 0) {
           return;
         }
@@ -1423,65 +1424,65 @@
         }
 
         var attrName = muted ? "unmutelabel" : "mutelabel";
         var value = this.muteButton.getAttribute(attrName);
         this.muteButton.setAttribute("aria-label", value);
       },
 
       _getComputedPropertyValueAsInt(element, property) {
-        let value = getComputedStyle(element, null).getPropertyValue(property);
+        let value = this.window.getComputedStyle(element).getPropertyValue(property);
         return parseInt(value, 10);
       },
 
       keyHandler(event) {
         // Ignore keys when content might be providing its own.
         if (!this.video.hasAttribute("controls")) {
           return;
         }
 
         var keystroke = "";
         if (event.altKey) {
           keystroke += "alt-";
         }
         if (event.shiftKey) {
           keystroke += "shift-";
         }
-        if (navigator.platform.startsWith("Mac")) {
+        if (this.window.navigator.platform.startsWith("Mac")) {
           if (event.metaKey) {
             keystroke += "accel-";
           }
           if (event.ctrlKey) {
             keystroke += "control-";
           }
         } else {
           if (event.metaKey) {
             keystroke += "meta-";
           }
           if (event.ctrlKey) {
             keystroke += "accel-";
           }
         }
         switch (event.keyCode) {
-          case KeyEvent.DOM_VK_UP:
+          case this.window.KeyEvent.DOM_VK_UP:
             keystroke += "upArrow";
             break;
-          case KeyEvent.DOM_VK_DOWN:
+          case this.window.KeyEvent.DOM_VK_DOWN:
             keystroke += "downArrow";
             break;
-          case KeyEvent.DOM_VK_LEFT:
+          case this.window.KeyEvent.DOM_VK_LEFT:
             keystroke += "leftArrow";
             break;
-          case KeyEvent.DOM_VK_RIGHT:
+          case this.window.KeyEvent.DOM_VK_RIGHT:
             keystroke += "rightArrow";
             break;
-          case KeyEvent.DOM_VK_HOME:
+          case this.window.KeyEvent.DOM_VK_HOME:
             keystroke += "home";
             break;
-          case KeyEvent.DOM_VK_END:
+          case this.window.KeyEvent.DOM_VK_END:
             keystroke += "end";
             break;
         }
 
         if (String.fromCharCode(event.charCode) == " ") {
           keystroke += "space";
         }
 
@@ -1646,18 +1647,18 @@
             this.changeTextTrack(tt.index);
           }
           return;
         }
 
         tt.index = this.textTracksCount++;
 
         const label = tt.label || "";
-        const ttText = document.createTextNode(label);
-        const ttBtn = document.createElement("button");
+        const ttText = this.document.createTextNode(label);
+        const ttBtn = this.document.createElement("button");
 
         ttBtn.classList.add("textTrackItem");
         ttBtn.setAttribute("index", tt.index);
         ttBtn.appendChild(ttText);
 
         this.textTrackList.appendChild(ttBtn);
 
         if (tt.mode === "showing" && tt.index) {
@@ -1674,22 +1675,22 @@
           }
         }
 
         this.textTrackList.hidden = true;
       },
 
       onControlBarAnimationFinished() {
         this.textTrackList.hidden = true;
-        this.video.dispatchEvent(new CustomEvent("controlbarchange"));
+        this.video.dispatchEvent(new this.window.CustomEvent("controlbarchange"));
         this.adjustControlSize();
       },
 
       toggleCasting() {
-        this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
+        this.videocontrols.dispatchEvent(new this.window.CustomEvent("VideoBindingCast"));
       },
 
       toggleClosedCaption() {
         if (this.textTrackList.hidden) {
           this.textTrackList.hidden = false;
         } else {
           this.textTrackList.hidden = true;
         }
@@ -1711,17 +1712,17 @@
         for (let tti of ttItems) {
           const idx = +tti.getAttribute("index");
 
           if (idx === toRemoveIndex) {
             tti.remove();
             this.textTracksCount--;
           }
 
-          this.video.dispatchEvent(new CustomEvent("texttrackchange"));
+          this.video.dispatchEvent(new this.window.CustomEvent("texttrackchange"));
         }
 
         this.setClosedCaptionButtonState();
       },
 
       initTextTracks() {
         // add 'off' button anyway as new text track might be
         // dynamically added after initialization.
@@ -1748,24 +1749,22 @@
           }
           return false;
         }
         return isDescendant(event.target) && isDescendant(event.relatedTarget);
       },
 
       log(msg) {
         if (this.debug) {
-          console.log("videoctl: " + msg + "\n");
+          this.window.console.log("videoctl: " + msg + "\n");
         }
       },
 
       get isTopLevelSyntheticDocument() {
-        let doc = this.video.ownerDocument;
-        let win = doc.defaultView;
-        return doc.mozSyntheticDocument && win === win.top;
+        return this.document.mozSyntheticDocument && this.window === this.window.top;
       },
 
       controlBarMinHeight: 40,
       controlBarMinVisibleHeight: 28,
       adjustControlSize() {
         const minControlBarPaddingWidth = 18;
 
         this.fullscreenButton.isWanted = !this.controlBar.hasAttribute("fullscreen-unavailable");
@@ -1809,17 +1808,17 @@
 
         // 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.video instanceof HTMLAudioElement) {
+        if (this.video instanceof this.window.HTMLAudioElement) {
           if (givenHeight) {
             // The height of controlBar should be capped with the bounds between controlBarMinHeight
             // and controlBarMinVisibleHeight.
             let controlBarHeight = Math.max(Math.min(givenHeight, this.controlBarMinHeight), this.controlBarMinVisibleHeight);
             this.controlBar.style.height = `${controlBarHeight}px`;
           }
           // Bug 1367875: Set minimum required width to controlBar if the given size is smaller than padding.
           // This can help us expand the control and restore to the default size the next time we need
@@ -1854,75 +1853,78 @@
           if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
             this.clickToPlay.hiddenByAdjustment = false;
           }
           this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
           this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
         }
       },
 
-      init(binding) {
-        this.video = binding.parentNode;
-        this.videocontrols = binding;
+      init(shadowRoot) {
+        this.video = shadowRoot.host;
+        this.videocontrols = shadowRoot.firstChild;
+        this.document = this.videocontrols.ownerDocument;
+        this.window = this.document.defaultView;
+        this.shadowRoot = shadowRoot;
 
-        this.controlsContainer    = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
-        this.statusIcon    = document.getAnonymousElementByAttribute(binding, "anonid", "statusIcon");
-        this.controlBar    = document.getAnonymousElementByAttribute(binding, "anonid", "controlBar");
-        this.playButton    = document.getAnonymousElementByAttribute(binding, "anonid", "playButton");
-        this.controlBarSpacer    = document.getAnonymousElementByAttribute(binding, "anonid", "controlBarSpacer");
-        this.muteButton    = document.getAnonymousElementByAttribute(binding, "anonid", "muteButton");
-        this.volumeStack   = document.getAnonymousElementByAttribute(binding, "anonid", "volumeStack");
-        this.volumeControl = document.getAnonymousElementByAttribute(binding, "anonid", "volumeControl");
-        this.progressBar   = document.getAnonymousElementByAttribute(binding, "anonid", "progressBar");
-        this.bufferBar     = document.getAnonymousElementByAttribute(binding, "anonid", "bufferBar");
-        this.scrubberStack = document.getAnonymousElementByAttribute(binding, "anonid", "scrubberStack");
-        this.scrubber      = document.getAnonymousElementByAttribute(binding, "anonid", "scrubber");
-        this.durationLabel = document.getAnonymousElementByAttribute(binding, "anonid", "durationLabel");
-        this.positionLabel = document.getAnonymousElementByAttribute(binding, "anonid", "positionLabel");
-        this.positionDurationBox   = document.getAnonymousElementByAttribute(binding, "anonid", "positionDurationBox");
-        this.statusOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "statusOverlay");
-        this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "controlsOverlay");
-        this.controlsSpacer     = document.getAnonymousElementByAttribute(binding, "anonid", "controlsSpacer");
-        this.clickToPlay        = document.getAnonymousElementByAttribute(binding, "anonid", "clickToPlay");
-        this.fullscreenButton   = document.getAnonymousElementByAttribute(binding, "anonid", "fullscreenButton");
-        this.castingButton = document.getAnonymousElementByAttribute(binding, "anonid", "castingButton");
-        this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "anonid", "closedCaptionButton");
-        this.textTrackList = document.getAnonymousElementByAttribute(binding, "anonid", "textTrackList");
+        this.controlsContainer = this.shadowRoot.getElementById("controlsContainer");
+        this.statusIcon = this.shadowRoot.getElementById("statusIcon");
+        this.controlBar = this.shadowRoot.getElementById("controlBar");
+        this.playButton = this.shadowRoot.getElementById("playButton");
+        this.controlBarSpacer = this.shadowRoot.getElementById("controlBarSpacer");
+        this.muteButton = this.shadowRoot.getElementById("muteButton");
+        this.volumeStack = this.shadowRoot.getElementById("volumeStack");
+        this.volumeControl = this.shadowRoot.getElementById("volumeControl");
+        this.progressBar = this.shadowRoot.getElementById("progressBar");
+        this.bufferBar = this.shadowRoot.getElementById("bufferBar");
+        this.scrubberStack = this.shadowRoot.getElementById("scrubberStack");
+        this.scrubber = this.shadowRoot.getElementById("scrubber");
+        this.durationLabel = this.shadowRoot.getElementById("durationLabel");
+        this.positionLabel = this.shadowRoot.getElementById("positionLabel");
+        this.positionDurationBox = this.shadowRoot.getElementById("positionDurationBox");
+        this.statusOverlay = this.shadowRoot.getElementById("statusOverlay");
+        this.controlsOverlay = this.shadowRoot.getElementById("controlsOverlay");
+        this.controlsSpacer = this.shadowRoot.getElementById("controlsSpacer");
+        this.clickToPlay = this.shadowRoot.getElementById("clickToPlay");
+        this.fullscreenButton = this.shadowRoot.getElementById("fullscreenButton");
+        this.castingButton = this.shadowRoot.getElementById("castingButton");
+        this.closedCaptionButton = this.shadowRoot.getElementById("closedCaptionButton");
+        this.textTrackList = this.shadowRoot.getElementById("textTrackList");
 
         if (this.positionDurationBox) {
           this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
         }
 
-        let isMobile = navigator.appVersion.includes("Android");
+        let isMobile = this.window.navigator.appVersion.includes("Android");
         if (isMobile) {
           this.controlsContainer.classList.add("mobile");
         }
 
         // TODO: Switch to touch controls on touch-based desktops (bug 1447547)
         this.videocontrols.isTouchControls = isMobile;
         if (this.videocontrols.isTouchControls) {
           this.controlsContainer.classList.add("touch");
         }
 
-        this.controlBarComputedStyles = getComputedStyle(this.controlBar);
+        this.controlBarComputedStyles = this.window.getComputedStyle(this.controlBar);
 
         // Hide and show control in certain order.
         this.prioritizedControls = [
           this.playButton,
           this.muteButton,
           this.fullscreenButton,
           this.castingButton,
           this.closedCaptionButton,
           this.positionDurationBox,
           this.scrubberStack,
           this.durationSpan,
           this.volumeStack
         ];
 
-        this.isAudioOnly = (this.video instanceof HTMLAudioElement);
+        this.isAudioOnly = this.video instanceof this.window.HTMLAudioElement;
         this.setupInitialState();
         this.setupNewLoadState();
         this.initTextTracks();
 
         // Use the handleEvent() callback for all media events.
         // Only the "error" event listener must capture, so that it can trap error
         // events from <source> children, which don't bubble. But we use capture
         // for all events in order to simplify the event listener add/remove.
@@ -1946,17 +1948,17 @@
           { el: this.controlsSpacer, type: "click", nonTouchOnly: true },
           { el: this.controlsSpacer, type: "dblclick", nonTouchOnly: true },
 
           { el: this.textTrackList, type: "click" },
 
           { el: this.videocontrols, type: "resizevideocontrols" },
 
           // See comment at onFullscreenChange on bug 718107.
-          // { el: this.video.ownerDocument, type: "fullscreenchange" },
+          // { el: this.document, type: "fullscreenchange" },
           { el: this.video, type: "keypress", capture: true },
 
           // Prevent any click event within media controls from dispatching through to video.
           { el: this.videocontrols, type: "click", mozSystemGroup: false },
 
           // prevent dragging of controls image (bug 517114)
           { el: this.videocontrols, type: "dragstart" },
 
@@ -2015,25 +2017,24 @@
         if (this.Utils.dynamicControls) {
           this.Utils.startFadeIn(this.Utils.controlBar);
           this.delayHideControls(this.controlsTimeout);
         }
       },
 
       clearTimer() {
         if (this.controlsTimer) {
-          clearTimeout(this.controlsTimer);
+          this.window.clearTimeout(this.controlsTimer);
           this.controlsTimer = null;
         }
       },
 
       delayHideControls(aTimeout) {
         this.clearTimer();
-        this.controlsTimer =
-          setTimeout(() => this.hideControls(), aTimeout);
+        this.controlsTimer = this.window.setTimeout(() => this.hideControls(), aTimeout);
       },
 
       hideControls() {
         if (!this.Utils.dynamicControls) {
           return;
         }
         this.Utils.startFadeOut(this.Utils.controlBar);
       },
@@ -2082,19 +2083,20 @@
           for (let { el, type, mozSystemGroup = true } of this.controlsEvents) {
             el.removeEventListener(type, this, { mozSystemGroup });
           }
         } catch (ex) {}
 
         this.clearTimer();
       },
 
-      init(binding) {
-        this.videocontrols = binding;
-        this.video = binding.parentNode;
+      init(shadowRoot) {
+        this.videocontrols = shadowRoot.firstChild;
+        this.video = shadowRoot.host;
+        this.shadowRoot = shadowRoot;
 
         this.controlsEvents = [
           { el: this.Utils.playButton, type: "click" },
           { el: this.Utils.scrubber, type: "touchstart" },
           { el: this.Utils.scrubber, type: "touchend" },
           { el: this.Utils.muteButton, type: "click" },
           { el: this.Utils.controlsSpacer, type: "mouseup" }
         ];
@@ -2119,76 +2121,139 @@
         // the controls to remain visible. this.controlsTimeout is a full
         // 5s, which feels too long after the transition.
         if (this.video.currentTime !== 0) {
           this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
         }
       }
     };
 
-    this.Utils.init(this);
+    this.Utils.init(this.shadowRoot);
     if (this.isTouchControls) {
-      this.TouchUtils.init(this);
+      this.TouchUtils.init(this.shadowRoot);
     }
-    this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
-    ]]>
-  </constructor>
-  <destructor>
-    <![CDATA[
+    this.shadowRoot.firstChild.dispatchEvent(new this.window.CustomEvent("VideoBindingAttached"));
+
+    this._setupEventListeners();
+  }
+
+  generateContent() {
+    /*
+     * Pass the markup through XML parser purely for the reason of loading the localization DTD.
+     * Remove it when migrate to Fluent.
+     */
+    const parser = new this.window.DOMParser();
+    let parserDoc = parser.parseFromString(`<!DOCTYPE bindings [
+      <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
+      %videocontrolsDTD;
+      ]>
+      <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml">
+        <link rel="stylesheet" type="text/css" href="chrome://global/skin/media/videocontrols.css" />
+        <div id="controlsContainer" class="controlsContainer" role="none">
+          <div id="statusOverlay" class="statusOverlay stackItem" hidden="true">
+            <div id="statusIcon" class="statusIcon"></div>
+            <span class="errorLabel" id="errorAborted">&error.aborted;</span>
+            <span class="errorLabel" id="errorNetwork">&error.network;</span>
+            <span class="errorLabel" id="errorDecode">&error.decode;</span>
+            <span class="errorLabel" id="errorSrcNotSupported">&error.srcNotSupported;</span>
+            <span class="errorLabel" id="errorNoSource">&error.noSource2;</span>
+            <span class="errorLabel" id="errorGeneric">&error.generic;</span>
+          </div>
+
+          <div id="controlsOverlay" class="controlsOverlay stackItem">
+            <div class="controlsSpacerStack">
+              <div id="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
+              <div id="clickToPlay" class="clickToPlay" hidden="true"></div>
+            </div>
+
+            <div id="controlBar" class="controlBar" hidden="true">
+              <button id="playButton"
+                      class="button playButton"
+                      playlabel="&playButton.playLabel;"
+                      pauselabel="&playButton.pauseLabel;"
+                      tabindex="-1"/>
+              <div id="scrubberStack" class="scrubberStack progressContainer" role="none">
+                <div class="progressBackgroundBar stackItem" role="none">
+                  <div class="progressStack" role="none">
+                    <progress id="bufferBar" class="bufferBar" value="0" max="100" tabindex="-1"></progress>
+                    <progress id="progressBar" class="progressBar" value="0" max="100" tabindex="-1"></progress>
+                  </div>
+                </div>
+                <input type="range" id="scrubber" class="scrubber" tabindex="-1"/>
+              </div>
+              <span id="positionLabel" class="positionLabel" role="presentation"></span>
+              <span id="durationLabel" class="durationLabel" role="presentation"></span>
+              <span id="positionDurationBox" class="positionDurationBox" aria-hidden="true">
+                &positionAndDuration.nameFormat;
+              </span>
+              <div id="controlBarSpacer" class="controlBarSpacer" hidden="true" role="none"></div>
+              <button id="muteButton"
+                      class="button muteButton"
+                      mutelabel="&muteButton.muteLabel;"
+                      unmutelabel="&muteButton.unmuteLabel;"
+                      tabindex="-1"/>
+              <div id="volumeStack" class="volumeStack progressContainer" role="none">
+                <input type="range" id="volumeControl" class="volumeControl" min="0" max="100" step="1" tabindex="-1"/>
+              </div>
+              <button id="castingButton" class="button castingButton"
+                      aria-label="&castingButton.castingLabel;"/>
+              <button id="closedCaptionButton" class="button closedCaptionButton"/>
+              <button id="fullscreenButton"
+                      class="button fullscreenButton"
+                      enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
+                      exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
+            </div>
+            <div id="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
+          </div>
+        </div>
+      </div>`, "application/xml");
+    this.shadowRoot.appendChild(this.document.importNode(parserDoc.documentElement, true));
+  }
+
+  destructor() {
     this.Utils.terminate();
     this.TouchUtils.terminate();
     this.Utils.updateOrientationState(false);
     // randomID used to be a <field>, which meant that the XBL machinery
     // undefined the property when the element was unbound. The code in
     // this file actually depends on this, so now that randomID is an
     // expando, we need to make sure to explicitly delete it.
     delete this.randomID;
-    ]]>
-  </destructor>
+  }
 
-  </implementation>
-
-  <handlers>
-    <handler event="mouseover">
+  _setupEventListeners() {
+    this.shadowRoot.firstChild.addEventListener("mouseover", event => {
       if (!this.isTouchControls) {
         this.Utils.onMouseInOut(event);
       }
-    </handler>
-    <handler event="mouseout">
+    });
+
+    this.shadowRoot.firstChild.addEventListener("mouseout", event => {
       if (!this.isTouchControls) {
         this.Utils.onMouseInOut(event);
       }
-    </handler>
-    <handler event="mousemove">
+    });
+
+    this.shadowRoot.firstChild.addEventListener("mousemove", event => {
       if (!this.isTouchControls) {
         this.Utils.onMouseMove(event);
       }
-    </handler>
-  </handlers>
-</binding>
-
-<binding id="noControls">
-
-  <resources>
-    <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
-  </resources>
+    });
+  }
+};
 
-  <xbl:content xmlns="http://www.w3.org/1999/xhtml" class="mediaControlsFrame">
-    <div anonid="controlsContainer" class="controlsContainer" role="none" hidden="true">
-      <div class="controlsOverlay stackItem">
-        <div class="controlsSpacerStack">
-          <div anonid="clickToPlay" class="clickToPlay"></div>
-        </div>
-      </div>
-    </div>
-  </xbl:content>
+this.NoControlsImplPageWidget = class {
+  constructor(shadowRoot) {
+    this.shadowRoot = shadowRoot;
+    this.element = shadowRoot.host;
+    this.document = this.element.ownerDocument;
+    this.window = this.document.defaultView;
 
-  <implementation>
-  <constructor>
-    <![CDATA[
+    this.generateContent();
+
     this.randomID = 0;
     this.Utils = {
       randomID: 0,
       videoEvents: ["play",
                     "playing",
                     "MozNoControlsBlockedVideo"],
       terminate() {
         for (let event of this.videoEvents) {
@@ -2251,26 +2316,31 @@
         } else if (e.button != 0) {
           return;
         }
 
         this.noControlsOverlay.hidden = true;
         this.video.play();
       },
 
-      init(binding) {
-        this.videocontrols = binding;
+      init(shadowRoot) {
+        this.video = shadowRoot.host;
+        this.videocontrols = shadowRoot.firstChild;
+        this.document = this.videocontrols.ownerDocument;
+        this.window = this.document.defaultView;
+        this.shadowRoot = shadowRoot;
+
         this.randomID = Math.random();
         this.videocontrols.randomID = this.randomID;
-        this.video = binding.parentNode;
-        this.controlsContainer = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
-        this.clickToPlay       = document.getAnonymousElementByAttribute(binding, "anonid", "clickToPlay");
-        this.noControlsOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
 
-        let isMobile = navigator.appVersion.includes("Android");
+        this.controlsContainer = this.shadowRoot.getElementById("controlsContainer");
+        this.clickToPlay = this.shadowRoot.getElementById("clickToPlay");
+        this.noControlsOverlay = this.shadowRoot.getElementById("controlsContainer");
+
+        let isMobile = this.window.navigator.appVersion.includes("Android");
         if (isMobile) {
           this.controlsContainer.classList.add("mobile");
         }
 
         // TODO: Switch to touch controls on touch-based desktops (bug 1447547)
         this.videocontrols.isTouchControls = isMobile;
         if (this.videocontrols.isTouchControls) {
           this.controlsContainer.classList.add("touch");
@@ -2281,26 +2351,44 @@
         for (let event of this.videoEvents) {
           this.video.addEventListener(event, this, {
             capture: true,
             mozSystemGroup: true
           });
         }
       }
     };
-    this.Utils.init(this);
-    this.Utils.video.dispatchEvent(new CustomEvent("MozNoControlsVideoBindingAttached"));
-    ]]>
-  </constructor>
-  <destructor>
-    <![CDATA[
+    this.Utils.init(this.shadowRoot);
+    this.Utils.video.dispatchEvent(new this.window.CustomEvent("MozNoControlsVideoBindingAttached"));
+  }
+
+  destructor() {
     this.Utils.terminate();
     // randomID used to be a <field>, which meant that the XBL machinery
     // undefined the property when the element was unbound. The code in
     // this file actually depends on this, so now that randomID is an
     // expando, we need to make sure to explicitly delete it.
     delete this.randomID;
-    ]]>
-  </destructor>
-  </implementation>
-</binding>
+  }
 
-</bindings>
+  generateContent() {
+    /*
+     * Pass the markup through XML parser purely for the reason of loading the localization DTD.
+     * Remove it when migrate to Fluent.
+     */
+    const parser = new this.window.DOMParser();
+    let parserDoc = parser.parseFromString(`<!DOCTYPE bindings [
+      <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
+      %videocontrolsDTD;
+      ]>
+      <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml">
+        <link rel="stylesheet" type="text/css" href="chrome://global/skin/media/videocontrols.css" />
+        <div id="controlsContainer" class="controlsContainer" role="none" hidden="true">
+          <div class="controlsOverlay stackItem">
+            <div class="controlsSpacerStack">
+              <div id="clickToPlay" class="clickToPlay"></div>
+            </div>
+          </div>
+        </div>
+      </div>`, "application/xml");
+    this.shadowRoot.appendChild(this.document.importNode(parserDoc.documentElement, true));
+  }
+};
--- a/toolkit/themes/shared/media/videocontrols.css
+++ b/toolkit/themes/shared/media/videocontrols.css
@@ -1,17 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace url("http://www.w3.org/1999/xhtml");
 
 video > xul|videocontrols,
-audio > xul|videocontrols {
+audio > xul|videocontrols,
+.videocontrols {
   writing-mode: horizontal-tb;
   width: 100%;
   height: 100%;
   display: inline-block;
   overflow: hidden;
 
   direction: ltr;
   /* Prevent unwanted style inheritance. See bug 554717. */