Bug 1431255 - Part IV, Load videocontrols.js, migrated from videoControls binding draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Wed, 27 Jun 2018 11:55:38 -0700
changeset 829409 82f0506e69de1d1d3dcc1f20c10d1f372c012b77
parent 829408 30f77c9ed6ec3a88cd95602f8b7f31210a42b685
child 829410 331b637785390c0b82582685bc635aae1e3a60fb
push id118779
push usertimdream@gmail.com
push dateWed, 15 Aug 2018 23:50:47 +0000
bugs1431255
milestone63.0a1
Bug 1431255 - Part IV, Load videocontrols.js, migrated from videoControls binding 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. */