Audio levels redesign.

This commit is contained in:
yanas 2016-09-28 16:31:40 -05:00
parent 4ec266ef11
commit 3bb877cc3a
12 changed files with 207 additions and 369 deletions

View File

@ -46,6 +46,8 @@ $participantNameColor: #fff;
$thumbnailPictogramColor: #fff;
$dominantSpeakerBg: #165ecc;
$raiseHandBg: #D6D61E;
$audioLevelBg: #44A5FF;
$audioLevelBorder: rgba(14, 56, 121, .5);
$rateStarDefault: #ccc;
$rateStarActivity: #165ecc;

View File

@ -363,6 +363,36 @@
background: $raiseHandBg;
}
/**
* Audio indicator on video thumbnails.
*/
.videocontainer>span.audioindicator {
position: absolute;
display: inline-block;
left: 6px;
top: 50%;
margin-top: -21px;
width: 6px;
height: 42px;
z-index: 2;
border: none;
.audiodot-top,
.audiodot-bottom {
opacity: 0;
display: inline-block;
@include circle(4px);
background: $audioLevelBg;
margin: 1px 0 1px 0;
-webkit-filter: blur(0.5px);
filter: blur(0.5px);
border: 1px solid rgba(0, 0, 0, .5);
transition: opacity .25s ease-in-out;
-moz-transition: opacity .25s ease-in-out;
z-index: 2;
}
}
#indicatoricon {
width: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
@ -399,25 +429,20 @@
width: 300px;
height: 300px;
margin: auto;
overflow: hidden;
position: relative;
}
#dominantSpeakerAudioLevel {
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
visibility: inherit;
}
#mixedstream {
display:none !important;
}
#dominantSpeakerAvatar {
#dominantSpeakerAvatar,
.dynamic-shadow {
width: 200px;
height: 200px;
}
#dominantSpeakerAvatar {
top: 50px;
margin: auto;
position: relative;
@ -427,6 +452,15 @@
background-color: #000000;
}
.dynamic-shadow {
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
margin: -100px 0 0 -100px;
transition: box-shadow 0.3s ease;
}
.userAvatar {
@include circle(60px);
@include absoluteAligning(60px, 60px);

View File

@ -225,8 +225,8 @@
<span data-i18n="poweredby"></span> jitsi.org
</a>
<div id="dominantSpeaker">
<div class="dynamic-shadow"></div>
<img id="dominantSpeakerAvatar" src=""/>
<canvas id="dominantSpeakerAudioLevel"></canvas>
</div>
<span id="remoteConnectionMessage"></span>
<div id="largeVideoWrapper">

View File

@ -41,5 +41,7 @@ var interfaceConfig = {
REMOTE_THUMBNAIL_RATIO_HEIGHT: 1,
// Enables feedback star animation.
ENABLE_FEEDBACK_ANIMATION: false,
DISABLE_FOCUS_INDICATOR: false
};
DISABLE_FOCUS_INDICATOR: false,
AUDIO_LEVEL_PRIMARY_COLOR: "rgba(255,255,255,0.7)",
AUDIO_LEVEL_SECONDARY_COLOR: "rgba(255,255,255,0.4)"
};

View File

