Bug 1543122 - Add the simple Picture-in-Picture toggle to the <video controls/> binding, still preffed off by default. r=jaws
authorMike Conley <mconley@mozilla.com>
Mon, 15 Apr 2019 01:07:53 +0000
changeset 469458 bd90178236758c38c1a9ed11b753d3c2eee25c1b
parent 469457 fbb2191cafbe3fc90a78e197a7d98af62bc95ac8
child 469459 7900dcbab822a5078491838b5fae04d66c20e975
push id112792
push userncsoregi@mozilla.com
push dateMon, 15 Apr 2019 09:49:11 +0000
treeherdermozilla-inbound@a57f27d3ccd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1543122
milestone68.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 1543122 - Add the simple Picture-in-Picture toggle to the <video controls/> binding, still preffed off by default. r=jaws This also stops the PictureInPictureToggleChild from tracking videos with controls for now. Depends on D26777 Differential Revision: https://phabricator.services.mozilla.com/D26778
toolkit/actors/PictureInPictureChild.jsm
toolkit/content/widgets/videocontrols.js
toolkit/themes/shared/media/videocontrols.css
--- a/toolkit/actors/PictureInPictureChild.jsm
+++ b/toolkit/actors/PictureInPictureChild.jsm
@@ -103,16 +103,17 @@ class PictureInPictureToggleChild extend
     return state;
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "canplay": {
         if (this.toggleEnabled &&
             event.target instanceof this.content.HTMLVideoElement &&
+            !event.target.controls &&
             event.target.ownerDocument == this.content.document) {
           this.registerVideo(event.target);
         }
         break;
       }
       case "click": {
         let state = this.docState;
         let clickedFlyout = state.flyoutToggle &&
--- a/toolkit/content/widgets/videocontrols.js
+++ b/toolkit/content/widgets/videocontrols.js
@@ -262,16 +262,20 @@ this.VideoControlsImplWidget = class {
           this.startFadeOut(this.clickToPlay, true);
           this.statusIcon.setAttribute("type", "error");
           this.updateErrorText();
           this.setupStatusFader(true);
         } else if (VideoControlsWidget.isPictureInPictureVideo(this.video)) {
           this.setShowPictureInPictureMessage(true);
         }
 
+        if (!this.pipToggleEnabled || this.isShowingPictureInPictureMessage) {
+          this.pictureInPictureToggleButton.setAttribute("hidden", true);
+        }
+
         let adjustableControls = [
           ...this.prioritizedControls,
           this.controlBar,
           this.clickToPlay,
         ];
 
         let throwOnGet = {
           get() {
@@ -664,16 +668,19 @@ this.VideoControlsImplWidget = class {
               case this.textTrackList:
                 const index = +aEvent.originalTarget.getAttribute("index");
                 this.changeTextTrack(index);
                 break;
               case this.videocontrols:
                 // Prevent any click event within media controls from dispatching through to video.
                 aEvent.stopPropagation();
                 break;
+              case this.pictureInPictureToggleButton:
+                this.video.togglePictureInPicture();
+                break;
             }
             break;
           case "dblclick":
             this.toggleFullscreen();
             break;
           case "resizevideocontrols":
             // Since this event come from the layout, this is the only place
             // we are sure of that probing into layout won't trigger or force
@@ -1977,16 +1984,18 @@ this.VideoControlsImplWidget = class {
         this.controlsOverlay = this.shadowRoot.getElementById("controlsOverlay");
         this.pictureInPictureOverlay = this.shadowRoot.getElementById("pictureInPictureOverlay");
         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");
+        this.pictureInPictureToggleButton =
+          this.shadowRoot.getElementById("pictureInPictureToggleButton");
 
         if (this.positionDurationBox) {
           this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
         }
 
         let isMobile = this.window.navigator.appVersion.includes("Android");
         if (isMobile) {
           this.controlsContainer.classList.add("mobile");
@@ -2064,16 +2073,18 @@ this.VideoControlsImplWidget = class {
           // isn't fired when the input value before/after dragging are the same. (bug 1328061)
           { el: this.scrubber, type: "mouseup" },
           { el: this.volumeControl, type: "input" },
           { el: this.video.textTracks, type: "addtrack" },
           { el: this.video.textTracks, type: "removetrack" },
           { el: this.video.textTracks, type: "change" },
 
           { el: this.video, type: "media-videoCasting", touchOnly: true },
+
+          { el: this.pictureInPictureToggleButton, type: "click" },
         ];
 
         for (let { el, type, nonTouchOnly = false, touchOnly = false,
                    mozSystemGroup = true, capture = false } of this.controlsEvents) {
           if ((this.isTouchControls && nonTouchOnly) ||
               (!this.isTouchControls && touchOnly)) {
             continue;
           }
@@ -2255,16 +2266,20 @@ this.VideoControlsImplWidget = class {
           </div>
 
           <div id="controlsOverlay" class="controlsOverlay stackItem" role="none">
             <div class="controlsSpacerStack">
               <div id="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
               <div id="clickToPlay" class="clickToPlay" hidden="true"></div>
             </div>
 
+            <button id="pictureInPictureToggleButton" class="pictureInPictureToggleButton">
+              <div id="pictureInPictureToggleIcon" class="pictureInPictureToggleIcon"></div>
+            </button>
+
             <div id="controlBar" class="controlBar" role="none" 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">
--- a/toolkit/themes/shared/media/videocontrols.css
+++ b/toolkit/themes/shared/media/videocontrols.css
@@ -23,16 +23,20 @@
 .controlsContainer {
   --clickToPlay-size: 48px;
   --button-size: 30px;
   --timer-size: 40px;
   --timer-long-size: 60px;
   --track-size: 5px;
   --thumb-size: 13px;
   --label-font-size: 13px;
+  --pip-toggle-bgcolor: rgb(0, 96, 223);
+  --pip-toggle-text-and-icon-color: rgb(255, 255, 255);
+  --pip-toggle-padding: 5px;
+  --pip-toggle-icon-width-height: 16px;
 }
 .controlsContainer.touch {
   --clickToPlay-size: 64px;
   --button-size: 40px;
   --timer-size: 52px;
   --timer-long-size: 78px;
   --track-size: 7px;
   --thumb-size: 16px;
@@ -60,17 +64,18 @@
 .touch .controlBar {
   /* Do not delete: these variables are accessed by JavaScript directly.
      see videocontrols.js and search for |-width|. */
   --scrubberStack-width: 84px;
   --volumeStack-width: 64px;
 }
 
 .controlsContainer [hidden],
-.controlBar[hidden] {
+.controlBar[hidden],
+.pictureInPictureToggleButton[hidden] {
   display: none;
 }
 
 .controlBar[size="hidden"] {
   display: none;
 }
 
 .controlsSpacer[hideCursor] {
@@ -428,16 +433,54 @@
   min-width: 84px;
   min-height: 84px;
   background-image: url(chrome://global/skin/media/pictureinpicture.svg);
   -moz-context-properties: fill, stroke;
   fill: #fff;
   stroke: #fff;
 }
 
+.pictureInPictureToggleButton {
+  display: flex;
+  -moz-appearance: none;
+  position: absolute;
+  background-color: var(--pip-toggle-bgcolor);
+  color: var(--pip-toggle-text-and-icon-color);
+  border: 0;
+  padding: var(--pip-toggle-padding);
+  right: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  transition: opacity 160ms linear;
+  min-width: max-content;
+  pointer-events: auto;
+  opacity: 0;
+}
+
+.pictureInPictureToggleIcon {
+  display: inline-block;
+  background-image: url(chrome://global/skin/media/pictureinpicture.svg);
+  background-position: center left;
+  background-repeat: no-repeat;
+  -moz-context-properties: fill, stroke;
+  fill: var(--pip-toggle-text-and-icon-color);
+  stroke: var(--pip-toggle-text-and-icon-color);
+  width: var(--pip-toggle-icon-width-height);
+  height: var(--pip-toggle-icon-width-height);
+  min-width: max-content;
+}
+
+.controlsOverlay:hover > .pictureInPictureToggleButton {
+  opacity: 0.8;
+}
+
+.controlsOverlay:hover > .pictureInPictureToggleButton:hover {
+  opacity: 1;
+}
+
 /* Overlay Play button */
 .clickToPlay {
   min-width: var(--clickToPlay-size);
   min-height: var(--clickToPlay-size);
   border-radius: 50%;
   background-image: url(chrome://global/skin/media/playButton.svg);
   background-repeat: no-repeat;
   background-position: 54% 50%;