Bug 887934 - Add closed caption button to video control. r?jaws draft
authorRay Lin <ralin@mozilla.com>
Tue, 07 Jun 2016 15:07:03 +0800
changeset 380512 bb26bbc20e3530780eb720592c73a9785f8fd7d6
parent 380354 51377a64158941f89ed73f388ae437cfa494c030
child 523741 41e5fb2e20c11bf91e42a827a742e97e1cae3264
push id21236
push userbmo:ralin@mozilla.com
push dateWed, 22 Jun 2016 05:29:33 +0000
reviewersjaws
bugs887934
milestone50.0a1
Bug 887934 - Add closed caption button to video control. r?jaws MozReview-Commit-ID: JykXcNBkSLr
toolkit/content/widgets/videocontrols.xml
toolkit/locales/en-US/chrome/global/videocontrols.dtd
toolkit/themes/osx/global/jar.mn
toolkit/themes/osx/global/media/closeCaptionButton.png
toolkit/themes/osx/global/media/closeCaptionButton@2x.png
toolkit/themes/osx/global/media/videocontrols.css
toolkit/themes/shared/non-mac.jar.inc.mn
toolkit/themes/windows/global/media/closeCaptionButton.png
toolkit/themes/windows/global/media/closeCaptionButton@2x.png
toolkit/themes/windows/global/media/videocontrols.css
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -215,16 +215,17 @@
                 <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
                 <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
             </vbox>
 
             <vbox class="controlsOverlay">
                 <stack flex="1">
                     <spacer class="controlsSpacer" flex="1"/>
                     <box class="clickToPlay" hidden="true" flex="1"/>
+                    <vbox class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
                 </stack>
                 <hbox class="controlBar" hidden="true">
                     <button class="playButton"
                             playlabel="&playButton.playLabel;"
                             pauselabel="&playButton.pauseLabel;"/>
                     <stack class="scrubberStack" flex="1">
                         <box class="backgroundBar"/>
                         <progressmeter class="bufferBar"/>
@@ -238,16 +239,17 @@
                     <button class="muteButton"
                             mutelabel="&muteButton.muteLabel;"
                             unmutelabel="&muteButton.unmuteLabel;"/>
                     <stack class="volumeStack">
                       <box class="volumeBackground"/>
                       <box class="volumeForeground" anonid="volumeForeground"/>
                       <scale class="volumeControl" movetoclick="true"/>
                     </stack>
+                    <button class="closedCaptionButton"/>
                     <button class="fullscreenButton"
                             enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
                             exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
                 </hbox>
             </vbox>
         </stack>
     </xbl:content>
 
@@ -272,17 +274,19 @@
                 scrubber       : null,
                 progressBar    : null,
                 bufferBar      : null,
                 statusOverlay  : null,
                 controlsSpacer : null,
                 clickToPlay    : null,
                 controlsOverlay : null,
                 fullscreenButton : null,
+                currentTextTrackIndex: 0,
 
+                textTracksCount: 0,
                 randomID : 0,
                 videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
                                "loadstart", "timeupdate", "progress",
                                "playing", "waiting", "canplay", "canplaythrough",
                                "seeking", "seeked", "emptied", "loadedmetadata",
                                "error", "suspend", "stalled",
                                "mozinterruptbegin", "mozinterruptend" ],
 
@@ -407,16 +411,17 @@
                     }
 
                     // An event handler for |onresize| should be added when bug 227495 is fixed.
                     this.controlBar.hidden = false;
                     this._playButtonWidth = this.playButton.clientWidth;
                     this._durationLabelWidth = this.durationLabel.clientWidth;
                     this._muteButtonWidth = this.muteButton.clientWidth;
                     this._volumeControlWidth = this.volumeControl.clientWidth;
+                    this._closedCaptionButtonWidth = this.closedCaptionButton.clientWidth;
                     this._fullscreenButtonWidth = this.fullscreenButton.clientWidth;
                     this._controlBarHeight = this.controlBar.clientHeight;
                     this.controlBar.hidden = true;
                     this.adjustControlSize();
 
                     // Can only update the volume controls once we've computed
                     // _volumeControlWidth, since the volume slider implementation
                     // depends on it.
@@ -902,16 +907,17 @@
                     if (!isMouseOver && !isMouseInControls) {
                         this.adjustControlSize();
 
                         // Keep the controls visible if the click-to-play is visible.
                         if (!this.clickToPlay.hidden)
                             return;
 
                         this.startFadeOut(this.controlBar, false);
+                        this.textTrackList.setAttribute("hidden", "true");
                         clearTimeout(this._showControlsTimeout);
                         Utils._controlsHiddenByTimeout = false;
                     }
                 },
 
                 startFadeIn : function (element, immediate) {
                     this.startFade(element, true, immediate);
                 },
@@ -1211,16 +1217,192 @@
                             default:
                                 return;
                         }
                     } catch(e) { /* ignore any exception from setting .currentTime */ }
 
                     event.preventDefault(); // Prevent page scrolling
                 },
 
