Compare commits

...

15 Commits

Author SHA1 Message Date
bgrozev
171b065db1 Revert "Add room name to browser tab title" (#5560)
Because it displays "undefined" in the title.

This reverts commit 86ebfe8dad.
2020-04-01 22:03:14 -05:00
Jaya Allamsetty
9da0b7fee3 deps(ljm): Bump adapter.js to 7.5.0 and validate ice candidate info 2020-04-01 18:28:30 -04:00
damencho
716c9eb46f debian: Change coturn unit file to start on 443. 2020-04-01 17:11:55 -05:00
Stefan
a85c72d859 doc: use sudo 2020-04-01 22:57:08 +02:00
Kenneth Peiruza
0ba567856e lang: fix catalan translation
There was a huge mistake in Catalan translation. Stop streaming and recording said "start" (inicia) instead of "stop" (atura).

Then, 1 small fix on a more polite way of refering to "using a software" -> *emprar* rather than *usar*.
2020-04-01 22:51:38 +02:00
Horatiu Muresan
7f1eb83dbd feat(notifications): Manage audio notifications 2020-04-01 22:40:07 +02:00
damencho
6e4c1f64d8 debian: Adds syslog logging. 2020-04-01 15:18:49 -05:00
damencho
82aa51770a debian: Skip turn config if other sites are available listening on 443. 2020-04-01 15:18:49 -05:00
damencho
990c77bd3d debian: Skip turnserver config if it is already configured. 2020-04-01 15:18:49 -05:00
bgrozev
5bb23b2d17 chore(package.json): Update js-utils (#5541) 2020-04-01 15:09:11 -05:00
Saúl Ibarra Corretgé
9cc05ef838 config: remove unneeded option 2020-04-01 20:40:18 +02:00
Saúl Ibarra Corretgé
a21e3a1946 Merge pull request #5510 from rubenk/add-roomname-to-title
Add room name to browser tab title
2020-04-01 20:08:05 +02:00
Vlad Piersec
c05ca1d9fc fix(prejoin_page) Add labels for video & more UI fixes 2020-04-01 19:57:04 +02:00
Vlad Piersec
1b05d7269c feat(prejoin_page) Add settings buttons
This reverts commit faf24ca7ec.
2020-04-01 19:57:04 +02:00
Ruben Kerkhof
86ebfe8dad Add room name to browser tab title
Fixes #5480

Signed-off-by: Ruben Kerkhof <ruben@rubenkerkhof.com>
2020-03-31 13:58:12 +02:00
47 changed files with 2242 additions and 66 deletions

View File

@@ -390,6 +390,9 @@ var config = {
// userRegion: "asia"
},
// Decides whether the start/stop recording audio notifications should play on record.
// disableRecordAudioNotification: false,
// Information for the chrome extension banner
// chromeExtensionBanner: {
// // The chrome extension to be installed address

130
css/_audio-preview.css Normal file
View File

@@ -0,0 +1,130 @@
.audio-preview {
&-content {
font-size: 15px;
line-height: 24px;
max-height: 456px;
overflow: auto;
width: 328px;
}
&-header {
color: #fff;
display: flex;
padding: 16px;
&-icon {
display: inline-block;
}
&-text {
font-weight: bold;
margin-left: 8px;
}
}
&-entry {
align-items: center;
color: #fff;
cursor: pointer;
display: flex;
padding: 12px 0;
margin-left: 48px;
&--selected {
background: rgba(28,32,37,0.5);
cursor: initial;
margin-left: 0;
padding-left: 21px;
}
&-text {
color: #fff;
font-size: 15px;
display: inline-block;
line-height: 24px;
text-overflow: ellipsis;
max-width: 213px;
overflow: hidden;
white-space: nowrap;
}
}
&-speaker {
position: relative;
&:hover {
.audio-preview-entry {
background: rgba(255,255,255, 0.2);
margin-left: 0;
padding-left: 48px;
&--selected {
padding-left: 21px;
}
}
.audio-preview-test-button {
display: inline-block;
}
}
.audio-preview-entry-text {
max-width: 256px;
}
}
&-microphone {
position: relative;
}
&-icon {
border-radius: 50%;
display: inline-block;
width: 14px;
& svg {
fill: #1C2025;
}
&--check {
background: #31B76A;
margin-right: 13px;
}
&--exclamation {
margin-left: 6px;
& svg {
fill: #E54B4B;
}
}
}
&-test-button {
display: none;
background: #FFF;
border: 1px solid #D1DBE8;
border-radius: 3px;
color: #1C2025;
cursor: pointer;
font-weight: 600;
font-size: 15px;
line-height: 24px;
padding: 4px 16px;
position: absolute;
right: 16px;
top: 8px;
}
&-meter-mic {
position: absolute;
right: 16px;
top: 18px;
}
// Override @atlaskit/InlineDialog container which is made with styled components
& > div > div:nth-child(2) > div > div {
outline: none;
padding: 0;
}
}

30
css/_meter.css Normal file
View File

@@ -0,0 +1,30 @@
.jitsi-icon {
&.metr {
display: inline-block;
& > svg {
fill: #4E5E6C;
width: 38px;
}
}
&.metr--disabled {
& > svg {
fill: #4E5E6C;
}
}
}
.metr-l-0 {
rect:first-child {
fill: #31B76A;
}
}
@for $i from 1 through 7 {
.metr-l-#{$i} {
rect:nth-child(-n+#{$i+1}) {
fill: #31B76A;
}
}
}

79
css/_settings-button.scss Normal file
View File

@@ -0,0 +1,79 @@
.settings-button {
&-container {
position: relative;
.toolbox-icon {
align-items: center;
cursor: pointer;
display: flex;
background-color: #fff;
border-radius: 50%;
border: 1px solid #d1dbe8;
justify-content: center;
width: 38px;
height: 38px;
&:hover {
background-color: #daebfa;
border: 1px solid #daebfa;
}
&.toggled {
background: #2a3a4b;
border: 1px solid #5e6d7a;
svg {
fill: #fff;
}
&:hover {
background-color: #5e6d7a;
}
}
&.disabled, .disabled & {
cursor: initial;
color: #fff;
background-color: #a4b8d1;
}
svg {
fill: #5e6d7a;
}
}
}
&-small-icon {
background: #FFF;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 50%;
bottom: 0;
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.25);
cursor: pointer;
height: 16px;
position: absolute;
text-align: center;
right: 4px;
width: 16px;
&> svg {
margin-top: 5px;
}
&--disabled {
background-color: #a4b8d1;
cursor: default;
}
&--hovered {
bottom: -1px;
height: 20px;
right: 2px;
width: 20px;
&> svg {
margin-top: 6px;
}
}
}
}

65
css/_video-preview.css Normal file
View File

@@ -0,0 +1,65 @@
.video-preview {
max-height: 290px;
overflow: auto;
&-entry {
cursor: pointer;
height: 135px;
margin-bottom: 16px;
position: relative;
width: 240px;
&:last-child {
margin-bottom: 0;
}
&--selected {
border: 3px solid #31B76A;
cursor: default;
height: 129px;
width: 234px;
}
}
&-video {
height: 100%;
object-fit: cover;
width: 100%;
}
&-overlay {
background: rgba(42, 58, 75, 0.6);
height: 100%;
position: absolute;
width: 100%;
z-index: 1;
}
&-error {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
width: 100%;
}
&-label {
color: #fff;
font-size: 13px;
line-height: 20px;
overflow: hidden;
padding: 8px;
position: absolute;
text-align: center;
text-overflow: ellipsis;
width: 220px;
z-index: 2;
}
// Override @atlaskit/InlineDialog container which is made with styled components
& > div > div:nth-child(2) > div > div {
outline: none;
padding: 16px;
}
}

View File

@@ -86,5 +86,9 @@ $flagsImagePath: "../images/";
@import 'avatar';
@import 'promotional-footer';
@import 'chrome-extension-banner';
@import 'settings-button';
@import 'meter';
@import 'audio-preview';
@import 'video-preview';
/* Modules END */

View File

