Bug 1000152: Add mute & unmute buttons to Loop conversation window. r=Standard8

This commit is contained in:
Nicolas Perriault 2014-06-27 12:34:22 +02:00
parent a506600ef9
commit a7eff06d5d
21 changed files with 416 additions and 46 deletions

View File

@ -4,11 +4,128 @@
/* Conversation window styles */
.conversation .controls {
background: #f2f2f2;
padding: .5em;
.conversation {
position: relative;
}
.conversation .controls {
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;
}
@ -20,13 +137,16 @@
}
.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;
}

View File

@ -1,4 +1,4 @@
<!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/. -->
@ -98,10 +98,11 @@
<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>
@ -112,6 +113,11 @@
<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>
@ -128,6 +134,68 @@
</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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

View File

@ -94,10 +94,21 @@ loop.shared.views = (function(_, OT, l10n) {
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,
@ -110,14 +121,19 @@ loop.shared.views = (function(_, OT, l10n) {
// 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'
},
/**
@ -153,9 +169,9 @@ loop.shared.views = (function(_, OT, l10n) {
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);
},
/**
@ -169,6 +185,54 @@ loop.shared.views = (function(_, OT, l10n) {
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.
*
@ -179,7 +243,7 @@ loop.shared.views = (function(_, OT, l10n) {
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) {
@ -187,6 +251,20 @@ loop.shared.views = (function(_, OT, l10n) {
}
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);
},

View File

@ -3,29 +3,59 @@
# 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)

View File

@ -4,7 +4,11 @@ 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
@ -21,7 +25,11 @@ 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é

View File

@ -59,7 +59,9 @@ describe("loop.shared.views", function() {
}, Backbone.Events);
fakePublisher = {
on: sandbox.spy(),
off: sandbox.spy()
off: sandbox.spy(),
publishAudio: sandbox.spy(),
publishVideo: sandbox.spy()
};
fakeSDK = {
initPublisher: sandbox.stub().returns(fakePublisher),
@ -124,7 +126,7 @@ describe("loop.shared.views", function() {
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");
});
@ -157,6 +159,66 @@ describe("loop.shared.views", function() {
});
});
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;

View File

@ -20,7 +20,11 @@ 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.