@ -1,183 +1,36 @@
/* global APP, interfaceConfig, $ */
/* jshint -W101 */
import CanvasUtil from './CanvasUtils';
import FilmStrip from '../videolayout/FilmStrip';
const LOCAL_LEVEL = 'local';
let ASDrawContext = null;
let audioLevelCanvasCache = {};
let dominantSpeakerAudioElement = null;
function _initDominantSpeakerAudioLevels(dominantSpeakerAvatarSize) {
let ASRadius = dominantSpeakerAvatarSize / 2;
let ASCenter = (dominantSpeakerAvatarSize + ASRadius) / 2;
// Draw a circle.
ASDrawContext.beginPath();
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
ASDrawContext.closePath();
// Add a shadow around the circle
ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR;
ASDrawContext.shadowOffsetX = 0;
ASDrawContext.shadowOffsetY = 0;
}
/* global interfaceConfig */
import UIUtil from "../util/UIUtil";
/**
* Resizes the given audio level canvas to match the given thumbnail size.
*/
function _resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
}
/**
* Draws the audio level canvas into the cached canvas object.
*
* @param id of the user for whom we draw the audio level
* @param audioLevel the newAudio level to render
*/
function drawAudioLevelCanvas(id, audioLevel) {
if (!audioLevelCanvasCache[id]) {
let videoSpanId = getVideoSpanId(id);
let audioLevelCanvasOrig = $(`#${videoSpanId}>canvas`).get(0);
/*
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
* In such a case, the method CanvasUtil.cloneCanvas may throw an
* error. Since audio levels are frequently updated, the errors have
* been observed to pile into the console, strain the CPU.
*/
if (audioLevelCanvasOrig) {
audioLevelCanvasCache[id]
= CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
}
}
let canvas = audioLevelCanvasCache[id];
if (!canvas) {
return;
}
let drawContext = canvas.getContext('2d');
drawContext.clearRect(0, 0, canvas.width, canvas.height);
let shadowLevel = getShadowLevel(audioLevel);
if (shadowLevel > 0) {
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
CanvasUtil.drawRoundRectGlow(
drawContext,
interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
canvas.width - interfaceConfig.CANVAS_EXTRA,
canvas.height - interfaceConfig.CANVAS_EXTRA,
interfaceConfig.CANVAS_RADIUS,
interfaceConfig.SHADOW_COLOR,
shadowLevel);
}
}
/**
* Returns the shadow/glow level for the given audio level.
*
* @param audioLevel the audio level from which we determine the shadow
* level
*/
function getShadowLevel (audioLevel) {
let shadowLevel = 0;
if (audioLevel <= 0.3) {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
} else if (audioLevel <= 0.6) {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
} else {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
}
/**
* Returns the video span id corresponding to the given user id
*/
function getVideoSpanId(id) {
let videoSpanId = null;
if (id === LOCAL_LEVEL || APP.conference.isLocalId(id)) {
videoSpanId = 'localVideoContainer';
} else {
videoSpanId = `participant_${id}`;
}
return videoSpanId;
}
/**
* The audio Levels plugin.
* Responsible for drawing audio levels.
*/
const AudioLevels = {
init () {
dominantSpeakerAudioElement = $('#dominantSpeakerAudioLevel')[0];
ASDrawContext = dominantSpeakerAudioElement.getContext('2d');
let parentContainer = $("#dominantSpeaker");
let dominantSpeakerWidth = parentContainer.width();
let dominantSpeakerHeight = parentContainer.height();
dominantSpeakerAudioElement.width = dominantSpeakerWidth;
dominantSpeakerAudioElement.height = dominantSpeakerHeight;
let dominantSpeakerAvatar = $("#dominantSpeakerAvatar");
_initDominantSpeakerAudioLevels(dominantSpeakerAvatar.width());
},
/**
* The number of dots per class. We have 2 classes of dots "top" and
* "bottom". The total number of dots will be twice the below value.
*/
_AUDIO_LEVEL_DOTS: 3,
/**
* Updates the audio level canvas for the given id. If the canvas
* didn't exist we create it.
* Creates the audio level indicator span element.
*
* @return {Element} the document element representing audio levels
*/
createAudioLevelCanvas (videoSpanId, thumbWidth, thumbHeight) {
createThumbnailAudioLevelIndicator() {
let videoSpan = document.getElementById(videoSpanId);
let audioSpan = document.createElement('span');
audioSpan.className = 'audioindicator';
if (!videoSpan) {
if (videoSpanId) {
console.error("No video element for id", videoSpanId);
} else {
console.error("No video element for local video.");
}
return;
}
let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel";
audioLevelCanvas.style.bottom
= `-${interfaceConfig.CANVAS_EXTRA/2}px`;
audioLevelCanvas.style.left
= `-${interfaceConfig.CANVAS_EXTRA/2}px`;
_resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
videoSpan.appendChild(audioLevelCanvas);
} else {
audioLevelCanvas = audioLevelCanvas.get(0);
_resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
for (let i = 0; i < this._AUDIO_LEVEL_DOTS*2; i++) {
var audioDot = document.createElement('span');
audioDot.className = (i < this._AUDIO_LEVEL_DOTS)
? "audiodot-top"
: "audiodot-bottom";
audioSpan.appendChild(audioDot);
}
return audioSpan;
},
/**
@ -186,74 +39,93 @@ const AudioLevels = {
* @param id id of the user for whom we draw the audio level
* @param audioLevel the newAudio level to render
*/
updateAudioLevel (id, audioLevel, largeVideoId) {
drawAudioLevelCanvas(id, audioLevel);
updateThumbnailAudioLevel (id, audioLevel) {
let audioSpan = document.getElementById(id)
.getElementsByClassName("audioindicator");
let videoSpanId = getVideoSpanId(id);
let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0);
if (!audioLevelCanvas) {
if (audioSpan && audioSpan.length > 0)
audioSpan = audioSpan[0];
else
return;
let audioTopDots
= audioSpan.getElementsByClassName("audiodot-top");
let audioBottomDots
= audioSpan.getElementsByClassName("audiodot-bottom");
let coloredDots = Math.round(this._AUDIO_LEVEL_DOTS*audioLevel);
let topColoredDots = this._AUDIO_LEVEL_DOTS - coloredDots;
for (let i = 0; i < audioTopDots.length; i++) {
if (i < topColoredDots)
audioTopDots[i].style.opacity = 0;
else if (i === topColoredDots && topColoredDots > 0)
audioTopDots[i].style.opacity = 0.5;
else
audioTopDots[i].style.opacity = 1;
}
let drawContext = audioLevelCanvas.getContext('2d');
let canvasCache = audioLevelCanvasCache[id];
drawContext.clearRect(
0, 0, audioLevelCanvas.width, audioLevelCanvas.height
);
drawContext.drawImage(canvasCache, 0, 0);
if (id === LOCAL_LEVEL) {
id = APP.conference.getMyUserId();
if (!id) {
return;
}
}
if(id === largeVideoId) {
window.requestAnimationFrame(function () {
AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
});
for (let i = 0; i < audioBottomDots.length; i++) {
if (i < coloredDots)
audioBottomDots[i].style.opacity = 1;
else if (i === coloredDots && coloredDots > 0)
audioBottomDots[i].style.opacity = 0.5;
else
audioBottomDots[i].style.opacity = 0;
}
},
updateDominantSpeakerAudioLevel (audioLevel) {
if($("#dominantSpeaker").css("visibility") == "hidden"
|| ASDrawContext === null) {
/**
* Updates the audio level of the large video.
*
* @param audioLevel the new audio level to set.
*/
updateLargeVideoAudioLevel(elementId, audioLevel) {
let element = document.getElementById(elementId);
if(!UIUtil.isVisible(element))
return;
}
ASDrawContext.clearRect(0, 0,
dominantSpeakerAudioElement.width,
dominantSpeakerAudioElement.height);
let level = parseFloat(audioLevel);
if (!audioLevel) {
return;
}
level = isNaN(level) ? 0 : level;
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
let shadowElement = element.getElementsByClassName("dynamic-shadow");
// Fill the shape.
ASDrawContext.fill();
if (shadowElement && shadowElement.length > 0)
shadowElement = shadowElement[0];
shadowElement.style.boxShadow = this._updateLargeVideoShadow(level);
},
updateCanvasSize (localVideo, remoteVideo) {
let { remoteThumbs, localThumb } = FilmStrip.getThumbs();
/**
* Updates the large video shadow effect.
*/
_updateLargeVideoShadow (level) {
var scale = 2,
remoteThumbs.each(( index, element ) => {
this.createAudioLevelCanvas(element.id,
remoteVideo.thumbWidth,
remoteVideo.thumbHeight);
});
// Internal circle audio level.
int = {
level: level > 0.15 ? 20 : 0,
color: interfaceConfig.AUDIO_LEVEL_PRIMARY_COLOR
},
if (localThumb) {
this.createAudioLevelCanvas(localThumb.get(0).id,
localVideo.thumbWidth,
localVideo.thumbHeight);
}
// External circle audio level.
ext = {
level: (int.level * scale * level + int.level).toFixed(0),
color: interfaceConfig.AUDIO_LEVEL_SECONDARY_COLOR
};
// Internal blur.
int.blur = int.level ? 2 : 0;
// External blur.
ext.blur = ext.level ? 6 : 0;
return [
`0 0 ${ int.blur }px ${ int.level }px ${ int.color }`,
`0 0 ${ ext.blur }px ${ ext.level }px ${ ext.color }`
].join(', ');
}
};

View File

@ -1,108 +0,0 @@
/**
* Utility class for drawing canvas shapes.
*/
const CanvasUtil = {
/**
* Draws a round rectangle with a glow. The glowWidth indicates the depth
* of the glow.
*
* @param drawContext the context of the canvas to draw to
* @param x the x coordinate of the round rectangle
* @param y the y coordinate of the round rectangle
* @param w the width of the round rectangle
* @param h the height of the round rectangle
* @param glowColor the color of the glow
* @param glowWidth the width of the glow
*/
drawRoundRectGlow (drawContext, x, y, w, h, r, glowColor, glowWidth) {
// Save the previous state of the context.
drawContext.save();
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
// Draw a round rectangle.
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// Add a shadow around the rectangle
drawContext.shadowColor = glowColor;
drawContext.shadowBlur = glowWidth;
drawContext.shadowOffsetX = 0;
drawContext.shadowOffsetY = 0;
// Fill the shape.
drawContext.fill();
drawContext.save();
drawContext.restore();
// 1) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.globalCompositeOperation='destination-out';
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// 2) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.fill();
// Comment these two lines if choosing to do the same with composite
// operation above 1 and 2.
drawContext.clip();
drawContext.clearRect(0, 0, 277, 200);
// Restore the previous context state.
drawContext.restore();
},
/**
* Clones the given canvas.
*
* @return the new cloned canvas.
*/
cloneCanvas (oldCanvas) {
/*
* FIXME Testing has shown that oldCanvas may not exist. In such a case,
* the method CanvasUtil.cloneCanvas may throw an error. Since audio
* levels are frequently updated, the errors have been observed to pile
* into the console, strain the CPU.
*/
if (!oldCanvas)
return oldCanvas;
//create a new canvas
var newCanvas = document.createElement('canvas');
var context = newCanvas.getContext('2d');
//set dimensions
newCanvas.width = oldCanvas.width;
newCanvas.height = oldCanvas.height;
//apply the old canvas to the new one
context.drawImage(oldCanvas, 0, 0);
//return the new canvas
return newCanvas;
}
};
export default CanvasUtil;

View File

@ -266,7 +266,7 @@ const FilmStrip = {
return { remoteThumbs, localThumb };
}
},
}
};

View File

@ -5,8 +5,11 @@ import Avatar from "../avatar/Avatar";
import {createDeferred} from '../../util/helpers';
import UIUtil from "../util/UIUtil";
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
import LargeContainer from "./LargeContainer";
import AudioLevels from "../audio_levels/AudioLevels";
/**
* Manager for all Large containers.
*/
@ -307,6 +310,15 @@ export default class LargeVideoManager {
$("#dominantSpeakerAvatar").attr('src', avatarUrl);
}
/**
* Updates the audio level indicator of the large video.
*
* @param lvl the new audio level to set
*/
updateLargeVideoAudioLevel (lvl) {
AudioLevels.updateLargeVideoAudioLevel("dominantSpeaker", lvl);
}
/**
* Show or hide watermark.
* @param {boolean} show

View File

@ -28,6 +28,7 @@ function LocalVideo(VideoLayout, emitter) {
this.setDisplayName();
this.createConnectionIndicator();
this.addAudioLevelIndicator();
}
LocalVideo.prototype = Object.create(SmallVideo.prototype);

View File

@ -63,13 +63,12 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
let { remoteVideo } = this.VideoLayout.resizeThumbnails(false, true);
let { thumbHeight, thumbWidth } = remoteVideo;
AudioLevels.createAudioLevelCanvas(
this.videoSpanId, thumbWidth, thumbHeight);
this.addAudioLevelIndicator();
return this.container;
};
/**
* Initializes the remote participant popup menu, by specifying previously
* constructed popupMenuElement, containing all the menu items.

View File

@ -2,6 +2,7 @@
import Avatar from "../avatar/Avatar";
import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import AudioLevels from "../audio_levels/AudioLevels";
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
@ -266,7 +267,7 @@ SmallVideo.prototype.getAudioMutedIndicator = function () {
* @param {boolean} isMuted indicates if we should set the view to muted view
* or not
*/
SmallVideo.prototype.setMutedView = function(isMuted) {
SmallVideo.prototype.setVideoMutedView = function(isMuted) {
this.isVideoMuted = isMuted;
this.updateView();
@ -308,10 +309,11 @@ SmallVideo.prototype.getVideoMutedIndicator = function () {
};
/**
* Creates the element indicating the moderator(owner) of the conference.
* Adds the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.createModeratorIndicatorElement = function () {
// don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
SmallVideo.prototype.addModeratorIndicator = function () {
// Don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
if (interfaceConfig.DISABLE_FOCUS_INDICATOR)
return false;
@ -339,10 +341,33 @@ SmallVideo.prototype.createModeratorIndicatorElement = function () {
indicatorSpan.appendChild(moderatorIndicator);
};
/**
* Adds the element indicating the audio level of the participant.
*/
SmallVideo.prototype.addAudioLevelIndicator = function () {
var audioSpan = $('#' + this.videoSpanId + ' .audioindicator');
if (audioSpan.length) {
return;
}
this.container.appendChild(
AudioLevels.createThumbnailAudioLevelIndicator());
};
/**
* Updates the audio level for this small video.
*
* @param lvl the new audio level to set
*/
SmallVideo.prototype.updateAudioLevelIndicator = function (lvl) {
AudioLevels.updateThumbnailAudioLevel(this.videoSpanId, lvl);
};
/**
* Removes the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.removeModeratorIndicatorElement = function () {
SmallVideo.prototype.removeModeratorIndicator = function () {
$('#' + this.videoSpanId + ' .focusindicator').remove();
};

View File

@ -1,7 +1,6 @@
/* global config, APP, $, interfaceConfig, JitsiMeetJS */
/* jshint -W101 */
import AudioLevels from "../audio_levels/AudioLevels";
import Avatar from "../avatar/Avatar";
import FilmStrip from "./FilmStrip";
import UIEvents from "../../../service/UI/UIEvents";
@ -108,10 +107,6 @@ var VideoLayout = {
// if we do not resize the thumbs here, if there is no video device
// the local video thumb maybe one pixel
let { localVideo } = this.resizeThumbnails(false, true);
AudioLevels.createAudioLevelCanvas(
"localVideoContainer",
localVideo.thumbWidth,
localVideo.thumbHeight);
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
this.lastNCount = config.channelLastN;
@ -123,16 +118,21 @@ var VideoLayout = {
largeVideo.onLocalFlipXChange(localFlipX);
}
largeVideo.updateContainerSize();
AudioLevels.init();
},
/**
* Sets the audio level of the video elements associated to the given id.
*
* @param id the video identifier in the form it comes from the library
* @param lvl the new audio level to update to
*/
setAudioLevel(id, lvl) {
if (!largeVideo) {
return;
}
AudioLevels.updateAudioLevel(
id, lvl, largeVideo.id
);
let smallVideo = this.getSmallVideo(id);
if (smallVideo)
smallVideo.updateAudioLevelIndicator(lvl);
if (largeVideo && id === largeVideo.id)
largeVideo.updateLargeVideoAudioLevel(lvl);
},
isInLastN (resource) {
@ -469,9 +469,9 @@ var VideoLayout = {
showModeratorIndicator () {
let isModerator = APP.conference.isModerator;
if (isModerator) {
localVideoThumbnail.createModeratorIndicatorElement();
localVideoThumbnail.addModeratorIndicator();
} else {
localVideoThumbnail.removeModeratorIndicatorElement();
localVideoThumbnail.removeModeratorIndicator();
}
APP.conference.listMembers().forEach(function (member) {
@ -481,7 +481,7 @@ var VideoLayout = {
return;
if (member.isModerator()) {
remoteVideo.createModeratorIndicatorElement();
remoteVideo.addModeratorIndicator();
}
if (isModerator) {
@ -528,7 +528,6 @@ var VideoLayout = {
FilmStrip.resizeThumbnails(localVideo, remoteVideo,
animate, forceUpdate)
.then(function () {
AudioLevels.updateCanvasSize(localVideo, remoteVideo);
if (onComplete && typeof onComplete === "function")
onComplete();
});
@ -558,11 +557,11 @@ var VideoLayout = {
*/
onVideoMute (id, value) {
if (APP.conference.isLocalId(id)) {
localVideoThumbnail.setMutedView(value);
localVideoThumbnail.setVideoMutedView(value);
} else {
let remoteVideo = remoteVideos[id];
if (remoteVideo)
remoteVideo.setMutedView(value);
remoteVideo.setVideoMutedView(value);
}
if (this.isCurrentlyOnLarge(id)) {