Bug 1444489 - Part IV, Implement Casting UI on videoControls draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 09 Mar 2018 17:39:44 -0800
changeset 770603 51ab81f98e43
parent 770602 07ca36eb8c65
child 770604 9e6a5f9fbde7
push id103447
push usertimdream@gmail.com
push dateWed, 21 Mar 2018 15:57:15 +0000
bugs1444489
milestone61.0a1
Bug 1444489 - Part IV, Implement Casting UI on videoControls 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 {