+                get videoSubtitles() {
+                  return Array.prototype.filter.call(this.video.textTracks, function (tt) {
+                    return tt.kind === "subtitles";
+                  });
+                },
+
+                isClosedCaptionOn : function () {
+                  for (let tt of this.videoSubtitles) {
+                    if (tt.mode === "showing") {
+                      return true;
+                    }
+                  }
+
+                  return false;
+                },
+
+                setClosedCaptionButtonState : function () {
+                  if (!this.videoSubtitles.length || this.videocontrols.isTouchControl) {
+                    this.closedCaptionButton.setAttribute("hidden", "true");
+                    return;
+                  }
+
+                  this.closedCaptionButton.removeAttribute("hidden");
+
+                  if (this.isClosedCaptionOn()) {
+                    this.closedCaptionButton.setAttribute("enabled", "true");
+                  } else {
+                    this.closedCaptionButton.removeAttribute("enabled");
+                  }
+
+                  let ttItems = this.textTrackList.childNodes;
+
+                  for (let tti of ttItems) {
+                    const idx = +tti.getAttribute("index");
+
+                    if (idx == this.currentTextTrackIndex) {
+                      tti.setAttribute("on", "true");
+                    } else {
+                      tti.removeAttribute("on");
+                    }
+                  }
+                },
+
+                addNewTextTrack : function (tt) {
+                  if (tt.kind !== "subtitles") {
+                    return;
+                  }
+
+                  if (tt.index && tt.index < this.textTracksCount) {
+                    return;
+                  }
+
+                  tt.index = this.textTracksCount++;
+
+                  const label = tt.label || "";
+                  const ttText = document.createTextNode(label);
+                  const ttBtn = document.createElement("button");
+
+                  ttBtn.classList.add("textTrackItem");
+                  ttBtn.setAttribute("index", tt.index);
+
+                  ttBtn.addEventListener("click", function (event) {
+                    event.stopPropagation();
+
+                    this.changeTextTrack(tt.index);
+                  }.bind(this));
+
+                  ttBtn.appendChild(ttText);
+
+                  this.textTrackList.appendChild(ttBtn);
+
+                  if (tt.mode === "showing" && tt.index) {
+                    this.changeTextTrack(tt.index);
+                  }
+                },
+
+                changeTextTrack : function (index) {
+                  for (let tt of this.videoSubtitles) {
+                    if (tt.index === index) {
+                      tt.mode = "showing";
+
+                      this.currentTextTrackIndex = tt.index;
+                    } else {
+                      tt.mode = "disabled";
+                    }
+                  }
+
+                  // should fallback to off
+                  if (this.currentTextTrackIndex !== index) {
+                    this.currentTextTrackIndex = 0;
+                  }
+
+                  this.textTrackList.setAttribute("hidden", "true");
+                  this.setClosedCaptionButtonState();
+                },
+
+                onControlBarTransitioned : function () {
+                  this.textTrackList.setAttribute("hidden", "true");
+                },
+
+                toggleClosedCaption : function () {
+                  if (this.videoSubtitles.length === 1) {
+                    const lastTTIdx = this.videoSubtitles[0].index;
+
+                    return this.changeTextTrack(this.isClosedCaptionOn() ? 0 : lastTTIdx);
+                  }
+
+                  if (this.textTrackList.hasAttribute("hidden")) {
+                    this.textTrackList.removeAttribute("hidden");
+                  } else {
+                    this.textTrackList.setAttribute("hidden", "true");
+                  }
+
+                  let maxButtonWidth = 0;
+
+                  for (let tti of this.textTrackList.childNodes) {
+                    if (tti.clientWidth > maxButtonWidth) {
+                      maxButtonWidth = tti.clientWidth;
+                    }
+                  }
+
+                  if (maxButtonWidth > this.video.clientWidth) {
+                    maxButtonWidth = this.video.clientWidth;
+                  }
+
+                  for (let tti of this.textTrackList.childNodes) {
+                    tti.style.width = maxButtonWidth + "px";
+                  }
+                },
+
+                onTextTrackAdd : function (trackEvent) {
+                  this.addNewTextTrack(trackEvent.track);
+                  this.setClosedCaptionButtonState();
+                },
+
+                onTextTrackRemove : function (trackEvent) {
+                  const toRemoveIndex = trackEvent.track.index;
+                  const ttItems = this.textTrackList.childNodes;
+
+                  if (!ttItems) {
+                    return;
+                  }
+
+                  for (let tti of ttItems) {
+                    const idx = +tti.getAttribute("index");
+
+                    if (idx === toRemoveIndex) {
+                      tti.remove();
+                      this.textTracksCount--;
+                    }
+
+                    if (idx === this.currentTextTrackIndex) {
+                      this.currentTextTrackIndex = 0;
+
+                      this.video.dispatchEvent(new CustomEvent("texttrackchange"));
+                    }
+                  }
+
+                  this.setClosedCaptionButtonState();
+                },
+
+                initTextTracks : function () {
+                  const offLabel = this.textTrackList.getAttribute("offlabel");
+
+                  this.addNewTextTrack({
+                    label: offLabel,
+                    kind: "subtitles"
+                  });
+
+                  for (let tt of this.videoSubtitles) {
+                    this.addNewTextTrack(tt);
+                  }
+
+                  this.setClosedCaptionButtonState();
+                },
+
                 isEventWithin : function (event, parent1, parent2) {
                     function isDescendant (node) {
                         while (node) {
                             if (node == parent1 || node == parent2)
                                 return true;
                             node = node.parentNode;
                         }
                         return false;
@@ -1238,32 +1420,34 @@
                   let win = doc.defaultView;
                   return doc.mozSyntheticDocument && win === win.top;
                 },
 
                 _playButtonWidth : 0,
                 _durationLabelWidth : 0,
                 _muteButtonWidth : 0,
                 _volumeControlWidth : 0,
+                _closedCaptionButtonWidth : 0,
                 _fullscreenButtonWidth : 0,
                 _controlBarHeight : 0,
                 _overlayPlayButtonHeight : 64,
                 _overlayPlayButtonWidth : 64,
                 _volumeStackMarginEnd : 8,
                 adjustControlSize : function adjustControlSize() {
                     let doc = this.video.ownerDocument;
 
                     // The scrubber has |flex=1|, therefore |minScrubberWidth|
                     // was generated by empirical testing.
                     let minScrubberWidth = 25;
                     let minWidthAllControls = this._playButtonWidth +
                                               minScrubberWidth +
                                               this._durationLabelWidth +
                                               this._muteButtonWidth +
                                               this._volumeControlWidth +
+                                              this._closedCaptionButtonWidth +
                                               this._fullscreenButtonWidth;
 
                     let isFullscreenUnavailable = this.controlBar.hasAttribute("fullscreen-unavailable");
                     if (isFullscreenUnavailable) {
                         // When the fullscreen button is hidden we add margin-end to the volume stack.
                         minWidthAllControls -= this._fullscreenButtonWidth - this._volumeStackMarginEnd;
                     }
 
@@ -1331,54 +1515,61 @@
                     this.durationLabel = document.getAnonymousElementByAttribute(binding, "class", "durationLabel");
                     this.positionLabel = document.getAnonymousElementByAttribute(binding, "class", "positionLabel");
                     this.statusOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
                     this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "controlsOverlay");
                     this.controlsSpacer     = document.getAnonymousElementByAttribute(binding, "class", "controlsSpacer");
                     this.clickToPlay        = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
                     this.fullscreenButton   = document.getAnonymousElementByAttribute(binding, "class", "fullscreenButton");
                     this.volumeForeground   = document.getAnonymousElementByAttribute(binding, "anonid", "volumeForeground");
+                    this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "class", "closedCaptionButton");
+                    this.textTrackList = document.getAnonymousElementByAttribute(binding, "class", "textTrackList");
 
                     this.isAudioOnly = (this.video instanceof 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.
                     for (let event of this.videoEvents) {
                         this.video.addEventListener(event, this, {
                             capture: true,
                             mozSystemGroup: true
                         });
                     }
 
                     var self = this;
-
                     this.controlListeners = [];
 
                     // Helper function to add an event listener to the given element
                     function addListener(elem, eventName, func) {
                       let boundFunc = func.bind(self);
                       self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
                       elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
                     }
 
                     addListener(this.muteButton, "command", this.toggleMute);
+                    addListener(this.closedCaptionButton, "command", this.toggleClosedCaption);
                     addListener(this.playButton, "click", this.clickToPlayClickHandler);
                     addListener(this.fullscreenButton, "command", this.toggleFullscreen);
                     addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
                     addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
                     addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
 
                     addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
                     addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
+                    addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
+                    addListener(this.videocontrols, "transitionend", this.onControlBarTransitioned);
                     addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
                     addListener(this.video, "keypress", this.keyHandler);
+                    addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd);
+                    addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove);
 
                     addListener(this.videocontrols, "dragstart", function(event) {
                         event.preventDefault(); //prevent dragging of controls image (bug 517114)
                     });
 
                     this.log("--- videocontrols initialized ---");
                 }
             };
