Merge pull request #955 from jitsi/audio-levels-redesign
Audio levels redesign. PR Review done from @damencho and @emcho.
This commit is contained in:
commit
854fef35cb
|
@ -46,6 +46,8 @@ $participantNameColor: #fff;
|
||||||
$thumbnailPictogramColor: #fff;
|
$thumbnailPictogramColor: #fff;
|
||||||
$dominantSpeakerBg: #165ecc;
|
$dominantSpeakerBg: #165ecc;
|
||||||
$raiseHandBg: #D6D61E;
|
$raiseHandBg: #D6D61E;
|
||||||
|
$audioLevelBg: #44A5FF;
|
||||||
|
$audioLevelShadow: rgba(9, 36, 77, 0.9);
|
||||||
|
|
||||||
$rateStarDefault: #ccc;
|
$rateStarDefault: #ccc;
|
||||||
$rateStarActivity: #165ecc;
|
$rateStarActivity: #165ecc;
|
||||||
|
|
|
@ -363,6 +363,46 @@
|
||||||
background: $raiseHandBg;
|
background: $raiseHandBg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio indicator on video thumbnails.
|
||||||
|
*/
|
||||||
|
.videocontainer>span.audioindicator {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
left: 6px;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -17px;
|
||||||
|
width: 6px;
|
||||||
|
height: 35px;
|
||||||
|
z-index: 2;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.audiodot-top,
|
||||||
|
.audiodot-bottom,
|
||||||
|
.audiodot-middle {
|
||||||
|
opacity: 0;
|
||||||
|
display: inline-block;
|
||||||
|
@include circle(5px);
|
||||||
|
background: $audioLevelShadow;
|
||||||
|
margin: 1px 0 1px 0;
|
||||||
|
transition: opacity .25s ease-in-out;
|
||||||
|
-moz-transition: opacity .25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.audiodot-top::after,
|
||||||
|
span.audiodot-bottom::after,
|
||||||
|
span.audiodot-middle::after {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
-webkit-filter: blur(0.5px);
|
||||||
|
filter: blur(0.5px);
|
||||||
|
background: $audioLevelBg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#indicatoricon {
|
#indicatoricon {
|
||||||
width: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
|
width: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
|
||||||
height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
|
height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
|
||||||
|
@ -399,25 +439,20 @@
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dominantSpeakerAudioLevel {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
z-index: 2;
|
|
||||||
visibility: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mixedstream {
|
#mixedstream {
|
||||||
display:none !important;
|
display:none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dominantSpeakerAvatar {
|
#dominantSpeakerAvatar,
|
||||||
|
.dynamic-shadow {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dominantSpeakerAvatar {
|
||||||
top: 50px;
|
top: 50px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -427,6 +462,15 @@
|
||||||
background-color: #000000;
|
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 {
|
.userAvatar {
|
||||||
@include circle(60px);
|
@include circle(60px);
|
||||||
@include absoluteAligning(60px, 60px);
|
@include absoluteAligning(60px, 60px);
|
||||||
|
|
|
@ -225,8 +225,8 @@
|
||||||
<span data-i18n="poweredby"></span> jitsi.org
|
<span data-i18n="poweredby"></span> jitsi.org
|
||||||
</a>
|
</a>
|
||||||
<div id="dominantSpeaker">
|
<div id="dominantSpeaker">
|
||||||
|
<div class="dynamic-shadow"></div>
|
||||||
<img id="dominantSpeakerAvatar" src=""/>
|
<img id="dominantSpeakerAvatar" src=""/>
|
||||||
<canvas id="dominantSpeakerAudioLevel"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
<span id="remoteConnectionMessage"></span>
|
<span id="remoteConnectionMessage"></span>
|
||||||
<div id="largeVideoWrapper">
|
<div id="largeVideoWrapper">
|
||||||
|
|
|
@ -41,5 +41,7 @@ var interfaceConfig = {
|
||||||
REMOTE_THUMBNAIL_RATIO_HEIGHT: 1,
|
REMOTE_THUMBNAIL_RATIO_HEIGHT: 1,
|
||||||
// Enables feedback star animation.
|
// Enables feedback star animation.
|
||||||
ENABLE_FEEDBACK_ANIMATION: false,
|
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)"
|
||||||
|
};
|
|
@ -1,259 +1,165 @@
|
||||||
/* global APP, interfaceConfig, $ */
|
/* global interfaceConfig */
|
||||||
/* jshint -W101 */
|
|
||||||
|
|
||||||
import CanvasUtil from './CanvasUtils';
|
import UIUtil from "../util/UIUtil";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes the given audio level canvas to match the given thumbnail size.
|
* Responsible for drawing audio levels.
|
||||||
*/
|
|
||||||
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.
|
|
||||||
*/
|
*/
|
||||||
const AudioLevels = {
|
const AudioLevels = {
|
||||||
|
|
||||||
init () {
|
/**
|
||||||
dominantSpeakerAudioElement = $('#dominantSpeakerAudioLevel')[0];
|
* The number of dots.
|
||||||
ASDrawContext = dominantSpeakerAudioElement.getContext('2d');
|
*
|
||||||
|
* IMPORTANT: functions below assume that this is an odd number.
|
||||||
let parentContainer = $("#dominantSpeaker");
|
*/
|
||||||
let dominantSpeakerWidth = parentContainer.width();
|
_AUDIO_LEVEL_DOTS: 5,
|
||||||
let dominantSpeakerHeight = parentContainer.height();
|
|
||||||
|
|
||||||
dominantSpeakerAudioElement.width = dominantSpeakerWidth;
|
|
||||||
dominantSpeakerAudioElement.height = dominantSpeakerHeight;
|
|
||||||
|
|
||||||
let dominantSpeakerAvatar = $("#dominantSpeakerAvatar");
|
|
||||||
_initDominantSpeakerAudioLevels(dominantSpeakerAvatar.width());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the audio level canvas for the given id. If the canvas
|
* Creates the audio level indicator span element.
|
||||||
* didn't exist we create it.
|
*
|
||||||
|
* IMPORTANT: This function assumes that the number of dots is an
|
||||||
|
* odd number.
|
||||||
|
*
|
||||||
|
* @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) {
|
this.sideDotsCount = Math.floor(this._AUDIO_LEVEL_DOTS/2);
|
||||||
if (videoSpanId) {
|
|
||||||
console.error("No video element for id", videoSpanId);
|
for (let i = 0; i < this._AUDIO_LEVEL_DOTS; i++) {
|
||||||
} else {
|
let audioDot = document.createElement('span');
|
||||||
console.error("No video element for local video.");
|
|
||||||
}
|
// The median index will be equal to the number of dots on each
|
||||||
return;
|
// side.
|
||||||
}
|
if (i === this.sideDotsCount)
|
||||||
|
audioDot.className = "audiodot-middle";
|
||||||
let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
|
else
|
||||||
|
audioDot.className = (i < this.sideDotsCount)
|
||||||
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
|
? "audiodot-top"
|
||||||
|
: "audiodot-bottom";
|
||||||
audioLevelCanvas = document.createElement('canvas');
|
|
||||||
audioLevelCanvas.className = "audiolevel";
|
audioSpan.appendChild(audioDot);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return audioSpan;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the audio level UI for the given id.
|
* Updates the audio level UI for the given id.
|
||||||
*
|
*
|
||||||
* @param id id of the user for whom we draw the audio level
|
* @param {string} id id of the user for whom we draw the audio level
|
||||||
* @param audioLevel the newAudio level to render
|
* @param {number} audioLevel the newAudio level to render
|
||||||
*/
|
*/
|
||||||
updateAudioLevel (id, audioLevel, largeVideoId) {
|
updateThumbnailAudioLevel (id, audioLevel) {
|
||||||
drawAudioLevelCanvas(id, audioLevel);
|
|
||||||
|
|
||||||
let videoSpanId = getVideoSpanId(id);
|
// First make sure we are sensitive enough.
|
||||||
|
audioLevel *= 1.2;
|
||||||
|
audioLevel = Math.min(audioLevel, 1);
|
||||||
|
|
||||||
let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0);
|
// Let's now stretch the audio level over the number of dots we have.
|
||||||
|
let stretchedAudioLevel = (this.sideDotsCount + 1) * audioLevel;
|
||||||
|
let dotLevel = 0.0;
|
||||||
|
|
||||||
if (!audioLevelCanvas) {
|
for (let i = 0; i < (this.sideDotsCount + 1); i++) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let drawContext = audioLevelCanvas.getContext('2d');
|
dotLevel = Math.min(1, Math.max(0, (stretchedAudioLevel - i)));
|
||||||
|
this._setDotLevel(id, i, dotLevel);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDominantSpeakerAudioLevel (audioLevel) {
|
/**
|
||||||
if($("#dominantSpeaker").css("visibility") == "hidden"
|
* Fills the dot(s) with the specified "index", with as much opacity as
|
||||||
|| ASDrawContext === null) {
|
* indicated by "opacity".
|
||||||
|
*
|
||||||
|
* @param {string} elementID the parent audio indicator span element
|
||||||
|
* @param {number} index the index of the dots to fill, where 0 indicates
|
||||||
|
* the middle dot and the following increments point toward the
|
||||||
|
* corresponding pair of dots.
|
||||||
|
* @param {number} opacity the opacity to set for the specified dot.
|
||||||
|
*/
|
||||||
|
_setDotLevel(elementID, index, opacity) {
|
||||||
|
|
||||||
|
let audioSpan = document.getElementById(elementID)
|
||||||
|
.getElementsByClassName("audioindicator");
|
||||||
|
|
||||||
|
// Make sure the audio span is still around.
|
||||||
|
if (audioSpan && audioSpan.length > 0)
|
||||||
|
audioSpan = audioSpan[0];
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
|
||||||
|
let audioTopDots
|
||||||
|
= audioSpan.getElementsByClassName("audiodot-top");
|
||||||
|
let audioDotMiddle
|
||||||
|
= audioSpan.getElementsByClassName("audiodot-middle");
|
||||||
|
let audioBottomDots
|
||||||
|
= audioSpan.getElementsByClassName("audiodot-bottom");
|
||||||
|
|
||||||
|
// First take care of the middle dot case.
|
||||||
|
if (index === 0){
|
||||||
|
audioDotMiddle[0].style.opacity = opacity;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ASDrawContext.clearRect(0, 0,
|
// Index > 0 : we are setting non-middle dots.
|
||||||
dominantSpeakerAudioElement.width,
|
index--;
|
||||||
dominantSpeakerAudioElement.height);
|
audioBottomDots[index].style.opacity = opacity;
|
||||||
|
audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
|
||||||
if (!audioLevel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
|
|
||||||
|
|
||||||
// Fill the shape.
|
|
||||||
ASDrawContext.fill();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCanvasSize (localVideo, remoteVideo) {
|
/**
|
||||||
let { remoteThumbs, localThumb } = FilmStrip.getThumbs();
|
* Updates the audio level of the large video.
|
||||||
|
*
|
||||||
|
* @param audioLevel the new audio level to set.
|
||||||
|
*/
|
||||||
|
updateLargeVideoAudioLevel(elementId, audioLevel) {
|
||||||
|
let element = document.getElementById(elementId);
|
||||||
|
|
||||||
remoteThumbs.each(( index, element ) => {
|
if(!UIUtil.isVisible(element))
|
||||||
this.createAudioLevelCanvas(element.id,
|
return;
|
||||||
remoteVideo.thumbWidth,
|
|
||||||
remoteVideo.thumbHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (localThumb) {
|
let level = parseFloat(audioLevel);
|
||||||
this.createAudioLevelCanvas(localThumb.get(0).id,
|
|
||||||
localVideo.thumbWidth,
|
level = isNaN(level) ? 0 : level;
|
||||||
localVideo.thumbHeight);
|
|
||||||
}
|
let shadowElement = element.getElementsByClassName("dynamic-shadow");
|
||||||
|
|
||||||
|
if (shadowElement && shadowElement.length > 0)
|
||||||
|
shadowElement = shadowElement[0];
|
||||||
|
|
||||||
|
shadowElement.style.boxShadow = this._updateLargeVideoShadow(level);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the large video shadow effect.
|
||||||
|
*/
|
||||||
|
_updateLargeVideoShadow (level) {
|
||||||
|
var scale = 2,
|
||||||
|
|
||||||
|
// Internal circle audio level.
|
||||||
|
int = {
|
||||||
|
level: level > 0.15 ? 20 : 0,
|
||||||
|
color: interfaceConfig.AUDIO_LEVEL_PRIMARY_COLOR
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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(', ');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
|
@ -266,7 +266,7 @@ const FilmStrip = {
|
||||||
return { remoteThumbs, localThumb };
|
return { remoteThumbs, localThumb };
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,11 @@ import Avatar from "../avatar/Avatar";
|
||||||
import {createDeferred} from '../../util/helpers';
|
import {createDeferred} from '../../util/helpers';
|
||||||
import UIUtil from "../util/UIUtil";
|
import UIUtil from "../util/UIUtil";
|
||||||
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
|
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
|
||||||
|
|
||||||
import LargeContainer from "./LargeContainer";
|
import LargeContainer from "./LargeContainer";
|
||||||
|
|
||||||
|
import AudioLevels from "../audio_levels/AudioLevels";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager for all Large containers.
|
* Manager for all Large containers.
|
||||||
*/
|
*/
|
||||||
|
@ -307,6 +310,15 @@ export default class LargeVideoManager {
|
||||||
$("#dominantSpeakerAvatar").attr('src', avatarUrl);
|
$("#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.
|
* Show or hide watermark.
|
||||||
* @param {boolean} show
|
* @param {boolean} show
|
||||||
|
|
|
@ -28,6 +28,7 @@ function LocalVideo(VideoLayout, emitter) {
|
||||||
this.setDisplayName();
|
this.setDisplayName();
|
||||||
|
|
||||||
this.createConnectionIndicator();
|
this.createConnectionIndicator();
|
||||||
|
this.addAudioLevelIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalVideo.prototype = Object.create(SmallVideo.prototype);
|
LocalVideo.prototype = Object.create(SmallVideo.prototype);
|
||||||
|
|
|
@ -63,13 +63,12 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
||||||
|
|
||||||
let { remoteVideo } = this.VideoLayout.resizeThumbnails(false, true);
|
let { remoteVideo } = this.VideoLayout.resizeThumbnails(false, true);
|
||||||
let { thumbHeight, thumbWidth } = remoteVideo;
|
let { thumbHeight, thumbWidth } = remoteVideo;
|
||||||
AudioLevels.createAudioLevelCanvas(
|
|
||||||
this.videoSpanId, thumbWidth, thumbHeight);
|
this.addAudioLevelIndicator();
|
||||||
|
|
||||||
return this.container;
|
return this.container;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the remote participant popup menu, by specifying previously
|
* Initializes the remote participant popup menu, by specifying previously
|
||||||
* constructed popupMenuElement, containing all the menu items.
|
* constructed popupMenuElement, containing all the menu items.
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import Avatar from "../avatar/Avatar";
|
import Avatar from "../avatar/Avatar";
|
||||||
import UIUtil from "../util/UIUtil";
|
import UIUtil from "../util/UIUtil";
|
||||||
import UIEvents from "../../../service/UI/UIEvents";
|
import UIEvents from "../../../service/UI/UIEvents";
|
||||||
|
import AudioLevels from "../audio_levels/AudioLevels";
|
||||||
|
|
||||||
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
|
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
|
* @param {boolean} isMuted indicates if we should set the view to muted view
|
||||||
* or not
|
* or not
|
||||||
*/
|
*/
|
||||||
SmallVideo.prototype.setMutedView = function(isMuted) {
|
SmallVideo.prototype.setVideoMutedView = function(isMuted) {
|
||||||
this.isVideoMuted = isMuted;
|
this.isVideoMuted = isMuted;
|
||||||
this.updateView();
|
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 () {
|
SmallVideo.prototype.addModeratorIndicator = function () {
|
||||||
// don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
|
|
||||||
|
// Don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
|
||||||
if (interfaceConfig.DISABLE_FOCUS_INDICATOR)
|
if (interfaceConfig.DISABLE_FOCUS_INDICATOR)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -339,10 +341,33 @@ SmallVideo.prototype.createModeratorIndicatorElement = function () {
|
||||||
indicatorSpan.appendChild(moderatorIndicator);
|
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.
|
* Removes the element indicating the moderator(owner) of the conference.
|
||||||
*/
|
*/
|
||||||
SmallVideo.prototype.removeModeratorIndicatorElement = function () {
|
SmallVideo.prototype.removeModeratorIndicator = function () {
|
||||||
$('#' + this.videoSpanId + ' .focusindicator').remove();
|
$('#' + this.videoSpanId + ' .focusindicator').remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/* global config, APP, $, interfaceConfig, JitsiMeetJS */
|
/* global config, APP, $, interfaceConfig, JitsiMeetJS */
|
||||||
/* jshint -W101 */
|
/* jshint -W101 */
|
||||||
|
|
||||||
import AudioLevels from "../audio_levels/AudioLevels";
|
|
||||||
import Avatar from "../avatar/Avatar";
|
import Avatar from "../avatar/Avatar";
|
||||||
import FilmStrip from "./FilmStrip";
|
import FilmStrip from "./FilmStrip";
|
||||||
import UIEvents from "../../../service/UI/UIEvents";
|
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
|
// if we do not resize the thumbs here, if there is no video device
|
||||||
// the local video thumb maybe one pixel
|
// the local video thumb maybe one pixel
|
||||||
let { localVideo } = this.resizeThumbnails(false, true);
|
let { localVideo } = this.resizeThumbnails(false, true);
|
||||||
AudioLevels.createAudioLevelCanvas(
|
|
||||||
"localVideoContainer",
|
|
||||||
localVideo.thumbWidth,
|
|
||||||
localVideo.thumbHeight);
|
|
||||||
|
|
||||||
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
||||||
this.lastNCount = config.channelLastN;
|
this.lastNCount = config.channelLastN;
|
||||||
|
@ -123,16 +118,21 @@ var VideoLayout = {
|
||||||
largeVideo.onLocalFlipXChange(localFlipX);
|
largeVideo.onLocalFlipXChange(localFlipX);
|
||||||
}
|
}
|
||||||
largeVideo.updateContainerSize();
|
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) {
|
setAudioLevel(id, lvl) {
|
||||||
if (!largeVideo) {
|
let smallVideo = this.getSmallVideo(id);
|
||||||
return;
|
if (smallVideo)
|
||||||
}
|
smallVideo.updateAudioLevelIndicator(lvl);
|
||||||
AudioLevels.updateAudioLevel(
|
|
||||||
id, lvl, largeVideo.id
|
if (largeVideo && id === largeVideo.id)
|
||||||
);
|
largeVideo.updateLargeVideoAudioLevel(lvl);
|
||||||
},
|
},
|
||||||
|
|
||||||
isInLastN (resource) {
|
isInLastN (resource) {
|
||||||
|
@ -469,9 +469,9 @@ var VideoLayout = {
|
||||||
showModeratorIndicator () {
|
showModeratorIndicator () {
|
||||||
let isModerator = APP.conference.isModerator;
|
let isModerator = APP.conference.isModerator;
|
||||||
if (isModerator) {
|
if (isModerator) {
|
||||||
localVideoThumbnail.createModeratorIndicatorElement();
|
localVideoThumbnail.addModeratorIndicator();
|
||||||
} else {
|
} else {
|
||||||
localVideoThumbnail.removeModeratorIndicatorElement();
|
localVideoThumbnail.removeModeratorIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
APP.conference.listMembers().forEach(function (member) {
|
APP.conference.listMembers().forEach(function (member) {
|
||||||
|
@ -481,7 +481,7 @@ var VideoLayout = {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (member.isModerator()) {
|
if (member.isModerator()) {
|
||||||
remoteVideo.createModeratorIndicatorElement();
|
remoteVideo.addModeratorIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isModerator) {
|
if (isModerator) {
|
||||||
|
@ -528,7 +528,6 @@ var VideoLayout = {
|
||||||
FilmStrip.resizeThumbnails(localVideo, remoteVideo,
|
FilmStrip.resizeThumbnails(localVideo, remoteVideo,
|
||||||
animate, forceUpdate)
|
animate, forceUpdate)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
AudioLevels.updateCanvasSize(localVideo, remoteVideo);
|
|
||||||
if (onComplete && typeof onComplete === "function")
|
if (onComplete && typeof onComplete === "function")
|
||||||
onComplete();
|
onComplete();
|
||||||
});
|
});
|
||||||
|
@ -558,11 +557,11 @@ var VideoLayout = {
|
||||||
*/
|
*/
|
||||||
onVideoMute (id, value) {
|
onVideoMute (id, value) {
|
||||||
if (APP.conference.isLocalId(id)) {
|
if (APP.conference.isLocalId(id)) {
|
||||||
localVideoThumbnail.setMutedView(value);
|
localVideoThumbnail.setVideoMutedView(value);
|
||||||
} else {
|
} else {
|
||||||
let remoteVideo = remoteVideos[id];
|
let remoteVideo = remoteVideos[id];
|
||||||
if (remoteVideo)
|
if (remoteVideo)
|
||||||
remoteVideo.setMutedView(value);
|
remoteVideo.setVideoMutedView(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isCurrentlyOnLarge(id)) {
|
if (this.isCurrentlyOnLarge(id)) {
|
||||||
|
|
Loading…
Reference in New Issue