Bug 1000152: Add mute & unmute buttons to Loop conversation window. r=Standard8
authorNicolas Perriault <nperriault@gmail.com>
Fri, 27 Jun 2014 12:34:22 +0200
changeset 192834 416d7931e3490c416b8544344eb32b8213bdd2b1
parent 192833 ccebb68bdf07420f01c29f71824464d532dc48bc
child 192835 7e27f2f4793c08f1147470a9196f7107131e9f51
push id7663
push userkwierso@gmail.com
push dateWed, 09 Jul 2014 03:08:08 +0000
treeherderfx-team@48de6f4f82af [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1000152
milestone33.0a1
Bug 1000152: Add mute & unmute buttons to Loop conversation window. r=Standard8
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/css/readme.html
browser/components/loop/content/shared/img/audio-highlight-14x14.png
browser/components/loop/content/shared/img/audio-highlight-14x14@2x.png
browser/components/loop/content/shared/img/audio-inverse-14x14.png
browser/components/loop/content/shared/img/audio-inverse-14x14@2x.png
browser/components/loop/content/shared/img/facemute-14x14.png
browser/components/loop/content/shared/img/facemute-14x14@2x.png
browser/components/loop/content/shared/img/hangup-inverse-14x14.png
browser/components/loop/content/shared/img/hangup-inverse-14x14@2x.png
browser/components/loop/content/shared/img/mute-inverse-14x14.png
browser/components/loop/content/shared/img/mute-inverse-14x14@2x.png
browser/components/loop/content/shared/img/video-highlight-14x14.png
browser/components/loop/content/shared/img/video-highlight-14x14@2x.png
browser/components/loop/content/shared/img/video-inverse-14x14.png
browser/components/loop/content/shared/img/video-inverse-14x14@2x.png
browser/components/loop/content/shared/js/views.js
browser/components/loop/jar.mn
browser/components/loop/standalone/content/l10n/data.ini
browser/components/loop/test/shared/views_test.js
browser/locales/en-US/chrome/browser/loop/loop.properties
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -1,37 +1,157 @@
 /* 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/. */
 
 /* Conversation window styles */
 
+.conversation {
+  position: relative;
+}
+
 .conversation .controls {
-  background: #f2f2f2;
-  padding: .5em;
+  position: absolute;
+  z-index: 999; /* required to have it superimposed to the video element */
+  left: 0;
+  right: 0;
+  background: rgba(0, 0, 0, .70);
+  border: 1px solid #5a5a5a;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+
+.conversation .controls li {
+  float: left;
+  font-size: 10px;
+}
+
+.conversation .controls .btn {
+  width: 40px;
+  height: 30px;
+  background: transparent;
+  background-repeat: no-repeat;
+  background-position: 12px 8px;
+  background-size: 14px 14px;
+  border-right: 1px solid #5a5a5a;
+  border-radius: 0;
+  cursor: pointer;
+}
+
+.conversation .controls .btn:hover {
+  background-color: rgba(255, 255, 255, .35);
+}
+
+/* Hangup button */
+.conversation .controls .btn-hangup {
+  background-color: #D74345;
+  background-image: url(../img/hangup-inverse-14x14.png);
+}
+.conversation .controls .btn-hangup:hover {
+  background-color: #C53436;
+}
+@media (min-resolution: 2dppx) {
+  .conversation .controls .btn-hangup {
+    background-image: url(../img/hangup-inverse-14x14@2x.png);
+  }
+}
+
+/* Common media control buttons behavior */
+.conversation .controls .media-control {
+  background-color: transparent;
+  opacity: .7; /* reduce the opacity for a non-streaming media */
+}
+.conversation .controls .media-control:hover {
+  background-color: rgba(255, 255, 255, .35);
+  opacity: 1;
 }
+.conversation .controls .media-control.muted {
+  background-color: #0096DD;
+  opacity: 1;
+}
+.conversation .controls .media-control.streaming {
+  opacity: 1;
+}
+.conversation .controls .media-control:not(.streaming):hover {
+  background-color: transparent;
+  opacity: 1;
+}
+
+/* Audio mute button */
+.conversation .controls .btn-mute-audio {
+  background-image: url(../img/audio-inverse-14x14.png);
+}
+.conversation .controls .btn-mute-audio.streaming {
+  background-image: url(../img/audio-highlight-14x14.png);
+}
+.conversation .controls .btn-mute-audio.muted,
+.conversation .controls .btn-mute-audio.streaming:hover {
+  background-image: url(../img/mute-inverse-14x14.png);
+}
+@media (min-resolution: 2dppx) {
+  .conversation .controls .btn-mute-audio {
+    background-image: url(../img/audio-inverse-14x14@2x.png);
+  }
+  .conversation .controls .btn-mute-audio.streaming {
+    background-image: url(../img/audio-highlight-14x14@2x.png);
+  }
+  .conversation .controls .btn-mute-audio.muted,
+  .conversation .controls .btn-mute-audio.streaming:hover {
+    background-image: url(../img/mute-inverse-14x14@2x.png);
+  }
+}
+
+/* Video mute button */
+.conversation .controls .btn-mute-video {
+  background-image: url(../img/video-inverse-14x14.png);
+}
+.conversation .controls .btn-mute-video.streaming {
+  background-image: url(../img/video-highlight-14x14.png);
+}
+.conversation .controls .btn-mute-video.muted,
+.conversation .controls .btn-mute-video.streaming:hover {
+  background-image: url(../img/facemute-14x14.png);
+}
+@media (min-resolution: 2dppx) {
+  .conversation .controls .btn-mute-video {
+    background-image: url(../img/video-inverse-14x14@2x.png);
+  }
+  .conversation .controls .btn-mute-video.streaming {
+    background-image: url(../img/video-highlight-14x14@2x.png);
+  }
+  .conversation .controls .btn-mute-video.muted,
+  .conversation .controls .btn-mute-video.streaming:hover {
+    background-image: url(../img/facemute-14x14@2x.png);
+  }
+}
+
+/* Video elements */
 
 .conversation .media video {
   background: #eee;
 }
 
 /* Nested video elements */
 
 .conversation .media.nested {
   position: relative;
 }
 
 .conversation .media.nested .remote {
+  display: inline-block;
+  background: #000;
   width: 100%;
+  min-height: 154px;
 }
 
 .conversation .media.nested .local {
   position: absolute;
-  bottom: .8em;
-  right: .8em;
+  bottom: 4px;
+  right: 0;
   width: 30%;
   max-width: 140px;
 }
 
 /* Side by side video elements */
 
 .conversation .media.side-by-side .remote {
   width: 50%;
--- a/browser/components/loop/content/shared/css/readme.html
+++ b/browser/components/loop/content/shared/css/readme.html
@@ -1,9 +1,9 @@
-<!DOCTYPE html>
+  <!DOCTYPE html>
 <!-- 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 file is intended to help frontend developers to easily identify what
      are the available styles for the Loop UI components. -->
 <html>
 <head>
@@ -93,46 +93,114 @@
       <video class="remote"></video>
       <video class="local"></video>
     </div>
   </div>
 
   <h3>Large with controls</h3>
 
   <div class="conversation">
-    <nav class="controls">
-      <button class="btn">Start</button>
-      <button class="btn">Stop</button>
-    </nav>
+    <ul class="controls">
+      <li><button class="btn btn-hangup" title="Hangup"></button></li>
+      <li><button class="btn media-control btn-mute-video streaming" title="Mute video"></button></li>
+      <li><button class="btn media-control btn-mute-audio" title="Mute audio"></button></li>
+    </ul>
     <div class="media nested">
       <video class="remote"></video>
       <video class="local"></video>
     </div>
   </div>
 
   <h3>Small (think chat window)</h3>
 
   <div style="width: 204px">
     <div class="conversation">
+      <ul class="controls">
+        <li><button class="btn btn-hangup" title="Hangup"></button></li>
+        <li><button class="btn media-control btn-mute-video streaming" title="Mute video"></button></li>
+        <li><button class="btn media-control btn-mute-audio" title="Mute audio"></button></li>
+      </ul>
       <div class="media nested">
         <video class="remote"></video>
         <video class="local"></video>
       </div>
     </div>
   </div>
 
   <h3>Side by side</h3>
 
   <div class="conversation">
     <div class="media side-by-side">
       <video class="remote"></video>
       <video class="local"></video>
     </div>
   </div>
 
+  <h2>Controls button variants</h2>
+
+  <h3>Nothing muted</h3>
+
+  <div style="width: 204px; min-height: 26px">
+    <div class="conversation">
+      <ul class="controls">
+        <li><button class="btn btn-hangup" title="Hangup"></button></li>
+        <li><button class="btn media-control btn-mute-video" title="Mute video"></button></li>
+        <li><button class="btn media-control btn-mute-audio" title="Mute audio"></button></li>
+      </ul>
+    </div>
+  </div>
+
+  <h3>Local audio muted</h3>
+
+  <div style="width: 204px; min-height: 26px">
+    <div class="conversation">
+      <ul class="controls">
+        <li><button class="btn btn-hangup" title="Hangup"></button></li>
+        <li><button class="btn media-control btn-mute-video" title="Mute video"></button></li>
+        <li><button class="btn media-control btn-mute-audio muted" title="Mute audio"></button></li>
+      </ul>
+    </div>
+  </div>
+
+  <h3>Local video muted</h3>
+
+  <div style="width: 204px; min-height: 26px">
+    <div class="conversation">
+      <ul class="controls">
+        <li><button class="btn btn-hangup" title="Hangup"></button></li>
+        <li><button class="btn media-control btn-mute-video muted" title="Mute video"></button></li>
+        <li><button class="btn media-control btn-mute-audio" title="Mute audio"></button></li>
+      </ul>
+    </div>
+  </div>
+
+  <h3>Local audio streaming</h3>
+
+  <div style="width: 204px; min-height: 26px">
+    <div class="conversation">
+      <ul class="controls">
+        <li><button class="btn btn-hangup" title="Hangup"></button></li>
+        <li><button class="btn media-control btn-mute-video" title="Mute video"></button></li>
+        <li><button class="btn media-control btn-mute-audio streaming" title="Mute audio"></button></li>
+      </ul>
+    </div>
+  </div>
+
+  <h3>Local video streaming</h3>
+
+  <div style="width: 204px; min-height: 26px">
+    <div class="conversation">
+      <ul class="controls">
+        <li><button class="btn btn-hangup" title="Hangup"></button></li>
+        <li><button class="btn media-control btn-mute-video streaming" title="Mute video"></button></li>
+        <li><button class="btn media-control btn-mute-audio" title="Mute audio"></button></li>
+      </ul>
+    </div>
+  </div>
+
   <h2>Buttons</h2>
 
   <h3>Using <code>&lt;a&gt;</code></h3>
 
   <p>
     <a href="" class="btn">default</a>
     <a href="" class="btn btn-info">info</a>
     <a href="" class="btn btn-success">success</a>
new file mode 100644
index 0000000000000000000000000000000000000000..62469ecd0e8f07001c0777057a4177fc82ba4241
GIT binary patch
literal 226
zc$@*^03H8{P)<h;3K|Lk000e1NJLTq000gE000gM1^@s6A4o0H0001~Nkl<ZD9>YH
zpepFODZ()I?oNg&cRn*rxx0p;;S3jX8mHXu2WtEe$CK}r5T|kK-7RPuXAx=Rovn1#
zIOXmMtQtY$1Z=wlRJ0jI<CHry!Q%J>=FTmkqKPOPr{3uX;-iG3q7tZaB@i!%*nVd|
zP}4SsNq6G#Yiwv>0y?z~h_3@RZ3o(T1E{f>_|!Ayb~ZF%0!Y!+547zz(AI}Qd=sd-
cnrg`z0C|s*<j*?MEdT%j07*qoM6N<$g14GoF#rGn
new file mode 100644
index 0000000000000000000000000000000000000000..deaa8dda7e694066e0c854bd041b4297e5d3b5cc
GIT binary patch
literal 439
zc$@*Z0Z9IdP)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<0004gNkl<ZNXKJf
z7zOBH@?8^#sdu&k@oR=DcONoLy}OuU(rp>qST^NOAkcz8K>QyKe_)t=Q-iveb>83w
z`wG)C2%d8HFqMPyjuTGHAow3c$6bDE#$5&h3;#1rykkto_`91)&_Ymx8ntlLLZC$#
zfcbAa!{pmpL|HiX?jfL_rQ}-(F>EK%KAd{@9k4_?J)|rImYsL@0dXop3#Z;P0IK^0
z#K$R#!|Ol;UNTI%ZHd>iDR+J|Ot_&AWiO(nhFA$UWYTRzpyfC56yB?WYU3e6d#8kw
zAhm(l29s`E01bVE#j@)R{kJrM^mCy6N1!@UO6$Ru(4blgG5B@|ki8dZ!Db*`$}s7s
zK9GHqa^Jx~!+s8+!RsJC0>$T@*^pFwrx=Kr0mZ*V9CW*pdL>QhDrRWbxPi6uopjrm
zR;?OF6iX>8RftYVcP=9=ygiRrtu<Y6&`r7f7Kr~M<-ecMc-_D-<+?C+ENf_B0vfmg
hY9StcyK1x@4FK+#GGf9RrW61G002ovPDHLkV1hPxza0Po
new file mode 100644
index 0000000000000000000000000000000000000000..1884088ee5d7a5e221fb8611de0061c874225b6f
GIT binary patch
literal 190
zc%17D@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0y_R!<kl5RLQ62@BMI{Qtk7L*PBj
zqw@}X*gAH0T)pvMUUZ7U>O$^W$0Cnp-SM*Tb#PVRkjc_1e5r!H!+de(98MwiB_G8E
z{9P{J;638bnYo7L(Nu>$GFiWC6z;JdDQqz@Vl0#vh-ZKF)}e;EqubNd_GUmj+k{1&
oQ;PDKKRPSy<2-UT#QQwMY5rdpYiBn)0UgBP>FVdQ&MBb@0L1=9UjP6A
new file mode 100644
index 0000000000000000000000000000000000000000..8443c4b67afda8196c3921ad0e69005aa3505dea
GIT binary patch
literal 340
zc$@)L0jvIrP)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<0003SNkl<Zc-rmO
zKTASU9EWlLEH2U@ZSLg|B{~#1wKwz%ngnlawzReR4jhCCLqUQNf}o*>&JfZPG+3jP
zT+FZ*3AO8Uw#x-Q$vIcJ;1@pg!*4m9kmEQ@6^LLKcj%#mD<u629%2md2G}$mjH8EH
z6Q+avn72QynG6=7TE=A1f@;}Sa21qZQO5B<gHNdG1m*4*xbO=vfeMcCh&)u=!4IBw
zf^FE?Q^ZEtfLv84*nnIbADFdk96_$A6U;$QU>Be8u8kDB_{O?U(0yeCDU|SlTbyDW
zE%$Z0!5D5JXQKiOC#YixxpR{*O%xX1;hn&NUw@jgCt6#^ukFnhyzncy1%W;$uHY|h
mG?6eBjNlSN;VkGMVSQ67n(uHit(U$40000<MNUMnLSTaU!H*FD
new file mode 100644
index 0000000000000000000000000000000000000000..d9cb2bb3d6690345150c7aad6a3829215105e3ef
GIT binary patch
literal 218
zc%17D@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0y_g`O^sAr-gwURua?h(W~dVXI`!
zmGT?f5@*fgodgcwYD!%lwm2j1rgDY>uStgB>|BFuX@cfQUUyD6yrK4pKYnMyEmM~#
z+Vz~dZ!TVc$ipt9onz?M#h@&EFL1kQm`6#(6&LT831$zOtUYF5aAwJr+V}7gh<xy1
zRi1Rrszd56PiH)0vz51XdHSJ;U9Dr`^h5?`v1e8#1}^M(g+4Fe|BErjdD+>m4)^Z@
P9n0Y9>gTe~DWM4f{iIVN
new file mode 100644
index 0000000000000000000000000000000000000000..a5fc41ab837bc5b0150e755b6a1a0a3ca3998b0d
GIT binary patch
literal 373
zc$@)s0gC>KP)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<0003zNkl<Zc-rmO
zAxHyZ7>Dt1r%h1V5DbDz3=Wepi(nfj5sNYyHo;&KW-$nY>4Go>MT;=7Y1OJhFlf+b
zP+1Vegdrzv>b*W;a5BDgAKZNdVSo7Lr@Y}+BGOi!;J*sGSZ_zr!5)P9m<<aiu!JIZ
zVXBA)bT^bF*fo-2!EHpDixUZUfj(^DNEa+4uAQS7#}Ia-3wnrSr|8ES#CEWR0bD_B
z2lp|Adx-6zi%~p5YzH?nj#r57;2JV`gV+pyV;R$Uhu92$B8M4#Kx_tUn8Q5k5Su{{
zS>*8r-B#hp5v*bgEASCE2aBjj305$H0$R<1LKa@DU=_`KK^j|zmz#iUm_+5T;3a<V
z86((%$!?*VjNvgXcp$+ZFpNW(tc(nlMi0`uU=BX+Fo-Us3?!pGQF){~Z(<Zb*)E+W
T=wgqX00000NkvXXu0mjf^5l}&
new file mode 100644
index 0000000000000000000000000000000000000000..d54db72b83969e3edd233ccd0bf5dbabff248f04
GIT binary patch
literal 188
zc%17D@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0y_W=|K#5RLQ62@6>M{Qn=%*CEbq
zD7;dt{7-ws$|YHQ{>U?SbzEdFzQv`oF@4F66YNLcFEERm^y*H<iepQkw7lb5@vI`q
zw20wjqQXDkBPW++i7`BC4w!AuSSTQ{ovF~&aMsPniih<+2Fx8ZI}H_hI_59Re5ZQ!
n{!E8G;vKgg)-BYiWo9@uYpa{g%gZ-_&SCI$^>bP0l+XkKWp+u!
new file mode 100644
index 0000000000000000000000000000000000000000..885500b708b3d369e49275566b9a55619a169287
GIT binary patch
literal 330
zc$@)B0k!^#P)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<0003INkl<ZNXO;W
zKPyFX7{Kv6Fi1C)B*QIbmJEs%vq_j3Y<Dqu33oD@Oo-wYP?Y?61vh0eu#>?e5;yr6
z+~GLx<qqe$CExLMI-j0?zvtv~|0=X$A3e<iw|!Pddo(uTYlmow1k1j+83~@NT`3AI
zsozutI8+C_MQ7BMr!~N($a5U40nS9P$59Ggiv|bqlmPG16^#~CQ?57TH+PT#-!UBx
zcH=66K4T;r?Z;i}JjbKaVLT?#7c4}hlXys7q&*Z;@6Dr_6*`EU6#9l^tl}*3d?hQi
zfJ=?Fi`FdAj_Su6=>++#rk$<c+TXfBUsh-@Hc<HitY9%>U$^j@!mcqIxjuQ!VF$~Z
cT$(@E2WYdCKle$kr~m)}07*qoM6N<$f=yzJM*si-
new file mode 100644
index 0000000000000000000000000000000000000000..36bb3cecc870b5e44ca0193059f035abb22c2234
GIT binary patch
literal 241
zc$@+801p3&P)<h;3K|Lk000e1NJLTq000gE000gM1ONa4wL#@t0002ENkl<ZcmZ|E
zJu5|V901_we|1U*Hgy;A1*F7oP!{_c>}NZfEM68bd8;$ZH;{Y+HWPIgL|K&7S#Cz9
zaF6jfxSsV%gf>a~0HhXx!hy`5-M=}mF$AXN9-I+5?-p=x(z5Hp`EE(gplmF9B9dc2
z*6`xch*>L<R60tJPE|I{$RjBoxpQIPmmR~_Bd1(>vu#u6+^PkykvDn@CnmH$m1-ip
rdcJi$>l)RENSIR@!-p-Gk^lMyg@HT)UJP4w00000NkvXXu0mjf7<*sD
new file mode 100644
index 0000000000000000000000000000000000000000..eebabcb7e0c3a5e9790f9a3cae49d5315ea51950
GIT binary patch
literal 465
zc$@*z0WSWDP)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<0004)Nkl<Zc-p<z
zO(+Cm7{KvaEeZ#lq`0tAa!^Vs2}e@vvdY0n`B(=I6iP{<Jt+soMPfyigVYwq8Aoz(
z;U=H!V@o1y{ndMT4%Xz|oq6h4^Uln^|I5s@Ml>3=mF^EykZwn?7RUHN1m|eCD%gTp
zGo9$53t_~XFI0&8$50+mupVYJiX<$kbr_Yp!CsinD%QmM6gj%VKA6p?I6j06hE+j1
z`{I?cJh+Wi7<L2$aKf-7IESQo-@|-7V%a3<M=>TafJZdy1l?-Gtv`Z8D8n6=Frv<T
z6CI4ehblaSi9eVpp1e^J`#>}5@G8#FV_Y43bRU{0+^fjXyrBVJyhBbmcGPj9ZtzfS
z7)A{`@CiBBa3h3gICO%8V#5cNqZMDj#Hz8O*26ll#iZEq3MKHN6rEVa3i?rmO?B@H
zQvMTkU{=H~P=F@!f!@Uw{0QPjT{}P~jQ<9w)dpwC71umCKm>A*(1zrAzBE0^ig7I{
zVmDU*iAuws2;L&YieN1+5o<!|N>tE^8LXQKAX_77Df@%p(oYB0+GjQD00000NkvXX
Hu0mjfVA;$S
new file mode 100644
index 0000000000000000000000000000000000000000..5d1e7ccc4c9e910f3cd6974e9428aac300cc6412
GIT binary patch
literal 190
zc%17D@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0y_R!<kl5RLQ62@**Or`}fb1i$}x
zyzl*<lo=(#@@v-pRx&^Q#5&bO>ixf?YXv)&-&vP3qbOEl#XA)NdzXwO%Q@G4Jjb)(
z>~)2lAd^KBPU5}6@BS!+CmRc_KfLUtb=}$WE=i|wLuS+SZdWaymI(%Y&<Qwb$z${M
oTv^*a>rDk)C+F?t@kwH45Q_L5A>3ME1auICr>mdKI;Vst0B)&ChX4Qo
new file mode 100644
index 0000000000000000000000000000000000000000..0692ad7087c1b9300af7bdce87daa98b4ff8d9f7
GIT binary patch
literal 301
zc$@((0n+}7P)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<0002=Nkl<ZNXKJf
z7zLwX=zxiLbQq@GJ<l-p?mr;@PXY$2zjKCR!VPt-=1#rJgI#XQons_ggo>x!*^6Q3
zq?`Ic$2<ah<S;f1e^X@P)H|QhEVBiQe}Fg!n}rlv2FBmvW=+2B2j;_lPfH7@-bug+
zEt*+4`A!MeL`NeF@g%5G3r8))UeW-w!ks7d^x=1gDc9tIhTo*0h2SFXwkXiIr|D-Q
z2z1`y1zNfXzlBrozNau@JV!0OZ?gl%R^##Eo#_-=IQecLhM5fwOh9vMffi1|w6I}6
z3s5o#7!<RC2FxKEgVayCo5>Kmig~mZGco`Gy)(0d(b#$<00000NkvXXu0mjfG0%V*
new file mode 100644
index 0000000000000000000000000000000000000000..2fbd6d10a0634ab6714088979122ce28906aea1c
GIT binary patch
literal 155
zc%17D@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0y_Bu^K|5RLQ62@+8c{{R2~`QKwU
z{l*CSRejPOrze>H``a%c!nDyqU_Q&E_7hT^jgk&Eyr<MWm5mnkACrHQmBaR^^oB{D
z`htxT9Wxs%%yu19Qn2Gb@_fUCw+=epORl9R1W7Yom_N<B;)UxnpluAEu6{1-oD!M<
DO6NHy
new file mode 100644
index 0000000000000000000000000000000000000000..e596bdc4eda93a68224611505473b7c7808289c6
GIT binary patch
literal 243
zc%17D@N?(olHy`uVBq!ia0vp^G9b*s1|*Ak?@s|zyF6VSLn>~)y{6B_<S5bh(UG;=
zZF6Dw1I|57=^MD*R)-sKwtZ!bbDW$Icq2#j;ED(}oiopcSyCnc>oCU`#{GD@e4m?3
z$k7BIHdgBs4+7Hos#Wrpcvbd25L1k<PKuS;_>r}^<xN<}*Mim`%?~#l@YhCkxD_<W
zEE1GC>?qQ79;ox*E*bT+3tXky&n|G}w%)67;mE7#4^0#1CdOUa_(x=pa!T-wCN8xZ
qKbuT@CL{$qbt$g;c=?a}2R@6Rv+SCDSMLD&g2B_(&t;ucLK6Vl1z*De
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -89,40 +89,56 @@ loop.shared.views = (function(_, OT, l10
   });
 
   /**
    * Conversation view.
    */
   var ConversationView = BaseView.extend({
     className: "conversation",
 
+    /**
+     * Local stream object.
+     * @type {OT.Stream|null}
+     */
+    localStream: null,
+
     template: _.template([
-      '<nav class="controls">',
-      '  <button class="btn stop" data-l10n-id="stop"></button>',
-      '</nav>',
+      '<ul class="controls cf">',
+      '  <li><button class="btn btn-hangup" ',
+      '              data-l10n-id="hangup_button"></button></li>',
+      '  <li><button class="btn media-control btn-mute-video"',
+      '              data-l10n-id="mute_local_video_button"></button></li>',
+      '  <li><button class="btn media-control btn-mute-audio"',
+      '              data-l10n-id="mute_local_audio_button"></button></li>',
+      '</ul>',
       '<div class="media nested">',
       // Both these wrappers are required by the SDK; this is fragile and
       // will break if a future version of the SDK updates this generated DOM,
       // especially as the SDK seems to actually move wrapped contents into
       // their own generated stuff.
       '  <div class="remote"><div class="incoming"></div></div>',
       '  <div class="local"><div class="outgoing"></div></div>',
       '</div>'
     ].join("")),
 
     // height set to "auto" to fix video layout on Google Chrome
     // @see https://bugzilla.mozilla.org/show_bug.cgi?id=991122
-    videoStyles: {
+    publisherConfig: {
       width: "100%",
       height: "auto",
-      style: { "bugDisplayMode": "off" }
+      style: {
+        bugDisplayMode: "off",
+        buttonDisplayMode: "off"
+      }
     },
 
     events: {
-      'click .btn.stop': 'hangup'
+      'click .btn-hangup': 'hangup',
+      'click .btn-mute-audio': 'toggleMuteAudio',
+      'click .btn-mute-video': 'toggleMuteVideo'
     },
 
     /**
      * Establishes webrtc communication using OT sdk.
      */
     initialize: function(options) {
       options = options || {};
       if (!options.sdk) {
@@ -148,50 +164,112 @@ loop.shared.views = (function(_, OT, l10
      *
      * @param  {StreamEvent} event
      */
     _streamCreated: function(event) {
       var incoming = this.$(".incoming").get(0);
       event.streams.forEach(function(stream) {
         if (stream.connection.connectionId !==
             this.model.session.connection.connectionId) {
-          this.model.session.subscribe(stream, incoming, this.videoStyles);
+          this.model.session.subscribe(stream, incoming, this.publisherConfig);
         }
-      }.bind(this));
+      }, this);
     },
 
     /**
      * Hangs up current conversation.
      *
      * @param  {MouseEvent} event
      */
     hangup: function(event) {
       event.preventDefault();
       this.unpublish();
       this.model.endSession();
     },
 
     /**
+     * Toggles audio mute state.
+     *
+     * @param  {MouseEvent} event
+     */
+    toggleMuteAudio: function(event) {
+      event.preventDefault();
+      if (!this.localStream) {
+        return;
+      }
+      var msgId;
+      var $button = this.$(".btn-mute-audio");
+      var enabled = !this.localStream.hasAudio;
+      this.publisher.publishAudio(enabled);
+      if (enabled) {
+        msgId = "mute_local_audio_button.title";
+        $button.removeClass("muted");
+      } else {
+        msgId = "unmute_local_audio_button.title";
+        $button.addClass("muted");
+      }
+      $button.attr("title", l10n.get(msgId));
+    },
+
+    /**
+     * Toggles video mute state.
+     *
+     * @param  {MouseEvent} event
+     */
+    toggleMuteVideo: function(event) {
+      event.preventDefault();
+      if (!this.localStream) {
+        return;
+      }
+      var msgId;
+      var $button = this.$(".btn-mute-video");
+      var enabled = !this.localStream.hasVideo;
+      this.publisher.publishVideo(enabled);
+      if (enabled) {
+        $button.removeClass("muted");
+        msgId = "mute_local_video_button.title";
+      } else {
+        $button.addClass("muted");
+        msgId = "unmute_local_video_button.title";
+      }
+      $button.attr("title", l10n.get(msgId));
+    },
+
+    /**
      * Publishes remote streams available once a session is connected.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
      *
      * @param  {SessionConnectEvent} event
      */
     publish: function(event) {
       var outgoing = this.$(".outgoing").get(0);
 
-      this.publisher = this.sdk.initPublisher(outgoing, this.videoStyles);
+      this.publisher = this.sdk.initPublisher(outgoing, this.publisherConfig);
 
       // Suppress OT GuM custom dialog, see bug 1018875
       function preventOpeningAccessDialog(event) {
         event.preventDefault();
       }
       this.publisher.on("accessDialogOpened", preventOpeningAccessDialog);
       this.publisher.on("accessDenied", preventOpeningAccessDialog);
+      this.publisher.on("streamCreated", function(event) {
+        this.localStream = event.stream;
+        if (this.localStream.hasAudio) {
+          this.$(".btn-mute-audio").addClass("streaming");
+        }
+        if (this.localStream.hasVideo) {
+          this.$(".btn-mute-video").addClass("streaming");
+        }
+      }.bind(this));
+      this.publisher.on("streamDestroyed", function() {
+        this.localStream = null;
+        this.$(".btn-mute-audio").removeClass("streaming muted");
+        this.$(".btn-mute-video").removeClass("streaming muted");
+      }.bind(this));
 
       this.model.session.publish(this.publisher);
     },
 
     /**
      * Unpublishes local stream.
      */
     unpublish: function() {
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -1,36 +1,66 @@
 # 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/.
 
 browser.jar:
-  content/browser/loop/conversation.html             (content/conversation.html)
-  content/browser/loop/panel.html                    (content/panel.html)
-  content/browser/loop/shared/css/common.css         (content/shared/css/common.css)
-  content/browser/loop/shared/css/panel.css          (content/shared/css/panel.css)
-  content/browser/loop/shared/css/conversation.css   (content/shared/css/conversation.css)
-  content/browser/loop/shared/img/icon_32.png        (content/shared/img/icon_32.png)
-  content/browser/loop/shared/img/icon_64.png        (content/shared/img/icon_64.png)
-  content/browser/loop/shared/img/loading-icon.gif   (content/shared/img/loading-icon.gif)
-  content/browser/loop/shared/js/models.js           (content/shared/js/models.js)
-  content/browser/loop/shared/js/router.js           (content/shared/js/router.js)
-  content/browser/loop/shared/js/views.js            (content/shared/js/views.js)
-  content/browser/loop/shared/libs/react-0.10.0.js   (content/shared/libs/react-0.10.0.js)
-  content/browser/loop/shared/libs/lodash-2.4.1.js   (content/shared/libs/lodash-2.4.1.js)
-  content/browser/loop/shared/libs/jquery-2.1.0.js   (content/shared/libs/jquery-2.1.0.js)
-  content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
-  content/browser/loop/shared/sounds/Firefox-Long.ogg   (content/shared/sounds/Firefox-Long.ogg)
-  content/browser/loop/libs/l10n.js                  (content/libs/l10n.js)
-  content/browser/loop/js/client.js                  (content/js/client.js)
-  content/browser/loop/js/conversation.js            (content/js/conversation.js)
-  content/browser/loop/js/desktopRouter.js           (content/js/desktopRouter.js)
-  content/browser/loop/js/panel.js                   (content/js/panel.js)
+  # Desktop html files
+  content/browser/loop/conversation.html            (content/conversation.html)
+  content/browser/loop/panel.html                   (content/panel.html)
+
+  # Desktop libs (see bottom of this file for TokBox sdk assets)
+  content/browser/loop/libs/l10n.js                 (content/libs/l10n.js)
+
+  # Desktop script
+  content/browser/loop/js/client.js                 (content/js/client.js)
+  content/browser/loop/js/desktopRouter.js          (content/js/desktopRouter.js)
+  content/browser/loop/js/conversation.js           (content/js/conversation.js)
+  content/browser/loop/js/panel.js                  (content/js/panel.js)
+
+  # Shared styles
+  content/browser/loop/shared/css/common.css        (content/shared/css/common.css)
+  content/browser/loop/shared/css/panel.css         (content/shared/css/panel.css)
+  content/browser/loop/shared/css/conversation.css  (content/shared/css/conversation.css)
+
+  # Shared images
+  content/browser/loop/shared/img/icon_32.png                   (content/shared/img/icon_32.png)
+  content/browser/loop/shared/img/icon_64.png                   (content/shared/img/icon_64.png)
+  content/browser/loop/shared/img/loading-icon.gif              (content/shared/img/loading-icon.gif)
+  content/browser/loop/shared/img/audio-highlight-14x14.png     (content/shared/img/audio-highlight-14x14.png)
+  content/browser/loop/shared/img/audio-highlight-14x14@2x.png  (content/shared/img/audio-highlight-14x14@2x.png)
+  content/browser/loop/shared/img/audio-inverse-14x14.png       (content/shared/img/audio-inverse-14x14.png)
+  content/browser/loop/shared/img/audio-inverse-14x14@2x.png    (content/shared/img/audio-inverse-14x14@2x.png)
+  content/browser/loop/shared/img/facemute-14x14.png            (content/shared/img/facemute-14x14.png)
+  content/browser/loop/shared/img/facemute-14x14@2x.png         (content/shared/img/facemute-14x14@2x.png)
+  content/browser/loop/shared/img/hangup-inverse-14x14.png      (content/shared/img/hangup-inverse-14x14.png)
+  content/browser/loop/shared/img/hangup-inverse-14x14@2x.png   (content/shared/img/hangup-inverse-14x14@2x.png)
+  content/browser/loop/shared/img/mute-inverse-14x14.png        (content/shared/img/mute-inverse-14x14.png)
+  content/browser/loop/shared/img/mute-inverse-14x14@2x.png     (content/shared/img/mute-inverse-14x14@2x.png)
+  content/browser/loop/shared/img/video-highlight-14x14.png     (content/shared/img/video-highlight-14x14.png)
+  content/browser/loop/shared/img/video-highlight-14x14@2x.png  (content/shared/img/video-highlight-14x14@2x.png)
+  content/browser/loop/shared/img/video-inverse-14x14.png       (content/shared/img/video-inverse-14x14.png)
+  content/browser/loop/shared/img/video-inverse-14x14@2x.png    (content/shared/img/video-inverse-14x14@2x.png)
+
+  # Shared scripts
+  content/browser/loop/shared/js/models.js  (content/shared/js/models.js)
+  content/browser/loop/shared/js/router.js  (content/shared/js/router.js)
+  content/browser/loop/shared/js/views.js   (content/shared/js/views.js)
+
+  # Shared libs
+  content/browser/loop/shared/libs/react-0.10.0.js    (content/shared/libs/react-0.10.0.js)
+  content/browser/loop/shared/libs/lodash-2.4.1.js    (content/shared/libs/lodash-2.4.1.js)
+  content/browser/loop/shared/libs/jquery-2.1.0.js    (content/shared/libs/jquery-2.1.0.js)
+  content/browser/loop/shared/libs/backbone-1.1.2.js  (content/shared/libs/backbone-1.1.2.js)
+
+  # Shared sounds
+  content/browser/loop/shared/sounds/Firefox-Long.ogg (content/shared/sounds/Firefox-Long.ogg)
+
   # Partner SDK assets
-  content/browser/loop/libs/sdk.js                                             (content/libs/sdk.js)
+  content/browser/loop/libs/sdk.js                                                    (content/libs/sdk.js)
   content/browser/loop/otcdn/webrtc/v2.2.5/css/ot.min.css                             (content/libs/otcdn/webrtc/v2.2.5/css/ot.min.css)
   content/browser/loop/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js                   (content/libs/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-chrome.png        (content/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-chrome.png)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-copy-firefox.png  (content/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-copy-firefox.png)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-firefox.png       (content/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-firefox.png)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-predenied-chrome.png     (content/libs/otcdn/webrtc/v2.2.5/images/rtc/access-predenied-chrome.png)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-prompt-chrome.png        (content/libs/otcdn/webrtc/v2.2.5/images/rtc/access-prompt-chrome.png)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/audioonly-publisher.png         (content/libs/otcdn/webrtc/v2.2.5/images/rtc/audioonly-publisher.png)
--- a/browser/components/loop/standalone/content/l10n/data.ini
+++ b/browser/components/loop/standalone/content/l10n/data.ini
@@ -1,32 +1,40 @@
 [en]
 call_has_ended=Your call has ended.
 missing_conversation_info=Missing conversation information.
 network_disconnected=The network connection terminated abruptly.
 peer_ended_conversation=Your peer ended the conversation.
 unable_retrieve_call_info=Unable to retrieve conversation information.
-stop=Stop
+hangup_button.title=Hangup
+mute_local_audio_button.title=Mute your audio
+unmute_local_audio_button.title=Unute your audio
+mute_local_video_button.title=Mute your video
+unmute_local_video_button.title=Unmute your video
 start_call=Start the call
 welcome=Welcome to the Loop web client.
 incompatible_browser=Incompatible Browser
 powered_by_webrtc=The audio and video components of Loop are powered by WebRTC.
 use_latest_firefox.innerHTML=To use Loop, please use the latest version of <a href="{{ff_url}}">Firefox</a>.
 incompatible_device=Incompatible device
 sorry_device_unsupported=Sorry, Loop does not currently support your device.
 use_firefox_windows_mac_linux=Please open this page using the latest Firefox on Windows, Android, Mac or Linux.
 connection_error_see_console_notification=Call failed; see console for details.
 
 [fr]
 call_has_ended=L'appel est terminé.
 missing_conversation_info=Informations de communication manquantes.
 network_disconnected=La connexion réseau semble avoir été interrompue.
 peer_ended_conversation=Votre correspondant a mis fin à la communication.
 unable_retrieve_call_info=Impossible de récupérer les informations liées à cet appel.
-stop=Arrêter
+hangup_button.title=Terminer l'appel
+mute_local_audio_button.title=Couper la diffusion audio
+unmute_local_audio_button.title=Reprendre la diffusion audio
+mute_local_video_button.title=Couper la diffusion vidéo
+unmute_local_video_button.title=Reprendre la diffusion vidéo
 start_call=Démarrer l'appel
 welcome=Bienvenue sur Loop.
 incompatible_browser=Navigateur non supporté
 powered_by_webrtc=Les fonctionnalités audio et vidéo de Loop utilisent WebRTC.
 use_latest_firefox.innerHTML=Pour utiliser Loop, merci d'utiliser la dernière version de <a href="{{ff_url}}">Firefox</a>.
 incompatible_device=Plateforme non supportée
 sorry_device_unsupported=Désolé, Loop ne fonctionne actuellement pas sur votre appareil.
 use_firefox_windows_mac_linux=Merci d'ouvrir cette page avec une version récente de Firefox pour Windows, Android, Mac ou Linux.
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -54,17 +54,19 @@ describe("loop.shared.views", function()
         connect: sandbox.spy(),
         disconnect: sandbox.spy(),
         publish: sandbox.spy(),
         unpublish: sandbox.spy(),
         subscribe: sandbox.spy()
       }, Backbone.Events);
       fakePublisher = {
         on: sandbox.spy(),
-        off: sandbox.spy()
+        off: sandbox.spy(),
+        publishAudio: sandbox.spy(),
+        publishVideo: sandbox.spy()
       };
       fakeSDK = {
         initPublisher: sandbox.stub().returns(fakePublisher),
         initSession: sandbox.stub().returns(fakeSession)
       };
       model = new sharedModels.ConversationModel(fakeSessionData, {
         sdk: fakeSDK
       });
@@ -119,17 +121,17 @@ describe("loop.shared.views", function()
           sinon.assert.calledOnce(fakeSession.publish);
         });
 
         it("should start listening to OT publisher accessDialogOpened and " +
           " accessDenied events",
           function() {
             view.publish();
 
-            sinon.assert.calledTwice(fakePublisher.on);
+            sinon.assert.called(fakePublisher.on);
             sinon.assert.calledWith(fakePublisher.on, "accessDialogOpened");
             sinon.assert.calledWith(fakePublisher.on, "accessDenied");
           });
       });
 
       describe("#unpublish", function() {
         var view;
 
@@ -152,16 +154,76 @@ describe("loop.shared.views", function()
             view.unpublish();
 
             sinon.assert.calledTwice(fakePublisher.off);
             sinon.assert.calledWith(fakePublisher.off, "accessDialogOpened");
             sinon.assert.calledWith(fakePublisher.off, "accessDenied");
           });
       });
 
+      describe("#toggleMuteAudio", function() {
+        var view;
+
+        beforeEach(function() {
+          view = new sharedViews.ConversationView({
+            sdk: fakeSDK,
+            model: model
+          });
+          view.publish();
+        });
+
+        it("should unpublish local audio when enabled", function() {
+          view.localStream = {hasAudio: true};
+
+          view.toggleMuteAudio({preventDefault: sandbox.spy()});
+
+          sinon.assert.calledOnce(fakePublisher.publishAudio);
+          sinon.assert.calledWithExactly(fakePublisher.publishAudio, false);
+        });
+
+        it("should publish local audio when disabled", function() {
+          view.localStream = {hasAudio: false};
+
+          view.toggleMuteAudio({preventDefault: sandbox.spy()});
+
+          sinon.assert.calledOnce(fakePublisher.publishAudio);
+          sinon.assert.calledWithExactly(fakePublisher.publishAudio, true);
+        });
+      });
+
+      describe("#toggleMuteVideo", function() {
+        var view;
+
+        beforeEach(function() {
+          view = new sharedViews.ConversationView({
+            sdk: fakeSDK,
+            model: model
+          });
+          view.publish();
+        });
+
+        it("should unpublish local video when enabled", function() {
+          view.localStream = {hasVideo: true};
+
+          view.toggleMuteVideo({preventDefault: sandbox.spy()});
+
+          sinon.assert.calledOnce(fakePublisher.publishVideo);
+          sinon.assert.calledWithExactly(fakePublisher.publishVideo, false);
+        });
+
+        it("should publish local video when disabled", function() {
+          view.localStream = {hasVideo: false};
+
+          view.toggleMuteVideo({preventDefault: sandbox.spy()});
+
+          sinon.assert.calledOnce(fakePublisher.publishVideo);
+          sinon.assert.calledWithExactly(fakePublisher.publishVideo, true);
+        });
+      });
+
       describe("Model events", function() {
         var view;
 
         beforeEach(function() {
           sandbox.stub(sharedViews.ConversationView.prototype, "publish");
           sandbox.stub(sharedViews.ConversationView.prototype, "unpublish");
           view = new sharedViews.ConversationView({sdk: fakeSDK, model: model});
         });
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -15,17 +15,21 @@ call_identifier_textinput_placeholder=Id
 unable_retrieve_url=Sorry, we were unable to retrieve a call url.
 
 # Conversation Window Strings
 
 incoming_call_title=Incoming Call…
 incoming_call=Incoming call
 accept_button=Accept
 decline_button=Decline
-stop=Stop
+hangup_button.title=Hangup
+mute_local_audio_button.title=Mute your audio
+unmute_local_audio_button.title=Unute your audio
+mute_local_video_button.title=Mute your video
+unmute_local_video_button.title=Unmute your video
 
 peer_ended_conversation=Your peer ended the conversation.
 call_has_ended=Your call has ended.
 close_window=Close this window
 
 cannot_start_call_session_not_ready=Can't start call, session is not ready.
 network_disconnected=The network connection terminated abruptly.