@@ -1427,16 +1618,17 @@
                 <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
                 <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
             </vbox>
 
             <vbox class="controlsOverlay">
                 <spacer class="controlsSpacer" flex="1"/>
                 <box flex="1" hidden="true">
                     <box class="clickToPlay" hidden="true" flex="1"/>
+                    <vbox class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
                 </box>
                 <vbox class="controlBar" hidden="true">
                     <hbox class="buttonsBar">
                         <button class="playButton"
                                 playlabel="&playButton.playLabel;"
                                 pauselabel="&playButton.pauseLabel;"/>
                         <label class="positionLabel" role="presentation"/>
                         <stack class="scrubberStack">
@@ -1452,16 +1644,17 @@
                                 unmutelabel="&muteButton.unmuteLabel;"/>
                         <stack class="volumeStack">
                           <box class="volumeBackground"/>
                           <box class="volumeForeground" anonid="volumeForeground"/>
                           <scale class="volumeControl" movetoclick="true"/>
                         </stack>
                         <button class="castingButton" hidden="true"
                                 aria-label="&castingButton.castingLabel;"/>
+                        <button class="closedCaptionButton" hidden="true"/>
                         <button class="fullscreenButton"
                             enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
                             exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
                     </hbox>
                 </vbox>
             </vbox>
         </stack>
     </xbl:content>
