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 192759 416d7931e3490c416b8544344eb32b8213bdd2b1
parent 192758 ccebb68bdf07420f01c29f71824464d532dc48bc
child 192760 7e27f2f4793c08f1147470a9196f7107131e9f51
push id45931
push usermbanner@mozilla.com
push dateTue, 08 Jul 2014 08:51:17 +0000
treeherdermozilla-inbound@7e27f2f4793c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1000152
milestone33.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 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..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.