Bug 1444489 - Part IV, Implement Casting UI on videoControls r=Gijs
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 09 Mar 2018 17:39:44 -0800
changeset 409382 2d3ee6cd29b1
parent 409381 05d8a651e3d4
child 409383 6bb4c1e01a69
push id33686
push userapavel@mozilla.com
push dateThu, 22 Mar 2018 09:30:27 +0000
treeherdermozilla-central@f82d56c64966 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1444489
milestone61.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 1444489 - Part IV, Implement Casting UI on videoControls r=Gijs Optimize and re-commit the casting buttons SVG removed from the previous commit. MozReview-Commit-ID: GICxaRZXTiJ
browser/base/content/test/static/browser_parsable_css.js
mobile/android/themes/geckoview/images/videocontrols-cast-active.svg
mobile/android/themes/geckoview/images/videocontrols-cast-ready.svg
toolkit/content/widgets/videocontrols.xml
toolkit/themes/mobile/jar.mn
toolkit/themes/shared/jar.inc.mn
toolkit/themes/shared/media/castingButton-active.svg
toolkit/themes/shared/media/castingButton-ready.svg
toolkit/themes/shared/media/videocontrols.css
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -113,16 +113,18 @@ let propNameWhitelist = [
    isFromDevTools: false},
   // These custom properties are retrieved directly from CSSOM
   // in videocontrols.xml to get pre-defined style instead of computed
   // dimensions, which is why they are not referenced by CSS.
   {propName: "--clickToPlay-width",
    isFromDevTools: false},
   {propName: "--muteButton-width",
    isFromDevTools: false},
+  {propName: "--castingButton-width",
+   isFromDevTools: false},
   {propName: "--closedCaptionButton-width",
    isFromDevTools: false},
   {propName: "--fullscreenButton-width",
    isFromDevTools: false},
   {propName: "--durationSpan-width",
    isFromDevTools: false},
   {propName: "--durationSpan-width-long",
    isFromDevTools: false},
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -61,16 +61,18 @@
           <button anonid="muteButton"
                   class="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="castingButton"
+                  aria-label="&castingButton.castingLabel;"/>
           <button anonid="closedCaptionButton" class="closedCaptionButton"/>
           <button anonid="fullscreenButton"
                   class="fullscreenButton"
                   enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
                   exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
         </div>
         <div anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
       </div>
@@ -1354,31 +1356,61 @@
         event.preventDefault(); // Prevent page scrolling
       },
 
       isSupportedTextTrack(textTrack) {
         return textTrack.kind == "subtitles" ||
                textTrack.kind == "captions";
       },
 
+      get isCastingAvailable() {
+        return !this.isAudioOnly && this.video.mozAllowCasting;
+      },
+
       get isClosedCaptionAvailable() {
         return this.overlayableTextTracks.length;
       },
 
       get overlayableTextTracks() {
         return Array.prototype.filter.call(this.video.textTracks, this.isSupportedTextTrack);
       },
 
       get currentTextTrackIndex() {
         const showingTT = this.overlayableTextTracks.find(tt => tt.mode == "showing");
 
         // fallback to off button if there's no showing track.
         return showingTT ? showingTT.index : 0;
       },
 
+      isCastingOn() {
+        return this.isCastingAvailable && this.video.mozIsCasting;
+      },
+
+      setCastingButtonState() {
+        if (this.isCastingOn()) {
+          this.castingButton.setAttribute("enabled", "true");
+        } else {
+          this.castingButton.removeAttribute("enabled");
+        }
+
+        this.adjustControlSize();
+      },
+
+      updateCasting(eventDetail) {
+        let castingData = JSON.parse(eventDetail);
+        if ("allow" in castingData) {
+          this.video.mozAllowCasting = !!castingData.allow;
+        }
+
+        if ("active" in castingData) {
+          this.video.mozIsCasting = !!castingData.active;
+        }
+        this.setCastingButtonState();
+      },
+
       isClosedCaptionOn() {
         for (let tt of this.overlayableTextTracks) {
           if (tt.mode === "showing") {
             return true;
           }
         }
 
         return false;
@@ -1458,16 +1490,20 @@
       },
 
       onControlBarTransitioned() {
         this.textTrackList.setAttribute("hidden", "true");
         this.video.dispatchEvent(new CustomEvent("controlbarchange"));
         this.adjustControlSize();
       },
 