--- a/toolkit/locales/en-US/chrome/global/videocontrols.dtd
+++ b/toolkit/locales/en-US/chrome/global/videocontrols.dtd
@@ -4,16 +4,17 @@
 
 <!ENTITY playButton.playLabel "Play">
 <!ENTITY playButton.pauseLabel "Pause">
 <!ENTITY muteButton.muteLabel "Mute">
 <!ENTITY muteButton.unmuteLabel "Unmute">
 <!ENTITY fullscreenButton.enterfullscreenlabel "Full Screen">
 <!ENTITY fullscreenButton.exitfullscreenlabel "Exit Full Screen">
 <!ENTITY castingButton.castingLabel "Cast to Screen">
+<!ENTITY closedCaption.off "Off">
 
 <!ENTITY stats.media "Media">
 <!ENTITY stats.size "Size">
 <!ENTITY stats.activity "Activity">
 <!ENTITY stats.activityPaused "Paused">
 <!ENTITY stats.activityPlaying "Playing">
 <!ENTITY stats.activityEnded "Ended">
 <!ENTITY stats.activitySeeking "(seeking)">
--- a/toolkit/themes/osx/global/jar.mn
+++ b/toolkit/themes/osx/global/jar.mn
@@ -142,16 +142,18 @@ toolkit.jar:
   skin/classic/global/media/playButton.png                           (media/playButton.png)
   skin/classic/global/media/playButton@2x.png                        (media/playButton@2x.png)
   skin/classic/global/media/muteButton.png                           (media/muteButton.png)
   skin/classic/global/media/muteButton@2x.png                        (media/muteButton@2x.png)
   skin/classic/global/media/unmuteButton.png                         (media/unmuteButton.png)
   skin/classic/global/media/unmuteButton@2x.png                      (media/unmuteButton@2x.png)
   skin/classic/global/media/noAudio.png                              (media/noAudio.png)
   skin/classic/global/media/noAudio@2x.png                           (media/noAudio@2x.png)
+  skin/classic/global/media/closeCaptionButton.png                   (media/closeCaptionButton.png)
+  skin/classic/global/media/closeCaptionButton@2x.png                (media/closeCaptionButton@2x.png)
   skin/classic/global/media/fullscreenButton.png                     (media/fullscreenButton.png)
   skin/classic/global/media/fullscreenButton@2x.png                  (media/fullscreenButton@2x.png)
   skin/classic/global/media/scrubberThumb.png                        (media/scrubberThumb.png)
   skin/classic/global/media/scrubberThumb@2x.png                     (media/scrubberThumb@2x.png)
   skin/classic/global/media/scrubberThumbWide.png                    (media/scrubberThumbWide.png)
   skin/classic/global/media/scrubberThumbWide@2x.png                 (media/scrubberThumbWide@2x.png)
   skin/classic/global/media/error.png                                (media/error.png)
   skin/classic/global/media/throbber.png                             (media/throbber.png)