@@ -36,11 +36,31 @@ case "$1" in
NGINX_CONFIG="/etc/nginx/sites-available/$JVB_HOSTNAME.conf"
JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
NGINX_SITES_ENABLED="/etc/nginx/sites-enabled/"
NGINX_CONFIG_ENABLED="${NGINX_SITES_ENABLED}${JVB_HOSTNAME}.conf"
for site in ${NGINX_SITES_ENABLED}*; do
# if it is not a file continue
[ -f "${site}" ] || continue
# if it is our config skip
[ "${site}" != "${NGINX_CONFIG_ENABLED}" ] || continue
# check whether other enabled hosts has listen 443
if cat ${site} | grep -v "^[[:space:]]*#" | grep listen | grep -q "^.*[[:space:]:]443[;[:space:]].*" ; then
# nothing to do
echo ""
echo "turnserver not configured as other nginx sites use port 443"
echo ""
db_stop
exit 0
fi
done
# detect dpkg-reconfigure, just delete old links
db_get jitsi-meet-turnserver/jvb-hostname
JVB_HOSTNAME_OLD=$RET
if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
rm -f $TURN_CONFIG
if [[ -f $TURN_CONFIG ]] && grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
rm -f $TURN_CONFIG
fi
fi
# this detect only old installations with no nginx
@@ -54,6 +74,14 @@ case "$1" in
exit 0
fi
if [[ -f $TURN_CONFIG ]] ; then
echo ""
echo "turnserver is already configured on this machine, skipping."
echo ""
db_stop
exit 0
fi
# stores the hostname so we will reuse it later, like in purge
db_set jitsi-meet-turnserver/jvb-hostname "$JVB_HOSTNAME"
@@ -65,46 +93,52 @@ case "$1" in
fi
TURN_SECRET="$RET"
if [[ -f $TURN_CONFIG ]] && ! grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com)
cp /usr/share/jitsi-meet-turnserver/turnserver.conf $TURN_CONFIG
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $TURN_CONFIG
sed -i "s/__turnSecret__/$TURN_SECRET/g" $TURN_CONFIG
sed -i "s/__external_ip_address__/$JVB_HOSTNAME/g" $TURN_CONFIG
# no turn config exists, lt's copy template and fill it in
PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com)
cp /usr/share/jitsi-meet-turnserver/turnserver.conf $TURN_CONFIG
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $TURN_CONFIG
sed -i "s/__turnSecret__/$TURN_SECRET/g" $TURN_CONFIG
sed -i "s/__external_ip_address__/$JVB_HOSTNAME/g" $TURN_CONFIG
# SSL for nginx
db_get jitsi-meet/cert-choice
CERT_CHOICE="$RET"
# Hack Debian Buster coturn to be able to bind privileged port 443
COTURN_UNIT_FILE="/lib/systemd/system/coturn.service"
if [[ -f $COTURN_UNIT_FILE ]] && ! grep -q "CAP_NET_BIND_SERVICE" "$COTURN_UNIT_FILE" ; then
sed -i "s/\[Service\]/\[Service\]\nAmbientCapabilities=CAP_NET_BIND_SERVICE/g" $COTURN_UNIT_FILE
systemctl daemon-reload
fi
if [ "$CERT_CHOICE" = "I want to use my own certificate" ] ; then
db_get jitsi-meet/cert-path-key
CERT_KEY="$RET"
db_get jitsi-meet/cert-path-crt
CERT_CRT="$RET"
# SSL for nginx
db_get jitsi-meet/cert-choice
CERT_CHOICE="$RET"
# replace self-signed certificate paths with user provided ones
CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
sed -i "s/pkey=\/etc\/jitsi\/meet\/.*key/pkey=$CERT_KEY_ESC/g" $TURN_CONFIG
CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
sed -i "s/cert=\/etc\/jitsi\/meet\/.*crt/cert=$CERT_CRT_ESC/g" $TURN_CONFIG
fi
if [ "$CERT_CHOICE" = "I want to use my own certificate" ] ; then
db_get jitsi-meet/cert-path-key
CERT_KEY="$RET"
db_get jitsi-meet/cert-path-crt
CERT_CRT="$RET"
sed -i "s/#TURNSERVER_ENABLED/TURNSERVER_ENABLED/g" /etc/default/coturn
invoke-rc.d coturn restart || true
# replace self-signed certificate paths with user provided ones
CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
sed -i "s/pkey=\/etc\/jitsi\/meet\/.*key/pkey=$CERT_KEY_ESC/g" $TURN_CONFIG
CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
sed -i "s/cert=\/etc\/jitsi\/meet\/.*crt/cert=$CERT_CRT_ESC/g" $TURN_CONFIG
fi
NGINX_STREAM_CONFIG="/etc/nginx/modules-enabled/60-jitsi-meet.conf"
if [ -f $NGINX_STREAM_CONFIG ] && [ -f $NGINX_CONFIG ] ; then
sed -i "s/listen 443 ssl/listen 4444 ssl http2/g" $NGINX_CONFIG
sed -i "s/listen \[\:\:\]\:443 ssl/listen \[\:\:\]\:4444 ssl http2/g" $NGINX_CONFIG
invoke-rc.d nginx reload || true
fi
sed -i "s/#TURNSERVER_ENABLED/TURNSERVER_ENABLED/g" /etc/default/coturn
invoke-rc.d coturn restart || true
# Enable turn server in config.js
if [ -f $JITSI_MEET_CONFIG ] ; then
sed -i "s/\/\/ useStunTurn: true/useStunTurn: true/g" $JITSI_MEET_CONFIG
fi
NGINX_STREAM_CONFIG="/etc/nginx/modules-enabled/60-jitsi-meet.conf"
if [ -f $NGINX_STREAM_CONFIG ] && [ -f $NGINX_CONFIG ] ; then
sed -i "s/listen 443 ssl/listen 4444 ssl http2/g" $NGINX_CONFIG
sed -i "s/listen \[\:\:\]\:443 ssl/listen \[\:\:\]\:4444 ssl http2/g" $NGINX_CONFIG
invoke-rc.d nginx reload || true
fi
# Enable turn server in config.js
if [ -f $JITSI_MEET_CONFIG ] ; then
sed -i "s/\/\/ useStunTurn: true/useStunTurn: true/g" $JITSI_MEET_CONFIG
fi
# and we're done with debconf

View File

@@ -11,3 +11,5 @@ no-tcp
listening-port=443
tls-listening-port=4445
external-ip=__external_ip_address__
syslog

View File