+      toggleCasting() {
+        this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
+      },
+
       toggleClosedCaption() {
         if (this.textTrackList.hasAttribute("hidden")) {
           this.textTrackList.removeAttribute("hidden");
         } else {
           this.textTrackList.setAttribute("hidden", "true");
         }
       },
 
@@ -1540,16 +1576,17 @@
       },
 
       controlBarMinHeight: 40,
       controlBarMinVisibleHeight: 28,
       adjustControlSize() {
         const minControlBarPaddingWidth = 18;
 
         this.fullscreenButton.isWanted = !this.controlBar.hasAttribute("fullscreen-unavailable");
+        this.castingButton.isWanted = this.isCastingAvailable;
         this.closedCaptionButton.isWanted = this.isClosedCaptionAvailable;
         this.volumeStack.isWanted = !this.muteButton.hasAttribute("noAudio");
 
         let minRequiredWidth = this.prioritizedControls
           .filter(control => control && control.isWanted)
           .reduce((accWidth, cc) => accWidth + cc.minWidth, minControlBarPaddingWidth);
         // Skip the adjustment in case the stylesheets haven't been loaded yet.
         if (!minRequiredWidth) {
@@ -1653,30 +1690,32 @@
         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");
 
         if (this.positionDurationBox) {
           this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
         }
 
         this.controlBarComputedStyles = 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.videocontrols.isTouchControls =
@@ -1700,28 +1739,29 @@
         var self = this;
         this.controlListeners = [];
 
         // Helper function to add an event listener to the given element
         // Due to this helper function, "Utils" is made available to the event
         // listener functions. Hence declare it as a global for ESLint.
         /* global Utils */
         function addListener(elem, eventName, func, {capture = false, mozSystemGroup = true} = {}) {
-          let boundFunc = func.bind(self);
+          let boundFunc = evt => evt.isTrusted && func.call(self, evt);
           self.controlListeners.push({
             item: elem,
             event: eventName,
             func: boundFunc,
             capture,
             mozSystemGroup,
           });
           elem.addEventListener(eventName, boundFunc, {mozSystemGroup, capture});
         }
 
         addListener(this.muteButton, "click", this.toggleMute);
+        addListener(this.castingButton, "click", this.toggleCasting);
         addListener(this.closedCaptionButton, "click", this.toggleClosedCaption);
         addListener(this.fullscreenButton, "click", this.toggleFullscreen);
         addListener(this.playButton, "click", this.clickToPlayClickHandler);
         addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
 
         // On touch videocontrols, tapping controlsSpacer should show/hide
         // the control bar, instead of playing the video or toggle fullscreen.
         if (!this.videocontrols.isTouchControls) {
@@ -1748,16 +1788,21 @@
         // add mouseup listener additionally to handle the case that `change` event
         // isn't fired when the input value before/after dragging are the same. (bug 1328061)
         addListener(this.scrubber, "mouseup", this.onScrubberChange);
         addListener(this.volumeControl, "input", this.updateVolume);
         addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd);
         addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove);
         addListener(this.video.textTracks, "change", this.setClosedCaptionButtonState);
 
+        if (this.videocontrols.isTouchControls) {
+          addListener(this.video, "media-videoCasting",
+            (evt) => this.updateCasting(evt.detail));
+        }
+
         this.log("--- videocontrols initialized ---");
       }
     };
 
     this.TouchUtils = {
       videocontrols: null,
       video: null,
       controlsTimer: null,
--- a/toolkit/themes/mobile/jar.mn
+++ b/toolkit/themes/mobile/jar.mn
@@ -41,16 +41,18 @@ toolkit.jar:
   skin/classic/global/media/pauseButton.svg                (../shared/media/pauseButton.svg)
   skin/classic/global/media/playButton.svg                 (../shared/media/playButton.svg)
   skin/classic/global/media/error.png                      (../shared/media/error.png)
   skin/classic/global/media/throbber.png                   (../shared/media/throbber.png)
   skin/classic/global/media/stalled.png                    (../shared/media/stalled.png)
   skin/classic/global/media/audioMutedButton.svg           (../shared/media/audioMutedButton.svg)
   skin/classic/global/media/audioNoAudioButton.svg         (../shared/media/audioNoAudioButton.svg)
   skin/classic/global/media/audioUnmutedButton.svg         (../shared/media/audioUnmutedButton.svg)
+  skin/classic/global/media/castingButton-ready.svg        (../shared/media/castingButton-ready.svg)
+  skin/classic/global/media/castingButton-active.svg       (../shared/media/castingButton-active.svg)
   skin/classic/global/media/closedCaptionButton-cc-off.svg (../shared/media/closedCaptionButton-cc-off.svg)
   skin/classic/global/media/closedCaptionButton-cc-on.svg  (../shared/media/closedCaptionButton-cc-on.svg)
   skin/classic/global/media/fullscreenEnterButton.svg      (../shared/media/fullscreenEnterButton.svg)
 
 % skin mozapps classic/1.0 %skin/classic/mozapps/
    skin/classic/mozapps/plugins/pluginProblem.css          (mozapps/plugins/pluginProblem.css)
 
    skin/classic/mozapps/plugins/contentPluginActivate.png  (mozapps/plugins/contentPluginActivate.png)
--- a/toolkit/themes/shared/jar.inc.mn
+++ b/toolkit/themes/shared/jar.inc.mn
@@ -67,16 +67,18 @@ toolkit.jar:
   skin/classic/global/reader/RM-Type-Controls-Arrow.svg    (../../shared/reader/RM-Type-Controls-Arrow.svg)
   skin/classic/global/reader/RM-Content-Width-Minus-42x16.svg            (../../shared/reader/RM-Content-Width-Minus-42x16.svg)
   skin/classic/global/reader/RM-Content-Width-Plus-44x16.svg             (../../shared/reader/RM-Content-Width-Plus-44x16.svg)
   skin/classic/global/reader/RM-Line-Height-Minus-38x14.svg            (../../shared/reader/RM-Line-Height-Minus-38x14.svg)
   skin/classic/global/reader/RM-Line-Height-Plus-38x24.svg             (../../shared/reader/RM-Line-Height-Plus-38x24.svg)
   skin/classic/global/media/audioMutedButton.svg           (../../shared/media/audioMutedButton.svg)
   skin/classic/global/media/audioNoAudioButton.svg         (../../shared/media/audioNoAudioButton.svg)
   skin/classic/global/media/audioUnmutedButton.svg         (../../shared/media/audioUnmutedButton.svg)
+  skin/classic/global/media/castingButton-ready.svg        (../../shared/media/castingButton-ready.svg)
+  skin/classic/global/media/castingButton-active.svg       (../../shared/media/castingButton-active.svg)
   skin/classic/global/media/closedCaptionButton-cc-off.svg (../../shared/media/closedCaptionButton-cc-off.svg)
   skin/classic/global/media/closedCaptionButton-cc-on.svg  (../../shared/media/closedCaptionButton-cc-on.svg)
   skin/classic/global/media/fullscreenEnterButton.svg      (../../shared/media/fullscreenEnterButton.svg)
   skin/classic/global/media/fullscreenExitButton.svg       (../../shared/media/fullscreenExitButton.svg)
   skin/classic/global/media/TopLevelImageDocument.css      (../../shared/media/TopLevelImageDocument.css)
   skin/classic/global/media/TopLevelVideoDocument.css      (../../shared/media/TopLevelVideoDocument.css)
   skin/classic/global/media/imagedoc-lightnoise.png        (../../shared/media/imagedoc-lightnoise.png)
   skin/classic/global/media/imagedoc-darknoise.png         (../../shared/media/imagedoc-darknoise.png)
rename from mobile/android/themes/geckoview/images/videocontrols-cast-active.svg
rename to toolkit/themes/shared/media/castingButton-active.svg
--- a/mobile/android/themes/geckoview/images/videocontrols-cast-active.svg
+++ b/toolkit/themes/shared/media/castingButton-active.svg
@@ -1,14 +1,9 @@
-<?xml version="1.0"?>
-<svg width="66px" height="54px" viewBox="0 0 66 54" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g transform="translate(-279.000000, -1435.000000)">
-            <g transform="translate(240.000000, 1390.000000)">
-                <g transform="translate(36.000000, 36.000000)">
-                    <path d="M0,0 L72,0 L72,72 L0,72 L0,0 Z" opacity="0.1"></path>
-                    <path d="M0,0 L72,0 L72,72 L0,72 L0,0 Z"></path>
-                    <path d="M3,54 L3,63 L12,63 C12,58.02 7.98,54 3,54 L3,54 Z M3,42 L3,48 C11.28,48 18,54.72 18,63 L24,63 C24,51.39 14.61,42 3,42 L3,42 Z M57,21 L15,21 L15,25.89 C26.88,29.73 36.27,39.12 40.11,51 L57,51 L57,21 L57,21 Z M3,30 L3,36 C17.91,36 30,48.09 30,63 L36,63 C36,44.76 21.21,30 3,30 L3,30 Z M63,9 L9,9 C5.7,9 3,11.7 3,15 L3,24 L9,24 L9,15 L63,15 L63,57 L42,57 L42,63 L63,63 C66.3,63 69,60.3 69,57 L69,15 C69,11.7 66.3,9 63,9 L63,9 Z" fill="#FFFFFF"></path>
-                </g>
-            </g>
-        </g>
-    </g>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="18px" height="18px" viewBox="0 0 18 18">
+  <path fill="context-fill"
+        d="M0 13.91v2.454h2.455A2.451 2.451 0 0 0 0 13.909zm0-3.274v1.637a4.092 4.092 0 0 1 4.09 4.09h1.637A5.723 5.723 0 0 0 0 10.637zM14.727 4.91H3.273v1.334a10.664 10.664 0 0 1 6.848 6.848h4.606zM0 7.364V9a7.364 7.364 0 0 1 7.364 7.364H9a9 9 0 0 0-9-9zm16.364-5.728H1.636C.736 1.636 0 2.373 0 3.273v2.454h1.636V3.273h14.728v11.454h-5.728v1.637h5.728c.9 0 1.636-.737 1.636-1.637V3.273c0-.9-.736-1.637-1.636-1.637z"
+        fill-rule="evenodd"/>
 </svg>
rename from mobile/android/themes/geckoview/images/videocontrols-cast-ready.svg
rename to toolkit/themes/shared/media/castingButton-ready.svg
--- a/mobile/android/themes/geckoview/images/videocontrols-cast-ready.svg
+++ b/toolkit/themes/shared/media/castingButton-ready.svg
@@ -1,14 +1,9 @@
-<?xml version="1.0"?>
-<svg width="66px" height="54px" viewBox="0 0 66 54" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g transform="translate(-87.000000, -1435.000000)">
-            <g transform="translate(48.000000, 1390.000000)">
-                <g transform="translate(36.000000, 36.000000)">
-                    <path d="M0,0 L72,0 L72,72 L0,72 L0,0 Z" opacity="0.1"></path>
-                    <path d="M0,0 L72,0 L72,72 L0,72 L0,0 Z"></path>
-                    <path d="M63,9 L9,9 C5.7,9 3,11.7 3,15 L3,24 L9,24 L9,15 L63,15 L63,57 L42,57 L42,63 L63,63 C66.3,63 69,60.3 69,57 L69,15 C69,11.7 66.3,9 63,9 L63,9 Z M3,54 L3,63 L12,63 C12,58.02 7.98,54 3,54 L3,54 Z M3,42 L3,48 C11.28,48 18,54.72 18,63 L24,63 C24,51.39 14.61,42 3,42 L3,42 Z M3,30 L3,36 C17.91,36 30,48.09 30,63 L36,63 C36,44.76 21.21,30 3,30 L3,30 Z" fill="#FFFFFF"></path>
-                </g>
-            </g>
-        </g>
-    </g>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="18px" height="18px" viewBox="0 0 18 18">
+  <path fill="context-fill"
+        d="M16.364 1.636H1.636C.736 1.636 0 2.373 0 3.273v2.454h1.636V3.273h14.728v11.454h-5.728v1.637h5.728c.9 0 1.636-.737 1.636-1.637V3.273c0-.9-.736-1.637-1.636-1.637zM0 13.91v2.455h2.455A2.451 2.451 0 0 0 0 13.909zm0-3.273v1.637a4.092 4.092 0 0 1 4.09 4.09h1.637A5.723 5.723 0 0 0 0 10.637zm0-3.272V9a7.364 7.364 0 0 1 7.364 7.364H9a9 9 0 0 0-9-9z"
+        fill-rule="evenodd" />
 </svg>
--- a/toolkit/themes/shared/media/videocontrols.css
+++ b/toolkit/themes/shared/media/videocontrols.css
@@ -30,19 +30,19 @@ audio > xul|videocontrols {
   /* Do not delete: these variables are accessed by JavaScript directly.
      see videocontrols.xml and search for |-width|. */
   --clickToPlay-width: var(--clickToPlay-size);
   --playButton-width: 30px;
   --playButton-height: var(--playButton-width);
   --scrubberStack-width: 64px;
   --muteButton-width: 30px;
   --volumeStack-width: 48px;
+  --castingButton-width: 30px;
   --closedCaptionButton-width: 30px;
   --fullscreenButton-width: 30px;
-
   --positionDurationBox-width: 40px;
   --durationSpan-width: 40px;
   --positionDurationBox-width-long: 60px;
   --durationSpan-width-long: 60px;
 }
 
 .controlsContainer [hidden="true"],
 .controlBar[hidden] {
@@ -103,16 +103,17 @@ audio > xul|videocontrols {
   overflow: hidden;
   height: 40px;
   padding: 0 9px;
   background-color: rgba(26,26,26,.8);
 }
 
 .playButton,
 .muteButton,
+.castingButton,
 .closedCaptionButton,
 .fullscreenButton {
   height: 100%;
   min-width: var(--playButton-width);
   min-height: var(--playButton-height);
   padding: 6px;
   border: 0;
   margin: 0;
@@ -122,23 +123,25 @@ audio > xul|videocontrols {
   background-origin: content-box;
   background-clip: content-box;
   -moz-context-properties: fill;
   fill: #ffffff;
 }
 
 .playButton:hover,
 .muteButton:hover,
+.castingButton:hover,
 .closedCaptionButton:hover,
 .fullscreenButton:hover {
   fill: #48a0f7;
 }
 
 .playButton:hover:active,
 .muteButton:hover:active,
+.castingButton:hover:active,
 .closedCaptionButton:hover:active,
 .fullscreenButton:hover:active {
   fill: #2d89e6;
 }
 
 .playButton {
   background-image: url(chrome://global/skin/media/pauseButton.svg);
 }
@@ -157,16 +160,24 @@ audio > xul|videocontrols {
 .muteButton[noAudio]:hover:active {
   background-image: url(chrome://global/skin/media/audioNoAudioButton.svg);
   fill: white;
 }
 .muteButton[noAudio] + .volumeStack {
   display: none;
 }
 
+.castingButton {
+  background-image: url(chrome://global/skin/media/castingButton-ready.svg);
+}
+
+.castingButton[enabled] {
+  background-image: url(chrome://global/skin/media/castingButton-active.svg);
+}
+
 .closedCaptionButton {
   background-image: url(chrome://global/skin/media/closedCaptionButton-cc-off.svg);
 }
 .closedCaptionButton[enabled] {
   background-image: url(chrome://global/skin/media/closedCaptionButton-cc-on.svg);
 }
 
 .fullscreenButton {