new file mode 100644
index 0000000000000000000000000000000000000000..469310fb1b21ed705925decd18710dd0df37d915
GIT binary patch
literal 583
zc$@)80=WH&P)<h;3K|Lk000e1NJLTq000~S000~a1ONa4*aR0J0006GNkl<ZcmeH_
zKWGzS7{;HWWDq2_UZH9T=^)YuOG`9SDy8)=Hh<duyKu?feMb@vv1wYdT)?42a1mWy
zTy=DE5phU6NWoM~#X=Mp!6_6AHa%!kdA$qS@=;fZKELI?=jF!@-;1OCpRw&0>G9$0
z7GXnI1j>_Uv8{b)8|;(1qO2(E=JZXgTfIdd0=nWPM(pX`zAR)U5JLpBCJ}Q=AZ5=!
z$ys|i(G?e|4R$<a_f*rZ^4{{h2pl-`I<U9gDu2$Mx^OJ`n~@3^PgFKljC>4Jv7t6K
zdhlxO<$2N%v>ir;FA_%987X{GUen<IHP=)e*=re+-)n0c$;piU&W^I3huiw%i^&Re
z_=weHO!QpopFVkgGeb#l0KQ)Tz$DxiMoGRXt*WQTN&2c_(9xG0zU=*=k#y6CUisX;
zA-GmmlK7%nRdMNu*i`&Ol~H=Q!Cs_De10oF&?*`87_6!!^2KN^1y5O|*nX*bpQ2Lm
zMx3I5ibl!Q18{GpD9RVZ2h#9Z&-kN6J@zv;C&Gt|lDHMG$DbumOq?AxBntDz5Y5!m
zx>uNLKfe$C5a7Wq;`Xca-ddVwD8v{2CK4@>-qMYw#-dR$3XKJ0X<sLu7D=-9Jkj|k
zM7KLkQ^5Lvn}8{F`nN&e$qAT*46O}=3I%c-6|B?BnBpC|%`Ba89fA`AZ1{JM@)uMS
V3)yh#?6?2`002ovPDHLkV1fX94?q9_
new file mode 100644
index 0000000000000000000000000000000000000000..03350789221b94d830ebc4dafb1b6e4447f79f71
GIT binary patch
literal 1335
zc$@(@1<3k|P)<h;3K|Lk000e1NJLTq001}u001}$1ONa40*~{R000F3Nkl<ZcmeIz
zTWB0_9metR%saE^O`2?zHqEK6w8_6`C6U&)Y6Yu^rCM90h)`OKih5BH!Mj4mB7$NM
zH-##Q7fPj4L9`x_YS9wf(r6FqHmA+TY?E}eNoIFvX6Nt<n*=Gry1Th_KR?)oxq0}#
z&pW%+)z#J2)%{Njt08+hMVz%UxW!9WQRM1J!uz%wW6k`$xQtiHhB0}83@&S<(Wd;6
z{OZT6#ovx|lwpw)Gr~~vYE}hHu@%aMj7ezl<1#K=y>4%t8<MPW$q=tBFo46dpND_2
zZZJ^t@$iuN_)z{{qLfFVmPgZ+oz6EF&lOnW5P(Vo6(nWXqhF7)ND+$?4(?mA0LpTd
zKtm_!1_vWwidph&h5ufd$~$~t<rM?m-#swoS-tY0w|{ZVU*>tj;L6;7o$K}v$b<II
zQSI}Oxz7N}@2Xyvuf=A07$B+k%<+`z2U`DGwUrgP_qa=$?9)Gk`kz}l_mh?dTG=(w
z((eK&e`UOi4WB@Y4CdaG>Uw4wVTdGY^4dp?-3A7W9r|%>CP^~H2-9k4-RX-ZGNcAs
zQCWu|0Supc?6IEEX2!5RejDoc%1MdP$0E;>i=V_GgPahLf=8`rmIbZr?E5c1Bv*8i
zlvUNC?PH^aKFAa{d|~~u4f{6zIt*mP;j0^dx<1}U200@3w4Z1_&<40nian^<whPq-
zN+eQxxP;;f4CHI|md%)-z7{|tncdX2sfQVS<dyJ3hLL~H|2}}k4u=TgN(Wh<>W&m2
z|KsbT77_*&Kgk&3zrVMm!w-h1rTwE$41nPRPs!1Kepk3fwKk3up%n0u)pc0rxX3LH
z6an%+DJBTz11~fhFp#AnK7cpJ1oPMvQ96(>@_;OOHC-B2!a|S|KvbcCi%E(+AuK#o
zA^{RbOg!d!&Lk=Z1%y`hh8{pnMS0}n0W|4&fPdHS4;NKEa-%roA_2N29$-xgp*u>X
zu{lkoLEzPJqyiXr(@Lj=_p}(0r+;hHi6-FCAAeOs2k37AKxsWH+QBz?Mp<=%K0ruC
z;{HiF<3Wr3O)oRobghpCfUCtJ%M$`P(s?<8edK+ji3Lbc6M?XbUqgq@I#BurXCz>L
z_@jRBp@<3-@KCueFm5-UvZ0A@otJNvHd2`IJ%<QpLc^;eFd<Mde>Z=t`IJS8oVo!R
zN5VHFSeW10`e{2X$*PUCQ=|xg3;fu|M`_k^YY5z?At*Bam5v5JVKPY;7YDwaxHn4{
z{~y`UcK&iEW|Jg{iF~P_j@zS{1hxk$*3e<xx!Hd1DX-_Ij`e)XK9lRS;1AL3iAQ<T
zEZ{S7H_^N0Z0@+xX{N=`8gDijBV;hIbhWrObx84ZAL<+4J4ez{?{wYiLI-TJydsT-
zWRmc|{7WsDHX`sSkRnO;&-&iOyg}BmA%3I2>%4sYYWBwX&0H=)6p6tUljQJ#MUoh_
z(}Yeb^yXyl0((B7?o&1FYX5+DvLwV-1Z8f?a~xNebW8Vz!~%3gNp%7;D0Drb=_mmB
zjIA`NJY&4Fz#Xj6XeL5{01EFWaVSw_4u@sRbH*z=(v;Hev&3PVB?5t39smJI{N+y%
zk5#{)DYeoL<rm{O3cU_*b8)G{GySVkWMv(;v7haPP^g{6BFO_Xm27A*0>n|NozEhp
t7*I)|$tbe~*0^B_1C##;b#--h{|0<^JW)j^8BzcM002ovPDHLkV1f*Ie}e!3
--- a/toolkit/themes/osx/global/media/videocontrols.css
+++ b/toolkit/themes/osx/global/media/videocontrols.css
@@ -7,37 +7,40 @@
 
 .controlBar {
   height: 28px;
   background-color: rgba(35,31,32,.74);
 }
 
 .playButton,
 .muteButton,
+.closedCaptionButton,
 .fullscreenButton {
   background-color: transparent;
   background-repeat: no-repeat;
   background-position: center;
   -moz-appearance: none;   /* Remove the native button appearance and styling */
   margin: 0;
   padding: 0;
   min-height: 28px;
   min-width: 28px;
   border: none;
   opacity: 0.7;
 }
 
 .playButton:hover,
 .muteButton:hover,
+.closedCaptionButton:hover,
 .fullscreenButton:hover {
   opacity: 1;
 }
 
 .playButton:hover:active,
 .muteButton:hover:active,
+.closedCaptionButton:hover:active,
 .fullscreenButton:hover:active {
   opacity: 0.4;
 }
 
 .playButton {
   background-image: url(chrome://global/skin/media/pauseButton.png);
   margin-right: -22px; /* 1/2 of scrubber thumb width, for overhang. */
   position: relative; /* Trick to work around negative margin interfering with clicking on the button. */
@@ -58,16 +61,28 @@
 .muteButton[noAudio] {
   background-image: url(chrome://global/skin/media/noAudio.png);
 }
 
 .muteButton[noAudio] + .volumeStack {
   display: none;
 }
 
+.closedCaptionButton {
+  background-image: url(chrome://global/skin/media/closeCaptionButton.png);
+}
+
+.closedCaptionButton[enabled] {
+  opacity: 1;
+}
+
+.closedCaptionButton[hidden] {
+  display: none;
+}
+
 .fullscreenButton {
   background-image: -moz-image-rect(url("chrome://global/skin/media/fullscreenButton.png"), 0, 16, 16, 0);
 }
 
 .fullscreenButton[fullscreened] {
   background-image: -moz-image-rect(url("chrome://global/skin/media/fullscreenButton.png"), 0, 32, 16, 16);
 }
 
@@ -87,16 +102,51 @@
   background-image: url(chrome://global/skin/media/volume-empty.png);
 }
 
 .volumeForeground {
   background-image: url(chrome://global/skin/media/volume-full.png);
   background-clip: content-box;
 }
 
+.textTrackList {
+  display: -moz-box;
+  -moz-appearance: none;
+  -moz-box-pack: end;
+  -moz-box-align: end;
+  padding: 0;
+}
+
+.textTrackList[hidden] {
+  display: none;
+}
+
+.textTrackList > html|*.textTrackItem {
+  -moz-appearance: none;
+  -moz-box-align: start;
+  text-align: start;
+  overflow: hidden;
+  margin: 0;
+  padding: 2px 10px;
+  -moz-margin-end: 10px;
+  border: none;
+  color: rgba(255,255,255,.5);
+  background-color: rgba(35,31,32,.74);
+  white-space: nowrap;
+}
+
+.textTrackList > html|*.textTrackItem[on] {
+  color: white;
+  background-color: black;
+}
+
+.textTrackList > html|*.textTrackItem:hover {
+  background-color: rgba(0,0,0,.55);
+}
+
 .controlBar[fullscreen-unavailable] > .volumeStack {
   /* This value is duplicated in the videocontrols.xml adjustControlSize function. */
   margin-inline-end: 8px;
 }
 
 .volumeControl .scale-thumb {
   min-width: 0;
   opacity: 0;
@@ -335,16 +385,20 @@ html|table {
   .muteButton[muted] {
     background-image: url(chrome://global/skin/media/unmuteButton@2x.png);
     background-size: 33px 28px;
   }
   .muteButton[noAudio] {
     background-image: url(chrome://global/skin/media/noAudio@2x.png);
     background-size: 33px 28px;
   }
+  .closeCaptionButton {
+    background-image: url(chrome://global/skin/media/closeCaptionButton@2x.png);
+    background-size: 28px 28px;
+  }
   .fullscreenButton {
     background-image: -moz-image-rect(url("chrome://global/skin/media/fullscreenButton@2x.png"), 0, 32, 32, 0);
     background-size: 16px 16px;
   }
   .fullscreenButton[fullscreened] {
     background-image: -moz-image-rect(url("chrome://global/skin/media/fullscreenButton@2x.png"), 0, 64, 32, 32);
     background-size: 16px 16px;
   }
--- a/toolkit/themes/shared/non-mac.jar.inc.mn
+++ b/toolkit/themes/shared/non-mac.jar.inc.mn
@@ -96,16 +96,18 @@
   skin/classic/global/media/imagedoc-lightnoise.png        (../../windows/global/media/imagedoc-lightnoise.png)
   skin/classic/global/media/imagedoc-darknoise.png         (../../windows/global/media/imagedoc-darknoise.png)
   skin/classic/global/media/videocontrols.css              (../../windows/global/media/videocontrols.css)
   skin/classic/global/media/pauseButton.png                (../../windows/global/media/pauseButton.png)
   skin/classic/global/media/playButton.png                 (../../windows/global/media/playButton.png)
   skin/classic/global/media/muteButton.png                 (../../windows/global/media/muteButton.png)
   skin/classic/global/media/unmuteButton.png               (../../windows/global/media/unmuteButton.png)
   skin/classic/global/media/noAudio.png                    (../../windows/global/media/noAudio.png)
+  skin/classic/global/media/closeCaptionButton.png         (../../windows/global/media/closeCaptionButton.png)
+  skin/classic/global/media/closeCaptionButton@2x.png      (../../windows/global/media/closeCaptionButton@2x.png)
   skin/classic/global/media/fullscreenButton.png           (../../windows/global/media/fullscreenButton.png)
   skin/classic/global/media/scrubberThumb.png              (../../windows/global/media/scrubberThumb.png)
   skin/classic/global/media/scrubberThumbWide.png          (../../windows/global/media/scrubberThumbWide.png)
   skin/classic/global/media/throbber.png                   (../../windows/global/media/throbber.png)
   skin/classic/global/media/stalled.png                    (../../windows/global/media/stalled.png)
   skin/classic/global/media/volume-empty.png               (../../windows/global/media/volume-empty.png)
   skin/classic/global/media/volume-full.png                (../../windows/global/media/volume-full.png)
   skin/classic/global/media/error.png                      (../../windows/global/media/error.png)
new file mode 100644
index 0000000000000000000000000000000000000000..469310fb1b21ed705925decd18710dd0df37d915
GIT binary patch
literal 583
zc$@)80=WH&P)<h;3K|Lk000e1NJLTq000~S000~a1ONa4*aR0J0006GNkl<ZcmeH_
zKWGzS7{;HWWDq2_UZH9T=^)YuOG`9SDy8)=Hh<duyKu?feMb@vv1wYdT)?42a1mWy
zTy=DE5phU6NWoM~#X=Mp!6_6AHa%!kdA$qS@=;fZKELI?=jF!@-;1OCpRw&0>G9$0
z7GXnI1j>_Uv8{b)8|;(1qO2(E=JZXgTfIdd0=nWPM(pX`zAR)U5JLpBCJ}Q=AZ5=!
z$ys|i(G?e|4R$<a_f*rZ^4{{h2pl-`I<U9gDu2$Mx^OJ`n~@3^PgFKljC>4Jv7t6K
zdhlxO<$2N%v>ir;FA_%987X{GUen<IHP=)e*=re+-)n0c$;piU&W^I3huiw%i^&Re
z_=weHO!QpopFVkgGeb#l0KQ)Tz$DxiMoGRXt*WQTN&2c_(9xG0zU=*=k#y6CUisX;
zA-GmmlK7%nRdMNu*i`&Ol~H=Q!Cs_De10oF&?*`87_6!!^2KN^1y5O|*nX*bpQ2Lm
zMx3I5ibl!Q18{GpD9RVZ2h#9Z&-kN6J@zv;C&Gt|lDHMG$DbumOq?AxBntDz5Y5!m
zx>uNLKfe$C5a7Wq;`Xca-ddVwD8v{2CK4@>-qMYw#-dR$3XKJ0X<sLu7D=-9Jkj|k
zM7KLkQ^5Lvn}8{F`nN&e$qAT*46O}=3I%c-6|B?BnBpC|%`Ba89fA`AZ1{JM@)uMS
V3)yh#?6?2`002ovPDHLkV1fX94?q9_
new file mode 100644
index 0000000000000000000000000000000000000000..03350789221b94d830ebc4dafb1b6e4447f79f71
GIT binary patch
literal 1335
zc$@(@1<3k|P)<h;3K|Lk000e1NJLTq001}u001}$1ONa40*~{R000F3Nkl<ZcmeIz
zTWB0_9metR%saE^O`2?zHqEK6w8_6`C6U&)Y6Yu^rCM90h)`OKih5BH!Mj4mB7$NM
zH-##Q7fPj4L9`x_YS9wf(r6FqHmA+TY?E}eNoIFvX6Nt<n*=Gry1Th_KR?)oxq0}#
z&pW%+)z#J2)%{Njt08+hMVz%UxW!9WQRM1J!uz%wW6k`$xQtiHhB0}83@&S<(Wd;6
z{OZT6#ovx|lwpw)Gr~~vYE}hHu@%aMj7ezl<1#K=y>4%t8<MPW$q=tBFo46dpND_2
zZZJ^t@$iuN_)z{{qLfFVmPgZ+oz6EF&lOnW5P(Vo6(nWXqhF7)ND+$?4(?mA0LpTd
zKtm_!1_vWwidph&h5ufd$~$~t<rM?m-#swoS-tY0w|{ZVU*>tj;L6;7o$K}v$b<II
zQSI}Oxz7N}@2Xyvuf=A07$B+k%<+`z2U`DGwUrgP_qa=$?9)Gk`kz}l_mh?dTG=(w
z((eK&e`UOi4WB@Y4CdaG>Uw4wVTdGY^4dp?-3A7W9r|%>CP^~H2-9k4-RX-ZGNcAs
zQCWu|0Supc?6IEEX2!5RejDoc%1MdP$0E;>i=V_GgPahLf=8`rmIbZr?E5c1Bv*8i
zlvUNC?PH^aKFAa{d|~~u4f{6zIt*mP;j0^dx<1}U200@3w4Z1_&<40nian^<whPq-
zN+eQxxP;;f4CHI|md%)-z7{|tncdX2sfQVS<dyJ3hLL~H|2}}k4u=TgN(Wh<>W&m2
z|KsbT77_*&Kgk&3zrVMm!w-h1rTwE$41nPRPs!1Kepk3fwKk3up%n0u)pc0rxX3LH
z6an%+DJBTz11~fhFp#AnK7cpJ1oPMvQ96(>@_;OOHC-B2!a|S|KvbcCi%E(+AuK#o
zA^{RbOg!d!&Lk=Z1%y`hh8{pnMS0}n0W|4&fPdHS4;NKEa-%roA_2N29$-xgp*u>X
zu{lkoLEzPJqyiXr(@Lj=_p}(0r+;hHi6-FCAAeOs2k37AKxsWH+QBz?Mp<=%K0ruC
z;{HiF<3Wr3O)oRobghpCfUCtJ%M$`P(s?<8edK+ji3Lbc6M?XbUqgq@I#BurXCz>L
z_@jRBp@<3-@KCueFm5-UvZ0A@otJNvHd2`IJ%<QpLc^;eFd<Mde>Z=t`IJS8oVo!R
zN5VHFSeW10`e{2X$*PUCQ=|xg3;fu|M`_k^YY5z?At*Bam5v5JVKPY;7YDwaxHn4{
z{~y`UcK&iEW|Jg{iF~P_j@zS{1hxk$*3e<xx!Hd1DX-_Ij`e)XK9lRS;1AL3iAQ<T
zEZ{S7H_^N0Z0@+xX{N=`8gDijBV;hIbhWrObx84ZAL<+4J4ez{?{wYiLI-TJydsT-
zWRmc|{7WsDHX`sSkRnO;&-&iOyg}BmA%3I2>%4sYYWBwX&0H=)6p6tUljQJ#MUoh_
z(}Yeb^yXyl0((B7?o&1FYX5+DvLwV-1Z8f?a~xNebW8Vz!~%3gNp%7;D0Drb=_mmB
zjIA`NJY&4Fz#Xj6XeL5{01EFWaVSw_4u@sRbH*z=(v;Hev&3PVB?5t39smJI{N+y%
zk5#{)DYeoL<rm{O3cU_*b8)G{GySVkWMv(;v7haPP^g{6BFO_Xm27A*0>n|NozEhp
t7*I)|$tbe~*0^B_1C##;b#--h{|0<^JW)j^8BzcM002ovPDHLkV1f*Ie}e!3
--- a/toolkit/themes/windows/global/media/videocontrols.css
+++ b/toolkit/themes/windows/global/media/videocontrols.css
@@ -7,37 +7,40 @@
 
 .controlBar {
   height: 28px;
   background-color: rgba(35,31,32,.74);
 }
 
 .playButton,
 .muteButton,
+.closedCaptionButton,
 .fullscreenButton {
   background-color: transparent;
   background-repeat: no-repeat;
   background-position: center;
   -moz-appearance: none;   /* Remove the native button appearance and styling */
   margin: 0;
   padding: 0;
   min-height: 28px;
   min-width: 28px;
   border: none;
   opacity: 0.7;
 }
 
 .playButton:hover,
 .muteButton:hover,
+.closedCaptionButton:hover,
 .fullscreenButton:hover {
   opacity: 1;
 }
 
 .playButton:hover:active,
 .muteButton:hover:active,
+.closedCaptionButton:hover:active,
 .fullscreenButton:hover:active {
   opacity: 0.4;
 }
 
 .playButton {
   background-image: url(chrome://global/skin/media/pauseButton.png);
   margin-right: -22px; /* 1/2 of scrubber thumb width, for overhang. */
   position: relative; /* Trick to work around negative margin interfering with clicking on the button. */
@@ -58,16 +61,28 @@
 .muteButton[noAudio] {
   background-image: url(chrome://global/skin/media/noAudio.png);
 }
 
 .muteButton[noAudio] + .volumeStack {
   display: none;
 }
 
+.closedCaptionButton {
+  background-image: url(chrome://global/skin/media/closeCaptionButton.png);
+}
+
+.closedCaptionButton[enabled] {
+  opacity: 1;
+}
+
+.closedCaptionButton[hidden] {
+  display: none;
+}
+
 .fullscreenButton {
   background-image: -moz-image-rect(url("chrome://global/skin/media/fullscreenButton.png"), 0, 16, 16, 0);
 }
 
 .fullscreenButton[fullscreened] {
   background-image: -moz-image-rect(url("chrome://global/skin/media/fullscreenButton.png"), 0, 32, 16, 16);
 }
 
@@ -87,16 +102,51 @@
   background-image: url(chrome://global/skin/media/volume-empty.png);
 }
 
 .volumeForeground {
   background-image: url(chrome://global/skin/media/volume-full.png);
   background-clip: content-box;
 }
 
+.textTrackList {
+  display: -moz-box;
+  -moz-appearance: none;
+  -moz-box-pack: end;
+  -moz-box-align: end;
+  padding: 0;
+}
+
+.textTrackList[hidden] {
+  display: none;
+}
+
+.textTrackList > html|*.textTrackItem {
+  -moz-appearance: none;
+  -moz-box-align: start;
+  text-align: start;
+  overflow: hidden;
+  margin: 0;
+  padding: 2px 10px;
+  -moz-margin-end: 10px;
+  border: none;
+  color: rgba(255,255,255,.5);
+  background-color: rgba(35,31,32,.74);
+  white-space: nowrap;
+}
+
+.textTrackList > html|*.textTrackItem[on] {
+  color: white;
+  background-color: black;
+}
+
+.textTrackList > html|*.textTrackItem:hover {
+  background-color: rgba(0,0,0,.55);
+}
+
 .controlBar[fullscreen-unavailable] > .volumeStack {
   /* This value is duplicated in the videocontrols.xml adjustControlSize function. */
   margin-inline-end: 8px;
 }
 
 .volumeControl .scale-thumb {
   min-width: 0;
   opacity: 0;