@@ -23,7 +23,7 @@ Finally on the same machine test that you can ping the FQDN with: `ping "$(hostn
### Add the Jitsi package repository
```sh
echo 'deb https://download.jitsi.org stable/' >> /etc/apt/sources.list.d/jitsi-stable.list
wget -qO - https://download.jitsi.org/jitsi-key.gpg.key | apt-key add -
wget -qO - https://download.jitsi.org/jitsi-key.gpg.key | sudo apt-key add -
```
### Install Jitsi Meet

View File

@@ -264,13 +264,13 @@
"startLiveStreaming": "Inicia la transmissió en directe",
"startRecording": "Inicia l'enregistrament",
"startRemoteControlErrorMessage": "S'ha produït un error en intentar iniciar la sessió de control remot!",
"stopLiveStreaming": "Inicia la transmissió en directe",
"stopRecording": "Inicia l'enregistrament",
"stopRecordingWarning": "Esteu segur de voler iniciar l'enregistrament?",
"stopLiveStreaming": "Atura la transmissió en directe",
"stopRecording": "Atura l'enregistrament",
"stopRecordingWarning": "Esteu segur de voler aturar l'enregistrament?",
"stopStreamingWarning": "Esteu segur de voler aturar la transmissió en directe?",
"streamKey": "Clau de transmissió en directe",
"Submit": "Tramet",
"thankYou": "Gràcies per usar {{appName}}!",
"thankYou": "Gràcies per emprar {{appName}}!",
"token": "identificador",
"tokenAuthFailed": "No esteu autoritzat a unir-vos a aquesta trucada.",
"tokenAuthFailedTitle": "L'autenticació ha fallat",

View File

@@ -525,6 +525,7 @@
"followMe": "Everyone follows me",
"language": "Language",
"loggedIn": "Logged in as {{name}}",
"microphones": "Microphones",
"moderator": "Moderator",
"more": "More",
"name": "Name",
@@ -532,6 +533,7 @@
"selectAudioOutput": "Audio output",
"selectCamera": "Camera",
"selectMic": "Microphone",
"speakers": "Speakers",
"startAudioMuted": "Everyone starts muted",
"startVideoMuted": "Everyone starts hidden",
"title": "Settings"
@@ -648,7 +650,7 @@
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
"noAudioSignalDialInDesc": "You can also dial-in using:",
"noAudioSignalDialInLinkDesc" : "Dial-in numbers",
"noAudioSignalDialInLinkDesc": "Dial-in numbers",
"noisyAudioInputTitle": "Your microphone appears to be noisy!",
"noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
"openChat": "Open chat",

19
package-lock.json generated
View File

@@ -10653,8 +10653,8 @@
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-utils": {
"version": "github:jitsi/js-utils#91c5e53ca5fa42907c88d56bc78254e6e56e058d",
"from": "github:jitsi/js-utils#91c5e53ca5fa42907c88d56bc78254e6e56e058d",
"version": "github:jitsi/js-utils#0b2cef90613a74777fefd98d4ee3eda3879809ab",
"from": "github:jitsi/js-utils#0b2cef90613a74777fefd98d4ee3eda3879809ab",
"requires": {
"bowser": "2.7.0",
"js-md5": "0.7.3",
@@ -10883,8 +10883,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#a7950f8ebb489225c2e8bf41fe65f330b3de0874",
"from": "github:jitsi/lib-jitsi-meet#a7950f8ebb489225c2e8bf41fe65f330b3de0874",
"version": "github:jitsi/lib-jitsi-meet#4a2abb179672aebecf9d780f796fcca531bb2398",
"from": "github:jitsi/lib-jitsi-meet#4a2abb179672aebecf9d780f796fcca531bb2398",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.2",
@@ -10897,7 +10897,7 @@
"strophe.js": "1.3.4",
"strophejs-plugin-disco": "0.0.2",
"strophejs-plugin-stream-management": "github:jitsi/strophejs-plugin-stream-management#cec7608601c1bc098543823fc658e3ddf758c009",
"webrtc-adapter": "github:webrtc/adapter#1eec19782b4058d186341263e7d049cea3e3290a"
"webrtc-adapter": "7.5.0"
},
"dependencies": {
"js-md5": {
@@ -18999,11 +18999,12 @@
}
},
"webrtc-adapter": {
"version": "github:webrtc/adapter#1eec19782b4058d186341263e7d049cea3e3290a",
"from": "github:webrtc/adapter#1eec19782b4058d186341263e7d049cea3e3290a",
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.5.0.tgz",
"integrity": "sha512-cUqlw310uLLSYvO8FTNCVmGWSMlMt6vuSDkcYL1nW+RUvAILJ3jEIvAUgFQU5EFGnU+mf9/No14BFv3U+hoxBQ==",
"requires": {
"rtcpeerconnection-shim": "^1.1.13",
"sdp": "^2.3.0"
"rtcpeerconnection-shim": "^1.2.15",
"sdp": "^2.12.0"
}
},
"websocket-driver": {

View File

@@ -53,10 +53,10 @@
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"js-utils": "github:jitsi/js-utils#91c5e53ca5fa42907c88d56bc78254e6e56e058d",
"js-utils": "github:jitsi/js-utils#0b2cef90613a74777fefd98d4ee3eda3879809ab",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#a7950f8ebb489225c2e8bf41fe65f330b3de0874",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#4a2abb179672aebecf9d780f796fcca531bb2398",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",

View File

@@ -1,3 +1,5 @@
import extraConfigWhitelist from './extraConfigWhitelist';
/**
* The config keys to whitelist, the keys that can be overridden.
* Currently we can only whitelist the first part of the properties, like
@@ -145,4 +147,4 @@ export default [
'useStunTurn',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable'
];
].concat(extraConfigWhitelist);

View File

@@ -0,0 +1,4 @@
/**
* Deploy-specific configuration whitelists
*/
export default [];

View File

@@ -216,6 +216,18 @@ export function setAudioInputDevice(deviceId) {
};
}
/**
* Updates the output device id.
*
* @param {string} deviceId - The id of the new output device.
* @returns {Function}
*/
export function setAudioOutputDevice(deviceId) {
return function(dispatch) {
return setAudioOutputDeviceId(deviceId, dispatch);
};
}
/**
* Signals to update the currently used video input device.
*

View File

@@ -174,6 +174,60 @@ export function formatDeviceLabel(label: string) {
return formattedLabel;
}
/**
* Returns a list of objects containing all the microphone device ids and labels.
*
* @param {Object} state - The state of the application.
* @returns {Object[]}
*/
export function getAudioInputDeviceData(state: Object) {
return state['features/base/devices'].availableDevices.audioInput.map(
({ deviceId, label }) => {
return {
deviceId,
label
};
});
}
/**
* Returns a list of objectes containing all the output device ids and labels.
*
* @param {Object} state - The state of the application.
* @returns {Object[]}
*/
export function getAudioOutputDeviceData(state: Object) {
return state['features/base/devices'].availableDevices.audioOutput.map(
({ deviceId, label }) => {
return {
deviceId,
label
};
});
}
/**
* Returns a list of all the camera device ids.
*
* @param {Object} state - The state of the application.
* @returns {string[]}
*/
export function getVideoDeviceIds(state: Object) {
return state['features/base/devices'].availableDevices.videoInput.map(({ deviceId }) => deviceId);
}
/**
* Returns true if there are devices of a specific type.
*
* @param {Object} state - The state of the application.
* @param {string} type - The type of device: VideoOutput | audioOutput | audioInput.
*
* @returns {boolean}
*/
export function hasAvailableDevices(state: Object, type: string) {
return state['features/base/devices'].availableDevices[type].length > 0;
}
/**
* Set device id of the audio output device which is currently in use.
* Empty string stands for default device.

View File

@@ -0,0 +1,3 @@
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07001 0.248238C8.3471 -0.0596449 8.82132 -0.0846038 9.1292 0.192491C9.43709 0.469585 9.46205 0.943802 9.18495 1.25168L5.65622 5.19348C5.35829 5.52451 4.83922 5.52451 4.54128 5.19348L1.06752 1.25168C0.79043 0.943802 0.81539 0.469585 1.12327 0.192491C1.43115 -0.0846038 1.90537 -0.0596449 2.18247 0.248238L5.09875 3.57062L8.07001 0.248238Z" fill="#5E6D7A"/>
</svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 14.6667C4.3181 14.6667 1.33333 11.682 1.33333 8.00007C1.33333 4.31817 4.3181 1.3334 8 1.3334C11.6819 1.3334 14.6667 4.31817 14.6667 8.00007C14.6667 11.682 11.6819 14.6667 8 14.6667ZM7.33333 4.66676C7.33333 4.29857 7.6318 4.00009 8 4.00009C8.36819 4.00009 8.66666 4.29857 8.66666 4.66676V8.00009C8.66666 8.36828 8.36819 8.66676 8 8.66676C7.6318 8.66676 7.33333 8.36828 7.33333 8.00009V4.66676ZM8 10.0001C7.63181 10.0001 7.33333 10.2985 7.33333 10.6667C7.33333 11.0349 7.63181 11.3334 8 11.3334C8.36818 11.3334 8.66666 11.0349 8.66666 10.6667C8.66666 10.2985 8.36818 10.0001 8 10.0001Z" />
</svg>

After

Width:  |  Height:  |  Size: 731 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.33331 8.00004C1.33331 11.6819 4.31808 14.6667 7.99998 14.6667C11.6819 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6819 1.33337 7.99998 1.33337C4.31808 1.33337 1.33331 4.31814 1.33331 8.00004ZM13.3333 8.00005C13.3333 10.9456 10.9455 13.3334 7.99998 13.3334C5.05446 13.3334 2.66665 10.9456 2.66665 8.00005C2.66665 5.05453 5.05446 2.66672 7.99998 2.66672C10.9455 2.66672 13.3333 5.05453 13.3333 8.00005ZM7.33331 4.66673C7.33331 4.29854 7.63179 4.00006 7.99998 4.00006C8.36817 4.00006 8.66665 4.29854 8.66665 4.66673V8.00006C8.66665 8.36825 8.36817 8.66673 7.99998 8.66673C7.63179 8.66673 7.33331 8.36825 7.33331 8.00006V4.66673ZM7.99998 10C7.63179 10 7.33331 10.2985 7.33331 10.6667C7.33331 11.0349 7.63179 11.3334 7.99998 11.3334C8.36817 11.3334 8.66665 11.0349 8.66665 10.6667C8.66665 10.2985 8.36817 10 7.99998 10Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1001 B

View File

@@ -3,6 +3,7 @@
export { default as IconAdd } from './add.svg';
export { default as IconAddPeople } from './link.svg';
export { default as IconArrowBack } from './arrow_back.svg';
export { default as IconArrowDown } from './arrow_down.svg';
export { default as IconAudioOnly } from './visibility.svg';
export { default as IconAudioOnlyOff } from './visibility-off.svg';
export { default as IconAudioRoute } from './volume.svg';
@@ -27,6 +28,8 @@ export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
export { default as IconDragHandle } from './drag-handle.svg';
export { default as IconEventNote } from './event_note.svg';
export { default as IconExclamation } from './exclamation.svg';
export { default as IconExclamationSolid } from './exclamation-solid.svg';
export { default as IconExitFullScreen } from './exit-full-screen.svg';
export { default as IconFeedback } from './feedback.svg';
export { default as IconFullScreen } from './full-screen.svg';
@@ -41,8 +44,10 @@ export { default as IconMenuDown } from './menu-down.svg';
export { default as IconMenuThumb } from './thumb-menu.svg';
export { default as IconMenuUp } from './menu-up.svg';
export { default as IconMessage } from './message.svg';
export { default as IconMeter } from './meter.svg';
export { default as IconMicDisabled } from './mic-disabled.svg';
export { default as IconMicrophone } from './microphone.svg';
export { default as IconMicrophoneEmpty } from './microphone-empty.svg';
export { default as IconModerator } from './star.svg';
export { default as IconMuteEveryone } from './mute-everyone.svg';
export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
@@ -76,3 +81,4 @@ export { default as IconVideoQualityHD } from './HD.svg';
export { default as IconVideoQualityLD } from './LD.svg';
export { default as IconVideoQualitySD } from './SD.svg';
export { default as IconVolume } from './volume.svg';
export { default as IconVolumeEmpty } from './volume-empty.svg';

View File

@@ -0,0 +1,10 @@
<svg width="38" height="12" viewBox="0 0 38 12" fill="#5E6D7A" xmlns="http://www.w3.org/2000/svg">
<rect width="3" height="12" rx="1"/>
<rect x="5" width="3" height="12" rx="1" />
<rect x="10" width="3" height="12" rx="1" />
<rect x="15" width="3" height="12" rx="1" />
<rect x="20" width="3" height="12" rx="1" />
<rect x="25" width="3" height="12" rx="1" />
<rect x="30" width="3" height="12" rx="1" />
<rect x="35" width="3" height="12" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 6C16 3.79086 14.2091 2 12 2C9.79086 2 8 3.79086 8 6V12C8 13.8666 9.27853 15.4346 11.0076 15.8759C11.0026 15.9166 11 15.958 11 16V17.917C8.16229 17.441 6 14.973 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 16.0796 7.05369 19.446 11 19.9381V21C11 21.5523 11.4477 22 12 22C12.5523 22 13 21.5523 13 21V19.9381C16.9463 19.446 20 16.0796 20 12C20 11.4477 19.5523 11 19 11C18.4477 11 18 11.4477 18 12C18 14.973 15.8377 17.441 13 17.917V16C13 15.958 12.9974 15.9166 12.9924 15.8759C14.7215 15.4346 16 13.8666 16 12V6ZM12 4C10.8954 4 10 4.89543 10 6V12C10 13.1046 10.8954 14 12 14C13.1046 14 14 13.1046 14 12V6C14 4.89543 13.1046 4 12 4Z" fill="#A4B8D1"/>
</svg>

After

Width:  |  Height:  |  Size: 817 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1799 3.68341L6 8H3C2.44772 8 2 8.44771 2 9V15C2 15.5523 2.44772 16 3 16H6L11.1799 20.3166C11.2698 20.3915 11.383 20.4325 11.5 20.4325C11.7761 20.4325 12 20.2086 12 19.9325V4.06752C12 3.95055 11.959 3.83728 11.8841 3.74743C11.7073 3.53529 11.392 3.50662 11.1799 3.68341ZM4 10H6.7241L10 7.27008V16.7299L6.7241 14H4V10ZM14 8C16.2091 8 18 9.79086 18 12C18 14.2091 16.2091 16 14 16V14C15.1046 14 16 13.1046 16 12C16 10.8954 15.1046 10 14 10V8ZM14 4C18.4183 4 22 7.58172 22 12C22 16.4183 18.4183 20 14 20V18C17.3137 18 20 15.3137 20 12C20 8.68629 17.3137 6 14 6V4Z" fill="#A4B8D1"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -2,6 +2,36 @@
export * from './functions.any';
/**
* Returns the deviceId for the currently used camera.
*
* @param {Object} state - The state of the application.
* @returns {void}
*/
export function getCurrentCameraDeviceId(state: Object) {
return state['features/base/settings'].cameraDeviceId;
}
/**
* Returns the deviceId for the currently used microphone.
*
* @param {Object} state - The state of the application.
* @returns {void}
*/
export function getCurrentMicDeviceId(state: Object) {
return state['features/base/settings'].micDeviceId;
}
/**
* Returns the deviceId for the currently used speaker.
*
* @param {Object} state - The state of the application.
* @returns {void}
*/
export function getCurrentOutputDeviceId(state: Object) {
return state['features/base/settings'].audioOutputDeviceId;
}
/**
* Handles changes to the `disableCallIntegration` setting.
* Noop on web.

View File

@@ -0,0 +1,137 @@
// @flow
import React, { Component } from 'react';
import { Icon } from '../../icons';
type Props = {
/**
* The decorated component (ToolboxButton).
*/
children: React$Node,
/**
* Icon of the button.
*/
icon: Function,
/**
* Flag used for disabling the small icon.
*/
iconDisabled: boolean,
/**
* Click handler for the small icon.
*/
onIconClick: Function,
/**
* Additional styles.
*/
styles?: Object,
};
type State = {
/**
* Whether the button is hovered or not.
*/
isHovered: boolean,
};
/**
* Displayes the `ToolboxButtonWithIcon` component.
*
* @returns {ReactElement}
*/
export default class ToolboxButtonWithIcon extends Component<Props, State> {
/**
* Initializes a new {@code ToolboxButtonWithIcon} instance.
*
* @param {Props} props - The props of the component.
*/
constructor(props: Props) {
super(props);
this.state = {
isHovered: false
};
this._onMouseEnter = this._onMouseEnter.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
}
_onMouseEnter: () => void;
/**
* Handler for when the small button has the mouse over.
*
* @returns {void}.
*/
_onMouseEnter() {
this.setState({
isHovered: true
});
}
_onMouseLeave: () => void;
/**
* Handler for when the mouse leaves the small button.
*
* @returns {void}
*/
_onMouseLeave() {
this.setState({
isHovered: false
});
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {React$Node}
*/
render() {
const {
children,
icon,
iconDisabled,
onIconClick,
styles
} = this.props;
const iconProps = {};
let size = 9;
if (iconDisabled) {
iconProps.className
= 'settings-button-small-icon settings-button-small-icon--disabled';
} else {
iconProps.className = 'settings-button-small-icon';
iconProps.onClick = onIconClick;
if (this.state.isHovered) {
iconProps.className = `${iconProps.className} settings-button-small-icon--hovered`;
size = 11;
}
}
return (
<div
className = 'settings-button-container'
styles = { styles }>
{children}
<div
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }>
<Icon
{ ...iconProps }
size = { size }
src = { icon } />
</div>
</div>
);
}
}

View File

@@ -7,3 +7,4 @@ export { default as AbstractHangupButton } from './AbstractHangupButton';
export { default as AbstractVideoMuteButton } from './AbstractVideoMuteButton';
export { default as BetaTag } from './BetaTag';
export { default as OverflowMenuItem } from './OverflowMenuItem';
export { default as ToolboxButtonWithIcon } from './ToolboxButtonWithIcon';

View File

@@ -130,7 +130,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
// but we want to indicate those in case of sip gateway
const {
iAmRecorder,
iAmSipGateway
iAmSipGateway,
disableRecordAudioNotification
} = getState()['features/base/config'];
if (iAmRecorder && !iAmSipGateway) {
@@ -153,6 +154,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {
break;
}
let soundID;
@@ -163,7 +169,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
}
if (soundID) {
sendAnalytics(createRecordingEvent('start', mode));
dispatch(playSound(soundID));
}
} else if (updatedSessionData.status === OFF
@@ -176,6 +181,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
duration
= (Date.now() / 1000) - oldSessionData.timestamp;
}
sendAnalytics(createRecordingEvent('stop', mode, duration));
if (disableRecordAudioNotification) {
break;
}
if (mode === JitsiRecordingConstants.mode.FILE) {
soundOff = RECORDING_OFF_SOUND_ID;
@@ -186,7 +196,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
}
if (soundOff && soundOn) {
sendAnalytics(createRecordingEvent('stop', mode, duration));
dispatch(stopSound(soundOn));
dispatch(playSound(soundOff));
}

View File

@@ -1,3 +1,6 @@
// The type of (redux) action which sets the visibility of the audio settings popup.
export const SET_AUDIO_SETTINGS_VISIBILITY = 'SET_AUDIO_SETTINGS_VISIBILITY';
/**
* The type of (redux) action which sets the visibility of the view/UI rendering
* the app's settings.
@@ -8,3 +11,6 @@
* }
*/
export const SET_SETTINGS_VIEW_VISIBLE = 'SET_SETTINGS_VIEW_VISIBLE';
// The type of (redux) action which sets the visibility of the video settings popup.
export const SET_VIDEO_SETTINGS_VISIBILITY = 'SET_VIDEO_SETTINGS_VISIBILITY';

View File

@@ -4,7 +4,11 @@ import { setFollowMe, setStartMutedPolicy } from '../base/conference';
import { openDialog } from '../base/dialog';
import { i18next } from '../base/i18n';
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
import {
SET_AUDIO_SETTINGS_VISIBILITY,
SET_SETTINGS_VIEW_VISIBLE,
SET_VIDEO_SETTINGS_VISIBILITY
} from './actionTypes';
import { SettingsDialog } from './components';
import { getMoreTabProps, getProfileTabProps } from './functions';
@@ -38,6 +42,31 @@ export function openSettingsDialog(defaultTab: string) {
return openDialog(SettingsDialog, { defaultTab });
}
/**
* Sets the visiblity of the audio settings.
*
* @param {boolean} value - The new value.
* @returns {Function}
*/
function setAudioSettingsVisibility(value: boolean) {
return {
type: SET_AUDIO_SETTINGS_VISIBILITY,
value
};
}
/**
* Sets the visiblity of the video settings.
*
* @param {boolean} value - The new value.
* @returns {Function}
*/
function setVideoSettingsVisibility(value: boolean) {
return {
type: SET_VIDEO_SETTINGS_VISIBILITY,
value
};
}
/**
* Submits the settings from the "More" tab of the settings dialog.
@@ -84,3 +113,29 @@ export function submitProfileTab(newState: Object): Function {
}
};
}
/**
* Toggles the visiblity of the audio settings.
*
* @returns {void}
*/
export function toggleAudioSettings() {
return (dispatch: Function, getState: Function) => {
const value = getState()['features/settings'].audioSettingsVisible;
dispatch(setAudioSettingsVisibility(!value));
};
}
/**
* Toggles the visiblity of the video settings.
*
* @returns {void}
*/
export function toggleVideoSettings() {
return (dispatch: Function, getState: Function) => {
const value = getState()['features/settings'].videoSettingsVisible;
dispatch(setVideoSettingsVisibility(!value));
};
}

View File

@@ -0,0 +1,262 @@
// @flow
import React, { Component } from 'react';
import AudioSettingsHeader from './AudioSettingsHeader';
import { translate } from '../../../../base/i18n';
import { IconMicrophoneEmpty, IconVolumeEmpty } from '../../../../base/icons';
import { createLocalAudioTrack } from '../../../functions';
import MicrophoneEntry from './MicrophoneEntry';
import SpeakerEntry from './SpeakerEntry';
export type Props = {
/**
* The deviceId of the microphone in use.
*/
currentMicDeviceId: string,
/**
* The deviceId of the output device in use.
*/
currentOutputDeviceId: string,
/**
* Used to set a new microphone as the current one.
*/
setAudioInputDevice: Function,
/**
* Used to set a new output device as the current one.
*/
setAudioOutputDevice: Function,
/**
* A list of objects containing the labels and deviceIds
* of all the output devices.
*/
outputDevices: Object[],
/**
* A list with objects containing the labels and deviceIds
* of all the input devices.
*/
microphoneDevices: Object[],
/**
* Invoked to obtain translated strings.
*/
t: Function
};
type State = {
/**
* An object containing the jitsiTrack and the error (if the case)
* for the microphone that is in use.
*/
currentMicData: Object
}
/**
* Implements a React {@link Component} which displayes a list of all
* the audio input & output devices to choose from.
*
* @extends Component
*/
class AudioSettingsContent extends Component<Props, State> {
_componentWasUnmounted: boolean;
/**
* Initializes a new {@code AudioSettingsContent} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this);
this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this);
this.state = {
currentMicData: {
error: false,
jitsiTrack: null
}
};
}
_onMicrophoneEntryClick: (string) => void;
/**
* Click handler for the microphone entries.
*
* @param {string} deviceId - The deviceId for the clicked microphone.
* @returns {void}
*/
_onMicrophoneEntryClick(deviceId) {
this.props.setAudioInputDevice(deviceId);
}
_onSpeakerEntryClick: (string) => void;
/**
* Click handler for the speaker entries.
*
* @param {string} deviceId - The deviceId for the clicked speaker.
* @returns {void}
*/
_onSpeakerEntryClick(deviceId) {
this.props.setAudioOutputDevice(deviceId);
}
/**
* Renders a single microphone entry.
*
* @param {Object} data - An object with the deviceId and label of the microphone.
* @param {number} index - The index of the element, used for creating a key.
* @returns {React$Node}
*/
_renderMicrophoneEntry(data, index) {
const { deviceId, label } = data;
const key = `me-${index}`;
const isSelected = deviceId === this.props.currentMicDeviceId;
let jitsiTrack = null;
let hasError = false;
if (isSelected) {
({ jitsiTrack, hasError } = this.state.currentMicData);
}
return (
<MicrophoneEntry
deviceId = { deviceId }
hasError = { hasError }
isSelected = { isSelected }
jitsiTrack = { jitsiTrack }
key = { key }
onClick = { this._onMicrophoneEntryClick }>
{label}
</MicrophoneEntry>
);
}
/**
* Renders a single speaker entry.
*
* @param {Object} data - An object with the deviceId and label of the speaker.
* @param {number} index - The index of the element, used for creating a key.
* @returns {React$Node}
*/
_renderSpeakerEntry(data, index) {
const { deviceId, label } = data;
const key = `se-${index}`;
return (
<SpeakerEntry
deviceId = { deviceId }
isSelected = { deviceId === this.props.currentOutputDeviceId }
key = { key }
onClick = { this._onSpeakerEntryClick }>
{label}
</SpeakerEntry>
);
}
/**
* Disposes the audio track for a given micData object.
*
* @param {Object} micData - The object holding the track.
* @returns {Promise<void>}
*/
_disposeTrack(micData) {
const { jitsiTrack } = micData;
return jitsiTrack ? jitsiTrack.dispose() : Promise.resolve();
}
/**
* Updates the current microphone data.
* Disposes previously created track and creates a new one.
*
* @returns {void}
*/
async _updateCurrentMicData() {
await this._disposeTrack(this.state.currentMicData);
const currentMicData = await createLocalAudioTrack(
this.props.currentMicDeviceId,
);
// In case the component gets unmounted before the track is created
// avoid a leak by not setting the state
if (this._componentWasUnmounted) {
this._disposeTrack(currentMicData);
} else {
this.setState({
currentMicData
});
}
}
/**
* Implements React's {@link Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (prevProps.currentMicDeviceId !== this.props.currentMicDeviceId) {
this._updateCurrentMicData();
}
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._updateCurrentMicData();
}
/**
* Implements React's {@link Component#componentWillUnmount}.
*
* @inheritdoc
*/
componentWillUnmount() {
this._componentWasUnmounted = true;
this._disposeTrack(this.state.currentMicData);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { microphoneDevices, outputDevices, t } = this.props;
return (
<div>
<div className = 'audio-preview-content'>
<AudioSettingsHeader
IconComponent = { IconMicrophoneEmpty }
text = { t('settings.microphones') } />
{microphoneDevices.map((data, i) =>
this._renderMicrophoneEntry(data, i),
)}
<AudioSettingsHeader
IconComponent = { IconVolumeEmpty }
text = { t('settings.speakers') } />
{outputDevices.map((data, i) =>
this._renderSpeakerEntry(data, i),
)}
</div>
</div>
);
}
}
export default translate(AudioSettingsContent);

View File

@@ -0,0 +1,53 @@
// @flow
import React from 'react';
import { Icon, IconCheck, IconExclamationSolid } from '../../../../base/icons';
/**
* The type of the React {@code Component} props of {@link AudioSettingsEntry}.
*/
export type Props = {
/**
* The text for this component.
*/
children: React$Node,
/**
* Flag indicating an error.
*/
hasError?: boolean,
/**
* Flag indicating the selection state.
*/
isSelected: boolean,
};
/**
* React {@code Component} representing an entry for the audio settings.
*
* @returns { ReactElement}
*/
export default function AudioSettingsEntry({ children, hasError, isSelected }: Props) {
const className = `audio-preview-entry ${isSelected
? 'audio-preview-entry--selected' : ''}`;
return (
<div className = { className }>
{isSelected && (
<Icon
className = 'audio-preview-icon audio-preview-icon--check'
color = '#1C2025'
size = { 14 }
src = { IconCheck } />
)}
<span className = 'audio-preview-entry-text'>{children}</span>
{hasError && <Icon
className = 'audio-preview-icon audio-preview-icon--exclamation'
size = { 16 }
src = { IconExclamationSolid } />}
</div>
);
}

View File

@@ -0,0 +1,39 @@
// @flow
import React from 'react';
import { Icon } from '../../../../base/icons';
/**
* The type of the React {@code Component} props of {@link AudioSettingsHeader}.
*/
type Props = {
/**
* The Icon used for the Header.
*/
IconComponent: Function,
/**
* The text of the Header.
*/
text: string,
};
/**
* React {@code Component} representing the Header of an audio option group.
*
* @returns { ReactElement}
*/
export default function AudioSettingsHeader({ IconComponent, text }: Props) {
return (
<div className = 'audio-preview-header'>
<div className = 'audio-preview-header-icon'>
{ <Icon
color = '#A4B8D1'
size = { 24 }
src = { IconComponent } />}
</div>
<div className = 'audio-preview-header-text'>{text}</div>
</div>
);
}

View File

@@ -0,0 +1,97 @@
// @flow
import React from 'react';
import InlineDialog from '@atlaskit/inline-dialog';
import AudioSettingsContent, { type Props as AudioSettingsContentProps } from './AudioSettingsContent';
import { toggleAudioSettings } from '../../../actions';
import {
getAudioInputDeviceData,
getAudioOutputDeviceData,
setAudioInputDevice as setAudioInputDeviceAction,
setAudioOutputDevice as setAudioOutputDeviceAction
} from '../../../../base/devices';
import { connect } from '../../../../base/redux';
import { getAudioSettingsVisibility } from '../../../functions';
import {
getCurrentMicDeviceId,
getCurrentOutputDeviceId
} from '../../../../base/settings';
type Props = AudioSettingsContentProps & {
/**
* Component's children (the audio button).
*/
children: React$Node,
/**
* Flag controlling the visibility of the popup.
*/
isOpen: boolean,
/**
* Callback executed when the popup closes.
*/
onClose: Function,
}
/**
* Popup with audio settings.
*
* @returns {ReactElement}
*/
function AudioSettingsPopup({
children,
currentMicDeviceId,
currentOutputDeviceId,
isOpen,
microphoneDevices,
setAudioInputDevice,
setAudioOutputDevice,
onClose,
outputDevices
}: Props) {
return (
<div className = 'audio-preview'>
<InlineDialog
content = { <AudioSettingsContent
currentMicDeviceId = { currentMicDeviceId }
currentOutputDeviceId = { currentOutputDeviceId }
microphoneDevices = { microphoneDevices }
outputDevices = { outputDevices }
setAudioInputDevice = { setAudioInputDevice }
setAudioOutputDevice = { setAudioOutputDevice } /> }
isOpen = { isOpen }
onClose = { onClose }
position = 'top left'>
{children}
</InlineDialog>
</div>
);
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
return {
currentMicDeviceId: getCurrentMicDeviceId(state),
currentOutputDeviceId: getCurrentOutputDeviceId(state),
isOpen: getAudioSettingsVisibility(state),
microphoneDevices: getAudioInputDeviceData(state),
outputDevices: getAudioOutputDeviceData(state)
};
}
const mapDispatchToProps = {
onClose: toggleAudioSettings,
setAudioInputDevice: setAudioInputDeviceAction,
setAudioOutputDevice: setAudioOutputDeviceAction
};
export default connect(mapStateToProps, mapDispatchToProps)(AudioSettingsPopup);

View File

@@ -0,0 +1,45 @@
// @flow
import React from 'react';
import { Icon, IconMeter } from '../../../../base/icons';
type Props = {
/**
* Own class name for the component.
*/
className: string,
/**
* Flag indicating whether the component is greyed out/disabled.
*/
isDisabled?: boolean,
/**
* The level of the meter.
* Should be between 0 and 7 as per the used SVG.
*/
level: number,
};
/**
* React {@code Component} representing an audio level meter.
*
* @returns { ReactElement}
*/
export default function({ className, isDisabled, level }: Props) {
let ownClassName;
if (level > -1) {
ownClassName = `metr metr-l-${level}`;
} else {
ownClassName = `metr ${isDisabled ? 'metr--disabled' : ''}`;
}
return (
<Icon
className = { `${ownClassName} ${className}` }
size = { 12 }
src = { IconMeter } />
);
}

View File

@@ -0,0 +1,172 @@
// @flow
import React, { Component } from 'react';
import AudioSettingsEntry, { type Props as AudioSettingsEntryProps } from './AudioSettingsEntry';
import JitsiMeetJS from '../../../../base/lib-jitsi-meet/_';
import Meter from './Meter';
const JitsiTrackEvents = JitsiMeetJS.events.track;
type Props = AudioSettingsEntryProps & {
/**
* The deviceId of the microphone.
*/
deviceId: string,
/**
* Flag indicating if there is a problem with the device.
*/
hasError?: boolean,
/**
* The audio track for the current entry.
*/
jitsiTrack: Object,
/**
* Click handler for component.
*/
onClick: Function,
}
type State = {
/**
* The audio level.
*/
level: number,
}
/**
* React {@code Component} representing an entry for the microphone audio settings.
*
* @param {Props} props - The props of the component.
* @returns { ReactElement}
*/
export default class MicrophoneEntry extends Component<Props, State> {
/**
* Initializes a new {@code MicrophoneEntry} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
level: -1
};
this._onClick = this._onClick.bind(this);
this._updateLevel = this._updateLevel.bind(this);
}
_onClick: () => void;
/**
* Click handler for the entry.
*
* @returns {void}
*/
_onClick() {
this.props.onClick(this.props.deviceId);
}
_updateLevel: (number) => void;
/**
* Updates the level of the meter.
*
* @param {number} num - The audio level provided by the jitsiTrack.
* @returns {void}
*/
_updateLevel(num) {
this.setState({
level: Math.floor(num / 0.125)
});
}
/**
* Subscribes to audio level chanages comming from the jitsiTrack.
*
* @returns {void}
*/
_startListening() {
const { jitsiTrack } = this.props;
jitsiTrack && jitsiTrack.on(
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
this._updateLevel);
}
/**
* Unsubscribes from chanages comming from the jitsiTrack.
*
* @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from.
* @returns {void}
*/
_stopListening(jitsiTrack) {
jitsiTrack && jitsiTrack.off(
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
this._updateLevel);
this.setState({
level: -1
});
}
/**
* Implements React's {@link Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: Props) {
if (prevProps.jitsiTrack !== this.props.jitsiTrack) {
this._stopListening(prevProps.jitsiTrack);
this._startListening();
}
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._startListening();
}
/**
* Implements React's {@link Component#componentWillUnmount}.
*
* @inheritdoc
*/
compmonentWillUnmount() {
this._stopListening(this.props.jitsiTrack);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { children, hasError, isSelected } = this.props;
return (
<div
className = 'audio-preview-microphone'
onClick = { this._onClick }>
<AudioSettingsEntry
hasError = { hasError }
isSelected = { isSelected }>
{children}
</AudioSettingsEntry>
<Meter
className = 'audio-preview-meter-mic'
isDisabled = { hasError }
level = { this.state.level } />
</div>
);
}
}

View File

@@ -0,0 +1,119 @@
// @flow
import React, { Component } from 'react';
import AudioSettingsEntry from './AudioSettingsEntry';
import logger from '../../../logger';
import TestButton from './TestButton';
const TEST_SOUND_PATH = 'sounds/ring.wav';
/**
* The type of the React {@code Component} props of {@link SpeakerEntry}.
*/
type Props = {
/**
* The text label for the entry.
*/
children: React$Node,
/**
* Flag controlling the selection state of the entry.
*/
isSelected: boolean,
/**
* The deviceId of the speaker.
*/
deviceId: string,
/**
* Click handler for the component.
*/
onClick: Function,
};
/**
* Implements a React {@link Component} which displays an audio
* output settings entry. The user can click and play a test sound.
*
* @extends Component
*/
export default class SpeakerEntry extends Component<Props> {
/**
* A React ref to the HTML element containing the {@code audio} instance.
*/
audioRef: Object;
/**
* Initializes a new {@code SpeakerEntry} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.audioRef = React.createRef();
this._onTestButtonClick = this._onTestButtonClick.bind(this);
this._onClick = this._onClick.bind(this);
}
_onClick: () => void;
/**
* Click handler for the entry.
*
* @returns {void}
*/
_onClick() {
this.props.onClick(this.props.deviceId);
}
_onTestButtonClick: Object => void;
/**
* Click handler for Test button.
* Sets the current audio output id and plays a sound.
*
* @param {Object} e - The sythetic event.
* @returns {void}
*/
async _onTestButtonClick(e) {
e.stopPropagation();
try {
await this.audioRef.current.setSinkId(this.props.deviceId);
this.audioRef.current.play();
} catch (err) {
logger.log('Could not set sink id', err);
}
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { children, isSelected, deviceId } = this.props;
return (
<div
className = 'audio-preview-speaker'
onClick = { this._onClick }>
<AudioSettingsEntry
isSelected = { isSelected }
key = { deviceId }>
{children}
</AudioSettingsEntry>
<TestButton onClick = { this._onTestButtonClick } />
<audio
preload = 'auto'
ref = { this.audioRef }
src = { TEST_SOUND_PATH } />
</div>
);
}
}

View File

@@ -0,0 +1,26 @@
// @flow
import React from 'react';
type Props = {
/**
* Click handler for the button.
*/
onClick: Function,
};
/**
* React {@code Component} representing an button used for testing output sound.
*
* @returns { ReactElement}
*/
export default function TestButton({ onClick }: Props) {
return (
<div
className = 'audio-preview-test-button'
onClick = { onClick }>
Test
</div>
);
}

View File

@@ -1,2 +1,4 @@
export { default as SettingsButton } from './SettingsButton';
export { default as SettingsDialog } from './SettingsDialog';
export { default as AudioSettingsPopup } from './audio/AudioSettingsPopup';
export { default as VideoSettingsPopup } from './video/VideoSettingsPopup';

View File

@@ -0,0 +1,222 @@
// @flow
import React, { Component } from 'react';
import { translate } from '../../../../base/i18n';
import { equals } from '../../../../base/redux';
import Video from '../../../../base/media/components/Video';
import { createLocalVideoTracks } from '../../../functions';
const videoClassName = 'video-preview-video flipVideoX';
/**
* The type of the React {@code Component} props of {@link VideoSettingsContent}.
*/
export type Props = {
/**
* The deviceId of the camera device currently being used.
*/
currentCameraDeviceId: string,
/**
* Callback invoked to change current camera.
*/
setVideoInputDevice: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* Callback invoked to toggle the settings popup visibility.
*/
toggleVideoSettings: Function,
/**
* All the camera device ids currently connected.
*/
videoDeviceIds: string[],
};
/**
* The type of the React {@code Component} state of {@link VideoSettingsContent}.
*/
type State = {
/**
* An array of all the jitsiTracks and eventual errors.
*/
trackData: Object[],
};
/**
* Implements a React {@link Component} which displays a list of video
* previews to choose from.
*
* @extends Component
*/
class VideoSettingsContent extends Component<Props, State> {
_componentWasUnmounted: boolean;
/**
* Initializes a new {@code VideoSettingsContent} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
trackData: new Array(props.videoDeviceIds.length).fill({
jitsiTrack: null
})
};
}
/**
* Creates and updates the track data.
*
* @returns {void}
*/
async _setTracks() {
this._disposeTracks(this.state.trackData);
const trackData = await createLocalVideoTracks(
this.props.videoDeviceIds,
);
// In case the component gets unmounted before the tracks are created
// avoid a leak by not setting the state
if (this._componentWasUnmounted) {
this._disposeTracks(trackData);
} else {
this.setState({
trackData
});
}
}
/**
* Destroys all the tracks from trackData object.
*
* @param {Object[]} trackData - An array of tracks that are to be disposed.
* @returns {Promise<void>}
*/
_disposeTracks(trackData) {
trackData.forEach(({ jitsiTrack }) => {
jitsiTrack && jitsiTrack.dispose();
});
}
/**
* Returns the click handler used when selecting the video preview.
*
* @param {string} deviceId - The id of the camera device.
* @returns {Function}
*/
_onEntryClick(deviceId) {
return () => {
this.props.setVideoInputDevice(deviceId);
this.props.toggleVideoSettings();
};
}
/**
* Renders a preview entry.
*
* @param {Object} data - The track data.
* @param {number} index - The index of the entry.
* @returns {React$Node}
*/
_renderPreviewEntry(data, index) {
const { error, jitsiTrack, deviceId } = data;
const { currentCameraDeviceId, t } = this.props;
const isSelected = deviceId === currentCameraDeviceId;
const key = `vp-${index}`;
const className = 'video-preview-entry';
if (error) {
return (
<div
className = { className }
key = { key }>
<div className = 'video-preview-error'>{t(error)}</div>
</div>
);
}
const props: Object = {
className,
key
};
const label = jitsiTrack && jitsiTrack.getTrackLabel();
if (isSelected) {
props.className = `${className} video-preview-entry--selected`;
} else {
props.onClick = this._onEntryClick(deviceId);
}
return (
<div { ...props }>
<div className = 'video-preview-label'>{label}</div>
<div className = 'video-preview-overlay' />
<Video
className = { videoClassName }
videoTrack = {{ jitsiTrack }} />
</div>
);
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._setTracks();
}
/**
* Implements React's {@link Component#componentWillUnmount}.
*
* @inheritdoc
*/
componentWillUnmount() {
this._componentWasUnmounted = true;
this._disposeTracks(this.state.trackData);
}
/**
* Implements React's {@link Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) {
this._setTracks();
}
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { trackData } = this.state;
return (
<div className = 'video-preview'>
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
</div>
);
}
}
export default translate(VideoSettingsContent);

View File

@@ -0,0 +1,85 @@
// @flow
import React from 'react';
import InlineDialog from '@atlaskit/inline-dialog';
import { toggleVideoSettings } from '../../../actions';
import {
getVideoDeviceIds,
setVideoInputDevice as setVideoInputDeviceAction
} from '../../../../base/devices';
import { getVideoSettingsVisibility } from '../../../functions';
import { connect } from '../../../../base/redux';
import { getCurrentCameraDeviceId } from '../../../../base/settings';
import VideoSettingsContent, { type Props as VideoSettingsProps } from './VideoSettingsContent';
type Props = VideoSettingsProps & {
/**
* Component children (the Video button).
*/
children: React$Node,
/**
* Flag controlling the visibility of the popup.
*/
isOpen: boolean,
/**
* Callback executed when the popup closes.
*/
onClose: Function,
}
/**
* Popup with a preview of all the video devices.
*
* @returns {ReactElement}
*/
function VideoSettingsPopup({
currentCameraDeviceId,
children,
isOpen,
onClose,
setVideoInputDevice,
videoDeviceIds
}: Props) {
return (
<div className = 'video-preview'>
<InlineDialog
content = { <VideoSettingsContent
currentCameraDeviceId = { currentCameraDeviceId }
setVideoInputDevice = { setVideoInputDevice }
toggleVideoSettings = { onClose }
videoDeviceIds = { videoDeviceIds } /> }
isOpen = { isOpen }
onClose = { onClose }
position = 'top right'>
{ children }
</InlineDialog>
</div>
);
}
/**
* Maps (parts of) the redux state to the associated {@code VideoSettingsPopup}'s
* props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
return {
currentCameraDeviceId: getCurrentCameraDeviceId(state),
isOpen: getVideoSettingsVisibility(state),
videoDeviceIds: getVideoDeviceIds(state)
};
}
const mapDispatchToProps = {
onClose: toggleVideoSettings,
setVideoInputDevice: setVideoInputDeviceAction
};
export default connect(mapStateToProps, mapDispatchToProps)(VideoSettingsPopup);

View File

@@ -2,6 +2,7 @@
import { toState } from '../base/redux';
import { parseStandardURIString } from '../base/util';
import { i18next, DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n';
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
import {
getLocalParticipant,
isLocalParticipantModerator
@@ -130,3 +131,73 @@ export function getProfileTabProps(stateful: Object | Function) {
email: localParticipant.email
};
}
/**
* Returns a promise which resolves with a list of objects containing
* all the video jitsiTracks and appropriate errors for the given device ids.
*
* @param {string[]} ids - The list of the camera ids for wich to create tracks.
*
* @returns {Promise<Object[]>}
*/
export function createLocalVideoTracks(ids: string[]) {
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId)
.then(jitsiTrack => {
return {
jitsiTrack,
deviceId
};
})
.catch(() => {
return {
jitsiTrack: null,
deviceId,
error: 'deviceSelection.previewUnavailable'
};
})));
}
/**
* Returns a promise which resolves with an object containing the corresponding
* the audio jitsiTrack/error.
*
* @param {string} deviceId - The deviceId for the current microphone.
*
* @returns {Promise<Object>}
*/
export function createLocalAudioTrack(deviceId: string) {
return createLocalTrack('audio', deviceId)
.then(jitsiTrack => {
return {
hasError: false,
jitsiTrack
};
})
.catch(() => {
return {
hasError: true,
jitsiTrack: null
};
});
}
/**
* Returns the visibility state of the audio settings.
*
* @param {Object} state - The state of the application.
* @returns {boolean}
*/
export function getAudioSettingsVisibility(state: Object) {
return state['features/settings'].audioSettingsVisible;
}
/**
* Returns the visibility state of the video settings.
*
* @param {Object} state - The state of the application.
* @returns {boolean}
*/
export function getVideoSettingsVisibility(state: Object) {
return state['features/settings'].videoSettingsVisible;
}

View File

@@ -2,7 +2,11 @@
import { ReducerRegistry } from '../base/redux';
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
import {
SET_AUDIO_SETTINGS_VISIBILITY,
SET_SETTINGS_VIEW_VISIBLE,
SET_VIDEO_SETTINGS_VISIBILITY
} from './actionTypes';
ReducerRegistry.register('features/settings', (state = {}, action) => {
switch (action.type) {
@@ -11,6 +15,16 @@ ReducerRegistry.register('features/settings', (state = {}, action) => {
...state,
visible: action.visible
};
case SET_AUDIO_SETTINGS_VISIBILITY:
return {
...state,
audioSettingsVisible: action.value
};
case SET_VIDEO_SETTINGS_VISIBILITY:
return {
...state,
videoSettingsVisible: action.value
};
}
return state;

View File

@@ -0,0 +1,127 @@
// @flow
import React, { Component } from 'react';
import AudioMuteButton from '../AudioMuteButton';
import { hasAvailableDevices } from '../../../base/devices';
import { IconArrowDown } from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { ToolboxButtonWithIcon } from '../../../base/toolbox';
import { connect } from '../../../base/redux';
import { AudioSettingsPopup, toggleAudioSettings } from '../../../settings';
type Props = {
/**
* Click handler for the small icon. Opens audio options.
*/
onAudioOptionsClick: Function,
/**
* If the user has audio input or audio output devices.
*/
hasDevices: boolean,
/**
* Flag controlling the visibility of the button.
*/
visible: boolean,
};
type State = {
/**
* If there are permissions for audio devices.
*/
hasPermissions: boolean,
}
/**
* Button used for audio & audio settings.
*
* @returns {ReactElement}
*/
class AudioSettingsButton extends Component<Props, State> {
/**
* Initializes a new {@code AudioSettingsButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
hasPermissions: false
};
}
/**
* Updates device permissions.
*
* @returns {Promise<void>}
*/
async _updatePermissions() {
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
'audio',
);
this.setState({
hasPermissions
});
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._updatePermissions();
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { hasDevices, onAudioOptionsClick, visible } = this.props;
const settingsDisabled = !this.state.hasPermissions || !hasDevices;
return visible ? (
<AudioSettingsPopup>
<ToolboxButtonWithIcon
icon = { IconArrowDown }
iconDisabled = { settingsDisabled }
onIconClick = { onAudioOptionsClick }>
<AudioMuteButton />
</ToolboxButtonWithIcon>
</AudioSettingsPopup>
) : null;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
return {
hasDevices:
hasAvailableDevices(state, 'audioInput')
|| hasAvailableDevices(state, 'audioOutput')
};
}
const mapDispatchToProps = {
onAudioOptionsClick: toggleAudioSettings
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AudioSettingsButton);

View File

@@ -71,7 +71,7 @@ import {
setOverflowMenuVisible,
setToolbarHovered
} from '../../actions';
import AudioMuteButton from '../AudioMuteButton';
import AudioSettingsButton from './AudioSettingsButton';
import DownloadButton from '../DownloadButton';
import { isToolboxVisible } from '../../functions';
import HangupButton from '../HangupButton';
@@ -80,7 +80,7 @@ import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import MuteEveryoneButton from './MuteEveryoneButton';
import ToolbarButton from './ToolbarButton';
import VideoMuteButton from '../VideoMuteButton';
import VideoSettingsButton from './VideoSettingsButton';
import {
ClosedCaptionButton
} from '../../../subtitles';
@@ -1116,6 +1116,32 @@ class Toolbox extends Component<Props, State> {
});
}
/**
* Renders the Audio controlling button.
*
* @returns {ReactElement}
*/
_renderAudioButton() {
return this._shouldShowButton('microphone')
? <AudioSettingsButton
key = 'asb'
visible = { true } />
: null;
}
/**
* Renders the Video controlling button.
*
* @returns {ReactElement}
*/
_renderVideoButton() {
return this._shouldShowButton('camera')
? <VideoSettingsButton
key = 'vsb'
visible = { true } />
: null;
}
/**
* Renders the toolbox content.
*
@@ -1234,12 +1260,10 @@ class Toolbox extends Component<Props, State> {
}
</div>
<div className = 'button-group-center'>
<AudioMuteButton
visible = { this._shouldShowButton('microphone') } />
{ this._renderAudioButton() }
<HangupButton
visible = { this._shouldShowButton('hangup') } />
<VideoMuteButton
visible = { this._shouldShowButton('camera') } />
{ this._renderVideoButton() }
</div>
<div className = 'button-group-right'>
{ buttonsRight.indexOf('localrecording') !== -1
@@ -1303,6 +1327,7 @@ function _mapStateToProps(state) {
let { desktopSharingEnabled } = state['features/base/conference'];
const {
callStatsID,
enableFeaturesBasedOnToken,
iAmRecorder
} = state['features/base/config'];
const sharedVideoStatus = state['features/shared-video'].status;
@@ -1318,7 +1343,7 @@ function _mapStateToProps(state) {
let desktopSharingDisabledTooltipKey;
if (state['features/base/config'].enableFeaturesBasedOnToken) {
if (enableFeaturesBasedOnToken) {
// we enable desktop sharing if any participant already have this
// feature enabled
desktopSharingEnabled = getParticipants(state)

View File

@@ -0,0 +1,124 @@
// @flow
import React, { Component } from 'react';
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
import VideoMuteButton from '../VideoMuteButton';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { hasAvailableDevices } from '../../../base/devices';
import { IconArrowDown } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox';
type Props = {
/**
* Click handler for the small icon. Opens video options.
*/
onVideoOptionsClick: Function,
/**
* If the user has any video devices.
*/
hasDevices: boolean,
/**
* Flag controlling the visibility of the button.
*/
visible: boolean,
};
type State = {
/**
* Whether the app has video permissions or not.
*/
hasPermissions: boolean,
};
/**
* Button used for video & video settings.
*
* @returns {ReactElement}
*/
class VideoSettingsButton extends Component<Props, State> {
/**
* Initializes a new {@code VideoSettingsButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
hasPermissions: false
};
}
/**
* Updates device permissions.
*
* @returns {Promise<void>}
*/
async _updatePermissions() {
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
'video',
);
this.setState({
hasPermissions
});
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._updatePermissions();
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { hasDevices, onVideoOptionsClick, visible } = this.props;
const iconDisabled = !this.state.hasPermissions || !hasDevices;
return visible ? (
<VideoSettingsPopup>
<ToolboxButtonWithIcon
icon = { IconArrowDown }
iconDisabled = { iconDisabled }
onIconClick = { onVideoOptionsClick }>
<VideoMuteButton />
</ToolboxButtonWithIcon>
</VideoSettingsPopup>
) : null;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
return {
hasDevices: hasAvailableDevices(state, 'videoInput')
};
}
const mapDispatchToProps = {
onVideoOptionsClick: toggleVideoSettings
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(VideoSettingsButton);

View File

@@ -40,6 +40,8 @@ export function isToolboxVisible(state: Object) {
timeoutID,
visible
} = state['features/toolbox'];
const { audioSettingsVisible, videoSettingsVisible } = state['features/settings'];
return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible));
return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible
|| audioSettingsVisible || videoSettingsVisible));
}