Compare commits

...

60 Commits

Author SHA1 Message Date
damencho
029ca1753f Fixes renewing let's encrypt certificates when using jetty.
Uses webroot for obtaining certificate, avoids stopping jitsi-videobridge before obtaining certificate.
Adds a renew hook, where we reload apache or nginx and try to graceful shutdown jvb and restart it.
Before merging this we need to make sure graceful shutdown is enabled by default and also pubsub is enabled by default so after restarting jvb, jicofo will discover it.
2017-03-30 13:57:47 -05:00
virtuacoplenny
2301732e2d style: catalog all z-indexes and move toolbar down
All z-indexes found in css files have been moved into css
variables. If the z-index is used only once, the variable
name will be the same as the selector it is used in. If
the z-index is used multiple times, then the plain name
of $zindex# was used. This allowed a more confident
moving down of the toolbar so that the new modal dialog,
with z-index 500, could display on top of it.

#1436
2017-03-30 18:13:00 +01:00
virtuacoplenny
24ee8eb16a electron: add desktop picker
#1411
2017-03-30 17:58:31 +01:00
Lyubo Marinov
57065bb274 Update NPM dependencies/packages 2017-03-30 09:11:02 -05:00
Saúl Ibarra Corretgé
08531ee675 Merge pull request #1443 from ibc/master
edge: Add userMedia.edgeGrantPermissions in lang/main.json
2017-03-29 13:35:51 +02:00
Iñaki Baz Castillo
e7140ffec7 edge: Add userMedia.edgeGrantPermissions in lang/main.json 2017-03-29 13:03:57 +02:00
damencho
c58c4b7938 Commit from translate.jitsi.org by user damencho.: 306 of 318 strings translated (0 fuzzy). 2017-03-28 21:29:17 +00:00
Lyubo Marinov
4e276471e5 Comply w/ coding style: consistency 2017-03-28 11:43:33 -05:00
Saúl Ibarra Corretgé
c5eac63da1 [RN] Move all mobile only features to a subdirectory 2017-03-28 09:36:00 -05:00
Saúl Ibarra Corretgé
866c6d0cf9 Merge pull request #1378 from saghul/doc-api-params
doc: improve docs on external API constructor parameters
2017-03-28 11:26:14 +02:00
Lyubo Marinov
165294bfb1 Comply w/ coding style 2017-03-27 22:50:47 -05:00
Saúl Ibarra Corretgé
2d5f0479bd [RN] Disable remote video while in the background
Set the video channel "last N" property to 0, thus making the client not receive
any remote video.
2017-03-27 22:11:13 -05:00
yanas
e8068cf5ac Merge pull request #1393 from jitsi/filmstrip_overlays
Filmstrip overlays
2017-03-27 14:54:45 -05:00
yanas
d0171cf386 Merge pull request #1435 from jitsi/fix-settings-translation
Fixes settings panel translation.
2017-03-27 14:52:01 -05:00
hristoterezov
3ae99ea0b9 feat(overlays): for filmstrip only mode 2017-03-27 14:20:25 -05:00
damencho
4e9450f200 Fixes settings panel translation.
Strings are not translated when opening the settings side panel. It was that we were creating settings panel html after i18n library had loaded and had translated the rest of the html.
The element selecting the current language was also not translated, which end up with no selection in the UI for the current language.
2017-03-27 13:54:14 -05:00
Saúl Ibarra Corretgé
dc2c49f4a9 doc: improve docs on external API constructor parameters 2017-03-27 12:17:32 +02:00
hristoterezov
c461e8b63c ref(overlays): Replace the abstract class for overlays with overlay frame component
In this case makes more sense to have overlay frame included in every overlay instead
of abstract class that implements the overlay frame and have to be extended by every
overlay. In addition, mapStateToProps isn't working well with inheritance.
2017-03-24 13:16:14 -05:00
Saúl Ibarra Corretgé
f47bc1163b Merge pull request #1432 from jitsi/speaker-stats-analytics-event
Sends analytics event every time speaker stats is open.
2017-03-24 16:35:59 +01:00
Дамян Минков
851be2d76e Merge pull request #1385 from saghul/make-update-deps
build: remove no longer needed Makefile rule
2017-03-24 10:13:47 -05:00
damencho
63034e6cba Sends analytics event everytime speaker stats is open. 2017-03-24 10:07:46 -05:00
Lyubo Marinov
84b9c5f5fd Coding style 2017-03-24 09:06:54 -05:00
Saúl Ibarra Corretgé
43c8fc6847 [RN] Fix mirroring video views on platforms with native support 2017-03-24 09:02:32 -05:00
Saúl Ibarra Corretgé
bc60bd23b2 build: remove no longer needed Makefile rule
- we now use pinned dependencies, so there is no need to run npm update
- AFAICT the node-sass workaround is no longer needed
2017-03-24 11:02:09 +01:00
damencho
e29120a9c1 Changes lastN event params to leaving and entering endpoint IDs.
Uses leavingIDs to more efficiently iterate over remote videos.
2017-03-23 09:32:27 -05:00
damencho
d383230532 Removes unused code. 2017-03-23 09:32:27 -05:00
bbaldino
9a46896600 Merge pull request #1402 from jitsi/p2p_ver2
P2P ver2
2017-03-22 16:10:13 -07:00
paweldomas
fba086134d add default STUN servers to config.js 2017-03-22 11:23:30 -05:00
paweldomas
2973364c02 feat(stats - show more): local p2p transport indication
Will show (direct) next to the UPD or TCP transport type if we're
running on P2P connection.
2017-03-22 11:23:30 -05:00
paweldomas
542bb7caed doc: add FIXME 2017-03-22 11:23:29 -05:00
paweldomas
fb47b6ae21 feat: add test P2P methods 2017-03-22 11:23:29 -05:00
hristoterezov
aeb301c8d5 feat(iframe_api): Add jwt token parameter 2017-03-21 22:34:44 +01:00
yanas
704e14f008 Handle last n in the client (#1389)
* Handle last n in the client

* fix(LargeVideoManager.js): Fixes check for low bandwidth. Needs more work

* fix(LargeVideoManager.js): Fixes the Shared Video test.

* fix(LargeVideoManager): Fix shared video view and remove last n checks.

* fix(LargeVideoManager): Fixes jsdoc comment

* fix(RemoteVideo): Fix connection status check

* fix(LargeVideoManager,RemoteVideo): Syntax errors
2017-03-21 12:14:13 -05:00
Lyubo Marinov
d1050d6b02 Update NPM dependencies/packages 2017-03-21 09:22:53 -05:00
Aaron van Meerten
afc96808e8 added support of static directory in debian install and Makefile for source package 2017-03-20 19:22:06 -05:00
Aaron van Meerten
dc2bae4ae1 Merge pull request #1420 from jitsi/static-content-folder
Moves all static content/files in a new folder.
2017-03-20 17:13:10 -05:00
damencho
1d7da21e48 Moves all static content/files in a new folder.
Also clears debian package from including *.js files from source roote folder, files which were not used.
2017-03-20 16:05:11 -05:00
Ilya Daynatovich
affd965d5d Remove an unnecessary file 2017-03-20 14:42:54 -05:00
Leonard Kim
989161159d Modal dialog for displaying dominant speaker times 2017-03-20 12:47:20 -05:00
Ilya Daynatovich
59a74153dc Toolbar notice as React Component 2017-03-20 11:27:08 -05:00
yanas
6690c269ef Merge pull request #1413 from jitsi/fix-missing-translation
Fixes wrong i18n key for somebody.
2017-03-20 11:03:45 -05:00
George Politis
b7fd10b905 Merge pull request #1412 from jitsi/framerate-update
Updates framerate using local statistics.
2017-03-17 17:17:24 -05:00
damencho
08e1cf1b7e Fixes wrong i18n key for somebody. 2017-03-17 16:43:43 -05:00
damencho
54d891afa7 Updates framerate using local statistics. 2017-03-17 16:10:45 -05:00
Aaron van Meerten
ae41782cd4 Merge pull request #1410 from jitsi/letsencrypt-script
Adds a script which install certificates from let's encrypt.
2017-03-17 15:53:52 -05:00
damencho
8591fe00b6 Adds a script which install certificates from let's encrypt.
The script looks for nginx, apache2 or jetty configuration and edits the first one found. Nginx and apache2 will be reloaded, while jvb will be stopped, configured and started again.
2017-03-17 14:49:10 -05:00
Saúl Ibarra Corretgé
92f58cb3c1 doc: add information about how to contribute
Inspired by the document at jitsi/jitsi.
2017-03-17 08:35:56 -05:00
Saúl Ibarra Corretgé
4ad98ca505 doc: fix typo 2017-03-17 10:48:38 +01:00
yanas
b9374bde6b Merge pull request #1383 from jitsi/fix-wrong-pass-on-auth
Fix incorrect password dialog message when using authentication.
2017-03-16 14:13:18 -05:00
pierreozoux
1ff29384b3 Add a network schemas
This is to help understanding how things are wired.
2017-03-16 11:30:28 -05:00
Saúl Ibarra Corretgé
4fc714ff10 lang: don't use " " for the default policy text
Firefox (at least) renders it verbatim.
2017-03-16 11:16:14 -05:00
damencho
51f0c8a388 Adds base dialog implementation. 2017-03-15 16:33:04 -05:00
damencho
d01a65f73d Fixes stats, using wrong object members in latest update. 2017-03-15 15:48:13 -05:00
damencho
65239f9ffe Adds frame rate to statistics bubble. 2017-03-15 13:31:42 -05:00
damencho
e5cefcce70 Updates transport type.
Updates transport type to show multiple values as we do for addresses and ports.
2017-03-14 15:51:11 -05:00
Ingo Bauersachs
8002b5ec6a Add Esperanto 2017-03-14 20:36:09 +01:00
jitsi-pootle
a575f5cc77 New files added from translate.jitsi.org based on templates 2017-03-14 19:44:37 +00:00
ibauersachs
ab3a80e076 Commit from translate.jitsi.org by user ibauersachs.: 317 of 317 strings translated (0 fuzzy). 2017-03-14 19:44:07 +00:00
ibauersachs
dda3798ba9 Commit from translate.jitsi.org by user ibauersachs.: 317 of 317 strings translated (0 fuzzy). 2017-03-14 19:43:50 +00:00
damencho
5f387737a1 Fix incorrect password dialog message when using authentication. 2017-03-07 16:42:36 -06:00
121 changed files with 4251 additions and 1179 deletions

33
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,33 @@
# How to contribute
We would love to have your help. Before you start working however, please read
and follow this short guide.
# Reporting Issues
Before you open an issue on GitHub, please discuss it on one of our
[mailing lists](https://jitsi.org/Development/MailingLists) and wait for
confirmation from one of the committers. Once you have that confirmation,
please proceed to reporting the issue on GitHub, while providing as much
information as possible. Mention the version of Jitsi Meet, Jicofo and JVB
you are using, and explain (as detailed as you can) how the problem can
be reproduced.
# Code contributions
Found a bug and know how to fix it? Great! Please read on.
## Contributor License Agreement
While the Jitsi projects are released under the
[Apache License 2.0](https://github.com/jitsi/jitsi-meet/blob/master/LICENSE), the copyright
holder and principal creator is [Atlassian](https://www.atlassian.com/). To
ensure that we can continue making these projects available under an Open Source license,
we need you to sign our Apache-based contributor
license agreement as either a [corporation](https://jitsi.org/ccla) or an
[individual](https://jitsi.org/icla). If you cannot accept the terms laid out
in the agreement, unfortunately, we cannot accept your contribution.
## Creating Pull Requests
- Make sure your code passes the linter rules beforehand. The linter is exeuted
automatically when committing code.
- Perform **one** logical change per pull request.
- Maintain a clean list of commits, squash them if necessary.
- Rebase your topic branch on top of the master branch before creating the pull
request.

View File

@@ -10,13 +10,7 @@ STYLES_DESTINATION = css/all.css
STYLES_MAIN = css/main.scss
WEBPACK = ./node_modules/.bin/webpack
all: update-deps compile deploy clean
# FIXME: there is a problem with node-sass not correctly installed (compiled)
# a quick fix to make sure it is installed on every update
# the problem appears on linux and not on macosx
update-deps:
$(NPM) update && $(NPM) install node-sass
all: compile deploy clean
compile:
$(WEBPACK) -p
@@ -55,7 +49,7 @@ deploy-local:
source-package:
mkdir -p source_package/jitsi-meet/css && \
cp -r *.js *.html connection_optimization favicon.ico fonts images libs sounds LICENSE lang source_package/jitsi-meet && \
cp -r *.js *.html connection_optimization favicon.ico fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \
cp css/all.css source_package/jitsi-meet/css && \
(cd source_package ; tar cjf ../jitsi-meet.tar.bz2 jitsi-meet) && \
rm -rf source_package

View File

@@ -19,7 +19,7 @@ You can download Debian/Ubuntu binaries:
* [testing](https://download.jitsi.org/testing/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianTestingRepository))
* [nightly](https://download.jitsi.org/unstable/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianNightlyRepository))
You can get our mobile versoins from here:
You can get our mobile versions from here:
* [Android](https://play.google.com/store/apps/details?id=org.jitsi.meet)
* [iOS](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
@@ -86,6 +86,11 @@ npm unlink lib-jitsi-meet
npm install
```
## Contributing
If you are looking to contribute to Jitsi Meet, first of all, thank you! Please
see our [guidelines for contributing](CONTRIBUTING.md).
## Embedding in external applications
Jitsi Meet provides a very flexible way of embedding it in external applications by using the [Jitsi Meet API](doc/api.md).

View File

@@ -39,6 +39,9 @@ import {
participantLeft,
participantRoleChanged
} from './react/features/base/participants';
import {
showDesktopPicker
} from './react/features/desktop-picker';
import {
mediaPermissionPromptVisibilityChanged,
suspendDetected
@@ -66,6 +69,16 @@ let DSExternalInstallationInProgress = false;
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
/*
* Logic to open a desktop picker put on the window global for
* lib-jitsi-meet to detect and invoke
*/
window.JitsiMeetScreenObtainer = {
openDesktopPicker(onSourceChoose) {
APP.store.dispatch(showDesktopPicker(onSourceChoose));
}
};
/**
* Known custom conference commands.
*/
@@ -252,8 +265,8 @@ function maybeRedirectToWelcomePage(options) {
// save whether current user is guest or not, before navigating
// to close page
window.sessionStorage.setItem('guest', APP.tokenData.isGuest);
assignWindowLocationPathname(
options.feedbackSubmitted ? "close.html" : "close2.html");
assignWindowLocationPathname('static/'
+ (options.feedbackSubmitted ? "close.html" : "close2.html"));
return;
}
@@ -402,7 +415,7 @@ class ConferenceConnector {
case ConferenceErrors.NOT_ALLOWED_ERROR:
{
// let's show some auth not allowed page
assignWindowLocationPathname('authError.html');
assignWindowLocationPathname('static/authError.html');
}
break;
@@ -714,6 +727,18 @@ export default {
sendFeedback (overallFeedback, detailedFeedback) {
return room.sendFeedback (overallFeedback, detailedFeedback);
},
/**
* Get speaker stats that track total dominant speaker time.
*
* @returns {object} A hash with keys being user ids and values being the
* library's SpeakerStats model used for calculating time as dominant
* speaker.
*/
getSpeakerStats() {
return room.getSpeakerStats();
},
/**
* Returns the connection times stored in the library.
*/
@@ -729,6 +754,39 @@ export default {
return this._room
&& this._room.getConnectionState();
},
/**
* Obtains current P2P ICE connection state.
* @return {string|null} ICE connection state or <tt>null</tt> if there's no
* P2P connection
*/
getP2PConnectionState () {
return this._room
&& this._room.getP2PConnectionState();
},
/**
* Starts P2P (for tests only)
* @private
*/
_startP2P () {
try {
this._room && this._room.startP2PSession();
} catch (error) {
logger.error("Start P2P failed", error);
throw error;
}
},
/**
* Stops P2P (for tests only)
* @private
*/
_stopP2P () {
try {
this._room && this._room.stopP2PSession();
} catch (error) {
logger.error("Stop P2P failed", error);
throw error;
}
},
/**
* Checks whether or not our connection is currently in interrupted and
* reconnect attempts are in progress.
@@ -1259,20 +1317,12 @@ export default {
APP.UI.showCustomToolbarPopup('#talkWhileMutedPopup', true, 5000);
});
/*
room.on(ConferenceEvents.IN_LAST_N_CHANGED, (inLastN) => {
//FIXME
if (config.muteLocalVideoIfNotInLastN) {
// TODO mute or unmute if required
// mark video on UI
// APP.UI.markVideoMuted(true/false);
}
});
*/
room.on(
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
APP.UI.handleLastNEndpoints(ids, enteringIds);
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED,
(leavingIds, enteringIds) => {
APP.UI.handleLastNEndpoints(leavingIds, enteringIds);
});
room.on(
ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
(id, isActive) => {
@@ -1928,5 +1978,18 @@ export default {
*/
removeListener (eventName, listener) {
eventEmitter.removeListener(eventName, listener);
},
/**
* Checks if the participant given by participantId is currently in the
* last N set if there's one supported.
*
* @param participantId the identifier of the participant
* @returns {boolean} {true} if the participant given by the participantId
* is currently in the last N set or if there's no last N set at this point
* and {false} otherwise
*/
isInLastN (participantId) {
return room.isInLastN(participantId);
}
};

View File

@@ -20,6 +20,13 @@ var config = { // eslint-disable-line no-unused-vars
//focusUserJid: 'focus@auth.jitsi-meet.example.com', // The real JID of focus participant - can be overridden here
//defaultSipNumber: '', // Default SIP number
// The STUN servers that will be used in the peer to peer connections
p2pStunServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" },
{ urls: "stun:stun2.l.google.com:19302" }
],
// The ID of the jidesha extension for Chrome.
desktopSharingChromeExtId: null,
// Whether desktop sharing should be disabled on Chrome.
@@ -80,5 +87,14 @@ var config = { // eslint-disable-line no-unused-vars
// disables or enables RTX (RFC 4588) (defaults to false).
disableRtx: false,
// Sets the preferred resolution (height) for local video. Defaults to 360.
resolution: 720
resolution: 720,
// Enables peer to peer mode. When enabled system will try to establish
// direct connection given that there are exactly 2 participants in
// the room. If that succeeds the conference will stop sending data through
// the JVB and use the peer to peer connection instead. When 3rd participant
// joins the conference will be moved back to the JVB connection.
//enableP2P: true
// How long we're going to wait, before going back to P2P after
// the 3rd participant has left the conference (to filter out page reload)
//backToP2PDelay: 5
};

View File

@@ -84,7 +84,7 @@ form {
height: 74px;
background-size: contain;
background-repeat: no-repeat;
z-index: 2;
z-index: $zindex2;
}
.leftwatermark {
@@ -106,7 +106,7 @@ form {
font-size: 11pt;
color: rgba(255,255,255,.50);
text-decoration: none;
z-index: 100;
z-index: $poweredByZ;
}
.connected {

View File

@@ -212,24 +212,24 @@
line-height: 30px;
}
::-webkit-scrollbar {
:not(.default-scrollbar)::-webkit-scrollbar {
background: #06a5df;
width: 7px;
}
::-webkit-scrollbar-button {
:not(.default-scrollbar)::-webkit-scrollbar-button {
display: none;
}
::-webkit-scrollbar-track {
:not(.default-scrollbar)::-webkit-scrollbar-track {
background: black;
}
::-webkit-scrollbar-track-piece {
:not(.default-scrollbar)::-webkit-scrollbar-track-piece {
background: black;
}
::-webkit-scrollbar-thumb {
:not(.default-scrollbar)::-webkit-scrollbar-thumb {
background: #06a5df;
border-radius: 4px;
}

View File

@@ -17,7 +17,7 @@
flex-direction: column-reverse;
flex-wrap: nowrap;
position: relative;
z-index: 1; // Set z-index to make element visible
z-index: $zindex1; // Set z-index to make element visible
width: $hideFilmstripButtonWidth;
button {
@@ -55,7 +55,7 @@
bottom: 0;
width:auto;
border: $thumbnailsBorder solid transparent;
z-index: 5;
z-index: $filmstripVideosZ;
transition: bottom 2s;
overflow: visible !important;
/*!!!Removes the gap between the local video container and the remote

View File

@@ -27,7 +27,86 @@
font-size: 50px;
}
&__button {
float: none !important;
&-filmstrip-only {
background-color: $inlayFilmstripOnlyBg;
color: $inlayFilmstripOnlyColor;
margin-left: 20px;
margin-right: 20px;
margin-top: 20px;
bottom: 30px;
position: absolute;
display: flex;
max-height: 120px;
height: 80%;
right: 0px;
border-radius: 4px;
overflow: hidden;
&__content {
padding: 20px;
display: flex;
justify-content: center;
position: relative;
> .button-control {
align-self: center;
}
> #reloadProgressBar {
position: absolute;
left: 0px;
bottom: 0px;
margin-bottom: 0px;
width: 100%;
border-radius: 0px;
> .aui-progress-indicator-value {
border-radius: 0px;
}
}
}
&__title {
font-size: 18px;
font-weight: 600;
}
&__container {
align-self: center;
}
&__text {
margin-top: 10px;
font-size: 14px;
font-weight: 600;
}
&__icon {
font-size: 50px;
align-self: center;
color: $inlayIconColor;
opacity: 0.6;
}
&__icon-container {
text-align: center;
display: flex;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
top: 0px;
}
&__avatar-container {
position: relative;
> img {
height: 100%;
}
}
&__icon-background {
background: $inlayIconBg;
opacity: 0.6;
position: absolute;
width: 100%;
height: 100%;
top: 0px;
}
}
}
}

View File

@@ -2,7 +2,7 @@
position: absolute;
top: 0;
left: 0;
z-index: 1010;
z-index: $jitsipopoverZ;
display: none;
max-width: 300px;
min-width: 100px;

View File

@@ -143,7 +143,7 @@
position: absolute;
top: 50%;
right: 8px;
z-index: 1;
z-index: $zindex1;
width: 0;
height: 0;
content: '';

View File

@@ -6,7 +6,7 @@
overflow: hidden;
padding: 20px;
margin-left: 10px;
z-index: 10;
z-index: $zindex10;
border-radius: $borderRadius;
background-attachment: scroll;
background-size: auto auto;

View File

@@ -1,11 +1,12 @@
#notice {
.notice {
position: relative;
z-index: 3;
z-index: $zindex3;
margin-top: 6px;
&__message {
background-color: #000000;
color: white;
padding: 3px;
border-radius: 5px;
}
}
#noticeText {
background-color: #000000;
color: white;
padding: 3px;
border-radius: 5px;
}

View File

@@ -2,7 +2,7 @@
position: absolute;
top: 0;
left: 0;
z-index: 1015;
z-index: $popoverZ;
display: none;
max-width: 300px;
min-width: 100px;

View File

@@ -10,7 +10,7 @@
position: absolute;
top: 0;
width: 0;
z-index: 800;
z-index: $sideToolbarContainerZ;
/**
* Labels inside the side panel.

View File

@@ -33,7 +33,7 @@
#subject {
position: relative;
z-index: 3;
z-index: $zindex3;
width: auto;
padding: 5px;
margin-left: 40%;
@@ -106,7 +106,7 @@
height: 50px;
cursor: pointer;
text-align: center;
z-index: 1;
z-index: $zindex1;
font-size: $toolbarFontSize !important;
line-height: 50px !important;
vertical-align: middle;

View File

@@ -104,13 +104,26 @@ $happySoftwareBackground: transparent;
/**
* Z-indexes. TODO: Replace this by a function.
*/
$tooltipsZ: 901;
$toolbarZ: 900;
$overlayZ: 902;
$notificationZ: 1012;
$ringingZ: 800;
$dropdownZ: 901;
$zindex0: 0;
$zindex1: 1;
$zindex2: 2;
$zindex3: 3;
$filmstripVideosZ: 5;
$zindex10: 10;
$reloadZ: 20;
$poweredByZ: 100;
$ringingZ: 300;
$sideToolbarContainerZ: 300;
$toolbarZ: 400;
$tooltipsZ: 401;
$dropdownMaskZ: 900;
$dropdownZ: 901;
$overlayZ: 902;
$jitsipopoverZ: 1010;
$centeredVideoLabelZ: 1011;
$notificationZ: 1012;
$popoverZ: 1015;
/**
* Font Colors

View File

@@ -31,7 +31,7 @@
&__toptoolbar {
position: absolute;
left: 0;
z-index: 3;
z-index: $zindex3;
width: 100%;
box-sizing: border-box; // Includes the padding in the 100% width.
}
@@ -59,7 +59,7 @@
float: left;
@include circle($thumbnailIndicatorSize);
box-sizing: border-box;
z-index: 3;
z-index: $zindex3;
background: $dominantSpeakerBg;
color: $thumbnailPictogramColor;
border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor;
@@ -113,7 +113,7 @@
width: 100%;
height: 100%;
visibility: hidden;
z-index: 2;
z-index: $zindex2;
}
}
@@ -161,7 +161,7 @@
position: absolute;
left: 0;
top: 0;
z-index: 1;
z-index: $zindex1;
width: 100%;
height: 100%;
}
@@ -171,7 +171,7 @@
}
#etherpad {
z-index: 0;
z-index: $zindex0;
}
/**
@@ -193,7 +193,7 @@
overflow: hidden;
white-space: nowrap;
line-height: $thumbnailToolbarHeight;
z-index: 2;
z-index: $zindex2;
}
/**
@@ -233,7 +233,7 @@
padding: 3px 5px;
font-size: 9pt;
cursor: pointer;
z-index: 2;
z-index: $zindex2;
}
/**
@@ -283,7 +283,7 @@
top: 0px;
right: 0;
margin: 7px;
z-index: 3;
z-index: $zindex3;
width: 18px;
height: 13px;
color: #FFF;
@@ -301,7 +301,7 @@
margin-top: -17px;
width: 6px;
height: 35px;
z-index: 2;
z-index: $zindex2;
border: none;
.audiodot-top,
@@ -344,13 +344,13 @@
background-clip: padding-box;
-webkit-border-radius: 5px;
-webkit-background-clip: padding-box;
z-index: 20; /*The reload button should appear on top of the header!*/
z-index: $reloadZ; /*The reload button should appear on top of the header!*/
}
.audiolevel {
display: inline-block;
position: absolute;
z-index: 0;
z-index: $zindex0;
border-radius:1px;
pointer-events: none;
}
@@ -408,7 +408,7 @@
.noMic {
position: absolute;
border-radius: 8px;
z-index: 1;
z-index: $zindex1;
width: 100%;
height: 100%;
background-image: url("../images/noMic.png");
@@ -420,7 +420,7 @@
.noVideo {
position: absolute;
border-radius: 8px;
z-index: 1;
z-index: $zindex1;
width: 100%;
height: 100%;
background-image: url("../images/noVideo.png");
@@ -453,7 +453,7 @@
display: none;
position: absolute;
width: auto;
z-index: 2;
z-index: $zindex2;
font-weight: 600;
font-size: 14px;
text-align: center;
@@ -477,7 +477,7 @@
left: 0;
width: 100%;
top:50%;
z-index: 2;
z-index: $zindex2;
font-weight: 600;
font-size: 14px;
text-align: center;
@@ -506,7 +506,7 @@
#videoResolutionLabel,
.centeredVideoLabel {
display: none;
z-index: 1011;
z-index: $centeredVideoLabelZ;
}
.centeredVideoLabel {

View File

@@ -22,7 +22,7 @@
font-weight: 500;
font-size: 16px;
color: #acacac;
z-index: 2;
z-index: $zindex2;
}
#disable_welcome:checked + label
@@ -35,7 +35,7 @@
font-weight: 500;
font-size: 16px;
color: #acacac;
z-index: 2;
z-index: $zindex2;
}
#enter_room_form {
@@ -74,7 +74,7 @@
float: left;
background-color: #FFFFFF;
position: relative;
z-index: 2;
z-index: $zindex2;
}
&__reload {
@@ -83,7 +83,7 @@
color: #acacac;
font-size: 1.9em;
line-height: 55px;
z-index: 3;
z-index: $zindex3;
float: left;
cursor: pointer;
text-align: center;
@@ -104,7 +104,7 @@
outline: none;
float:left;
position: relative;
z-index: 2;
z-index: $zindex2;
}
}

View File

@@ -57,6 +57,18 @@
}
}
&_overlay {
color: $primaryButtonColor;
background-color: $overlayButtonBg;
border-radius: 2px;
border: none;
&:hover {
background-color: $primaryButtonBackground;
border: none;
}
}
&_primary {
background-color: $primaryButtonBackground;
border: 1px solid $primaryButtonBackground;
@@ -86,4 +98,4 @@
&_center {
float: none !important;
}
}
}

View File

@@ -37,8 +37,10 @@
@import 'overlay/overlay';
@import 'inlay';
@import 'reload_overlay/reload_overlay';
@import 'modals/desktop-picker/desktop-picker';
@import 'modals/dialog';
@import 'modals/feedback/feedback';
@import 'modals/speaker_stats/speaker_stats';
@import 'videolayout_default';
@import 'notice';
@import 'popup_menu';

View File

@@ -0,0 +1,59 @@
.desktop-picker-pane {
height: 320px;
overflow-x: hidden;
overflow-y: auto;
width: 100%;
&.source-type-screen {
.desktop-picker-source {
margin-left: auto;
margin-right: auto;
width: 50%;
}
.desktop-source-preview-thumbnail {
width: 100%;
}
.desktop-source-preview-label {
display: none;
}
}
&.source-type-window {
.desktop-picker-source {
display: inline-block;
width: 30%;
}
}
}
.desktop-picker-source {
color: $defaultDarkFontColor;
margin-top: 10px;
text-align: center;
&.is-selected {
.desktop-source-preview-image-container {
background: rgba(0, 0, 0, 0.1);
border-radius: $borderRadius;
}
}
}
.desktop-source-preview-label {
margin-top: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.desktop-source-preview-thumbnail {
box-shadow: 5px 5px 5px grey;
height: auto;
max-width: 100%;
}
.desktop-source-preview-image-container {
padding: 10px;
}

View File

@@ -0,0 +1,56 @@
.speaker-stats {
list-style: none;
padding: 0;
color: $auiDialogColor;
width: 100%;
font-weight: 500;
.speaker-stats-item__status-dot {
position: relative;
display: block;
width: 9px;
height: 9px;
border-radius: 50%;
margin: 0 auto;
&.status-active {
background: green;
}
&.status-inactive {
background: gray;
}
}
.status-user-left {
color: $placeHolderColor;
}
.speaker-stats-item__status,
.speaker-stats-item__name,
.speaker-stats-item__time {
display: inline-block;
margin: 5px 0;
vertical-align: middle;
}
.speaker-stats-item__status {
width: 5%;
}
.speaker-stats-item__name {
width: 40%;
}
.speaker-stats-item__time {
width: 55%;
}
.speaker-stats-item:nth-child(even) {
background: whitesmoke;
}
.speaker-stats-item__name,
.speaker-stats-item__time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@@ -8,10 +8,16 @@
position: fixed;
z-index: $overlayZ;
background: $defaultBackground;
&.filmstrip-only {
@include transparentBg($filmStripOnlyOverlayBg, 0.8);
}
}
&__container-light {
@include transparentBg($defaultBackground, 0.7);
&.filmstrip-only {
@include transparentBg($filmStripOnlyOverlayBg, 0.2);
}
}
&__content {
@@ -21,6 +27,11 @@
width: 56%;
left: 50%;
@include transform(translateX(-50%));
&.filmstrip-only {
left: 0px;
width: 100%;
@include transform(none);
}
&_bottom {
position: absolute;
@@ -33,4 +44,4 @@
bottom: 24px;
width: 100%;
}
}
}

View File

@@ -13,4 +13,7 @@
#reloadProgressBar {
width: 180px;
margin: 5px auto;
> .aui-progress-indicator-value {
background: $reloadProgressBarBg;
}
}

View File

@@ -35,10 +35,14 @@ $primaryButtonFontWeight: 400;
$buttonShadowColor: #192d4f;
$overlayButtonBg: #0074E0;
/**
* Color variables
**/
$defaultBackground: #474747;
$filmStripOnlyOverlayBg: #000;
$reloadProgressBarBg: #0074E0;
/**
* Connection indicator
@@ -60,6 +64,10 @@ $dialogTitleFontWeight: 400;
**/
$inlayColorBg: lighten($defaultBackground, 20%);
$inlayBorderColor: lighten($auiDialogContentBg, 10%);
$inlayIconBg: #000;
$inlayIconColor: #fff;
$inlayFilmstripOnlyColor: #474747;
$inlayFilmstripOnlyBg: #fff;
// Main controls
$inputBackground: $controlBackground;

View File

@@ -65,7 +65,7 @@ case "$1" in
# SSL for nginx
db_get jitsi-meet/cert-choice
CERT_CHOICE="$RET"
UPLOADED_CERT_CHOICE="A certificate is available and the files are uploaded on the server"
UPLOADED_CERT_CHOICE="I want to use my own certificate"
if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
db_set jitsi-meet/cert-path-key "/etc/ssl/$JVB_HOSTNAME.key"
@@ -223,6 +223,13 @@ case "$1" in
invoke-rc.d apache2 reload
fi
echo "----------------"
echo ""
echo "You can now switch to a Lets Encrypt certificate. To do so, execute:"
echo "/usr/share/jitsi-meet/scripts/install-letsencrypt-cert.sh"
echo ""
echo "----------------"
# and we're done with debconf
db_stop
;;

View File

@@ -1,9 +1,10 @@
Template: jitsi-meet/cert-choice
Type: select
__Choices: Self-signed certificate will be generated, A certificate is available and the files are uploaded on the server
__Choices: Generate a new self-signed certificate (You will later get a chance to obtain a Let's encrypt certificate), I want to use my own certificate
_Description: SSL certificate for the Jitsi Meet instance
Jitsi Meet is best to be set up with an SSL certificate.
Having no certificate, a self-signed one will be generated.
By choosing self-signed you will later have a chance to install Lets Encrypt certificates.
Having a certificate signed by a recognised CA, it can be uploaded on the server
and point its location. The default filenames will be /etc/ssl/--domain.name--.key
for the key and /etc/ssl/--domain.name--.crt for the certificate.

View File

@@ -1,11 +1,14 @@
*.js /usr/share/jitsi-meet/
interface_config.js /usr/share/jitsi-meet/
logging_config.js /usr/share/jitsi-meet/
*.json /usr/share/jitsi-meet/
*.html /usr/share/jitsi-meet/
*.ico /usr/share/jitsi-meet/
libs /usr/share/jitsi-meet/
static /usr/share/jitsi-meet/
css/all.css /usr/share/jitsi-meet/css/
sounds /usr/share/jitsi-meet/
fonts /usr/share/jitsi-meet/
images /usr/share/jitsi-meet/
lang /usr/share/jitsi-meet/
connection_optimization /usr/share/jitsi-meet/
resources/*.sh /usr/share/jitsi-meet/scripts/

View File

@@ -20,13 +20,13 @@ msgstr ""
#. Type: select
#. Choices
#: ../jitsi-meet-web-config.templates:1001
msgid "Self-signed certificate will be generated"
msgid "Generate a new self-signed certificate (You will later get a chance to obtain a Let's encrypt certificate)"
msgstr ""
#. Type: select
#. Choices
#: ../jitsi-meet-web-config.templates:1001
msgid "A certificate is available and the files are uploaded on the server"
msgid "I want to use my own certificate"
msgstr ""
#. Type: select

View File

@@ -10,37 +10,56 @@ To embed Jitsi Meet in your application you need to add the Jitsi Meet API libra
<script src="https://meet.jit.si/external_api.js"></script>
```
The next step for embedding Jitsi Meet is to create the Jitsi Meet API object:
```javascript
<script>
var domain = "meet.jit.si";
var room = "JitsiMeetAPIExample";
var width = 700;
var height = 700;
var api = new JitsiMeetExternalAPI(domain, room, width, height);
</script>
```
You can use the above lines to indicate where exactly you want the Jitsi Meet conference to be placed in your HTML code,
or you can specify the parent HTML element for the Jitsi Meet conference in the `JitsiMeetExternalAPI`
constructor:
## API
### `api = new JitsiMeetExternalAPI(domain, room, [width], [height], [htmlElement], [configOverwite], [interfaceConfigOverwrite], [noSsl], [jwt])`
The next step for embedding Jitsi Meet is to create the Jitsi Meet API object.
Its constructor gets a number of options:
* **domain**: domain used to build the conference URL, "meet.jit.si" for
example.
* **room**: name of the room to join.
* **width**: (optional) width for the iframe which will be created.
* **height**: (optional) height for the iframe which will be created.
* **htmlElement**: (optional) HTL DOM Element where the iframe will be added as
a child.
* **configOverwite**: (optional) JS object with overrides for options defined in
[config.js].
* **interfaceConfigOverwrite**: (optional) JS object with overrides for options
defined in [interface_config.js].
* **noSsl**: (optional, defaults to true) Boolean indicating if the server
should be contacted using HTTP or HTTPS.
* **jwt**: (optional) [JWT](https://jwt.io/) token.
Example:
```javascript
var domain = "meet.jit.si";
var room = "JitsiMeetAPIExample";
var width = 700;
var height = 700;
var htmlElement = document.querySelector('#meet');
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement);
```
If you don't specify the room the user will enter in new conference with a random room name.
You can overwrite options set in [config.js]() and [interface_config.js](). For example, to enable the film-strip-only interface mode and disable simulcast, you can use:
You can overwrite options set in [config.js] and [interface_config.js].
For example, to enable the film-strip-only interface mode, you can use:
```javascript
var configOverwrite = {disableSimulcast: true};
var interfaceConfigOverwrite = {filmStripOnly: true};
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement, configOverwrite, interfaceConfigOverwrite);
var api = new JitsiMeetExternalAPI(domain, room, width, height, undefined, undefined, interfaceConfigOverwrite);
```
## Controlling the embedded Jitsi Meet Conference
You can also pass a jwt token to Jitsi Meet:
```javascript
var jwt = "<jwt_token>";
var noSsl = false;
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement, configOverwrite, interfaceConfigOverwrite, noSsl, jwt);
```
### Controlling the embedded Jitsi Meet Conference
You can control the embedded Jitsi Meet conference using the `JitsiMeetExternalAPI` object by using `executeCommand`:

View File

@@ -20,7 +20,7 @@ server {
root /usr/share/jitsi-meet;
index index.html index.htm;
error_page 404 /404.html;
error_page 404 /static/404.html;
location /config.js {
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;

View File

@@ -29,6 +29,8 @@
Allow from all
</Directory>
ErrorDocument 404 /static/404.html
Alias "/config.js" "/etc/jitsi/meet/jitsi-meet.example.com-config.js"
<Location /config.js>
Require all granted

View File

@@ -6,6 +6,34 @@ change references to that to match your host, and generate some passwords for
There are also some complete [example config files](https://github.com/jitsi/jitsi-meet/tree/master/doc/example-config-files/) available, mentioned in each section.
## Network description
This how the network look like:
```
+ +
| |
| |
v |
443 |
+-------+ |
| | |
| NginX | |
| | |
+--+-+--+ |
| | |
+------------+ | | +--------------+ |
| | | | | | |
| jitsi-meet +<---+ +--->+ prosody/xmpp | |
| |files 5280 | | |
+------------+ +--------------+ v
5222,5347^ ^5347 4443
+--------+ | | +-------------+
| | | | | |
| jicofo +----^ ^----+ videobridge |
| | | |
+--------+ +-------------+
```
## Install prosody
```sh
apt-get install prosody
@@ -76,7 +104,9 @@ Add a new file `jitsi.example.com` in `/etc/nginx/sites-available` (see also the
server_names_hash_bucket_size 64;
server {
listen 80;
listen 443;
# tls configuration that is not covered in this guide
# we recommend the use of https://certbot.eff.org/
server_name jitsi.example.com;
# set the root
root /srv/jitsi.example.com;

View File

@@ -127,7 +127,7 @@
'error', loadErrHandler, true /* capture phase type of listener */);
</script>
<script><!--#include virtual="/config.js" --></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="utils.js?v=1"></script>
<script src="static/utils.js?v=1"></script>
<!--#include virtual="connection_optimization/connection_optimization.html" -->
<script src="connection_optimization/do_external_connect.js?v=1"></script>
<script><!--#include virtual="/interface_config.js" --></script>

19
lang/languages-eo.json Normal file
View File

@@ -0,0 +1,19 @@
{
"en": "",
"bg": "",
"de": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": "",
"zhCN": "",
"nb": ""
}

View File

@@ -13,5 +13,7 @@
"sk": "Eslovac",
"sl": "Eslovèn",
"sv": "Suedés",
"tr": "Turc"
"tr": "Turc",
"zhCN": "Chinés (China)",
"nb": "Norvegian Bokmål"
}

View File

@@ -6,12 +6,14 @@
"fr": "Francês",
"hy": "Armênio",
"it": "Italiano",
"oc": "Provençal",
"oc": "Occitano",
"pl": "Polonês",
"ptBR": "Português (Brasil)",
"ru": "Russo",
"sk": "Eslovaco",
"sl": "Esloveno",
"sv": "Sueco",
"tr": "Turco"
"tr": "Turco",
"zhCN": "Chinês (China)",
"nb": "Bokmal norueguês"
}

View File

@@ -1,17 +1,20 @@
{
"en": "",
"bg": "",
"de": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": ""
"en": "英语",
"bg": "保加利亚语",
"de": "德语",
"es": "西班牙语",
"fr": "法语",
"hy": "亚美尼亚语",
"it": "意大利语",
"oc": "欧西坦语",
"pl": "波兰语",
"ptBR": "葡萄牙语(巴西)",
"ru": "俄语",
"sk": "斯洛伐克语",
"sl": "斯洛文尼亚语",
"sv": "瑞典语",
"tr": "土耳其语",
"zhCN": "中文(中国)",
"nb": "",
"eo": ""
}

View File

@@ -15,5 +15,6 @@
"sv": "Swedish",
"tr": "Turkish",
"zhCN": "Chinese (China)",
"nb": "Norwegian Bokmal"
"nb": "Norwegian Bokmal",
"eo": "Esperanto"
}

374
lang/main-eo.json Normal file
View File

@@ -0,0 +1,374 @@
{
"contactlist": "",
"addParticipants": "",
"roomLocked": "",
"roomUnlocked": "",
"passwordSetRemotely": "",
"connectionsettings": "",
"poweredby": "",
"feedback": "",
"inviteUrlDefaultMsg": "",
"me": "",
"speaker": "",
"raisedHand": "",
"defaultNickname": "",
"defaultLink": "",
"callingName": "",
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"firefoxGrantPermissions": "",
"operaGrantPermissions": "",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "",
"nwjsGrantPermissions": ""
},
"keyboardShortcuts": {
"keyboardShortcuts": "",
"raiseHand": "",
"pushToTalk": "",
"toggleScreensharing": "",
"toggleFilmstrip": "",
"toggleShortcuts": "",
"focusLocal": "",
"focusRemote": "",
"toggleChat": "",
"mute": "",
"fullScreen": "",
"videoMute": ""
},
"welcomepage": {
"disable": "",
"feature1": {
"content": "",
"title": ""
},
"feature2": {
"content": "",
"title": ""
},
"feature3": {
"content": "",
"title": ""
},
"feature4": {
"content": "",
"title": ""
},
"feature5": {
"content": "",
"title": ""
},
"feature6": {
"content": "",
"title": ""
},
"feature7": {
"content": "",
"title": ""
},
"feature8": {
"content": "",
"title": ""
},
"go": "",
"join": "",
"privacy": "",
"roomname": "",
"roomnamePlaceHolder": "",
"sendFeedback": "",
"terms": ""
},
"startupoverlay": {
"policyText": "",
"title": ""
},
"suspendedoverlay": {
"title": "",
"rejoinKeyTitle": ""
},
"toolbar": {
"mute": "",
"videomute": "",
"authenticate": "",
"lock": "",
"invite": "",
"chat": "",
"etherpad": "",
"sharedvideo": "",
"sharescreen": "",
"fullscreen": "",
"sip": "",
"Settings": "",
"hangup": "",
"login": "",
"logout": "",
"dialpad": "",
"sharedVideoMutedPopup": "",
"micMutedPopup": "",
"talkWhileMutedPopup": "",
"unableToUnmutePopup": "",
"cameraDisabled": "",
"micDisabled": "",
"filmstrip": "",
"profile": "",
"raiseHand": ""
},
"unsupportedBrowser": {
"appInstalled": "",
"appNotInstalled": "",
"downloadApp": "",
"joinConversation": "",
"startConference": ""
},
"bottomtoolbar": {
"chat": "",
"filmstrip": "",
"contactlist": ""
},
"chat": {
"nickname": {
"title": "",
"popover": ""
},
"messagebox": ""
},
"settings": {
"title": "",
"update": "",
"name": "",
"startAudioMuted": "",
"startVideoMuted": "",
"selectCamera": "",
"selectMic": "",
"selectAudioOutput": "",
"followMe": "",
"noDevice": "",
"noPermission": "",
"cameraAndMic": "",
"moderator": "",
"password": "",
"audioVideo": "",
"setPasswordLabel": ""
},
"profile": {
"title": "",
"setDisplayNameLabel": "",
"setEmailLabel": "",
"setEmailInput": ""
},
"videothumbnail": {
"editnickname": "",
"moderator": "",
"videomute": "",
"mute": "",
"kick": "",
"muted": "",
"domute": "",
"flip": "",
"remoteControl": ""
},
"connectionindicator": {
"header": "",
"bitrate": "",
"packetloss": "",
"resolution": "",
"less": "",
"more": "",
"address": "",
"remoteport": "",
"remoteport_plural": "",
"localport": "",
"localport_plural": "",
"localaddress": "",
"localaddress_plural": "",
"remoteaddress": "",
"remoteaddress_plural": "",
"transport": "",
"bandwidth": "",
"na": ""
},
"notify": {
"disconnected": "",
"moderator": "",
"connected": "",
"somebody": "",
"me": "",
"focus": "",
"focusFail": "",
"grantedTo": "",
"grantedToUnknown": "",
"muted": "",
"mutedTitle": "",
"raisedHand": ""
},
"dialog": {
"add": "",
"kickMessage": "",
"popupError": "",
"passwordErrorTitle": "",
"passwordError": "",
"passwordError2": "",
"connectError": "",
"connectErrorWithMsg": "",
"incorrectPassword": "",
"connecting": "",
"copy": "",
"error": "",
"roomLocked": "",
"addPassword": "",
"createPassword": "",
"detectext": "",
"failtoinstall": "",
"failedpermissions": "",
"conferenceReloadTitle": "",
"conferenceReloadMsg": "",
"conferenceDisconnectTitle": "",
"conferenceDisconnectMsg": "",
"reconnectNow": "",
"conferenceReloadTimeLeft": "",
"maxUsersLimitReached": "",
"lockTitle": "",
"lockMessage": "",
"warning": "",
"passwordNotSupported": "",
"internalErrorTitle": "",
"internalError": "",
"unableToSwitch": "",
"SLDFailure": "",
"SRDFailure": "",
"oops": "",
"currentPassword": "",
"passwordLabel": "",
"defaultError": "",
"passwordRequired": "",
"Ok": "",
"done": "",
"Remove": "",
"removePassword": "",
"shareVideoTitle": "",
"shareVideoLinkError": "",
"removeSharedVideoTitle": "",
"removeSharedVideoMsg": "",
"alreadySharedVideoMsg": "",
"WaitingForHost": "",
"WaitForHostMsg": "",
"IamHost": "",
"Cancel": "",
"Submit": "",
"retry": "",
"logoutTitle": "",
"logoutQuestion": "",
"sessTerminated": "",
"hungUp": "",
"joinAgain": "",
"Share": "",
"Save": "",
"recording": "",
"recordingToken": "",
"Dial": "",
"sipMsg": "",
"passwordCheck": "",
"passwordMsg": "",
"shareLink": "",
"settings1": "",
"settings2": "",
"settings3": "",
"yourPassword": "",
"Back": "",
"serviceUnavailable": "",
"gracefulShutdown": "",
"Yes": "",
"reservationError": "",
"reservationErrorMsg": "",
"password": "",
"userPassword": "",
"token": "",
"tokenAuthFailedTitle": "",
"tokenAuthFailed": "",
"displayNameRequired": "",
"enterDisplayName": "",
"extensionRequired": "",
"firefoxExtensionPrompt": "",
"rateExperience": "",
"feedbackHelp": "",
"feedbackQuestion": "",
"thankYou": "",
"sorryFeedback": "",
"liveStreaming": "",
"streamKey": "",
"startLiveStreaming": "",
"stopStreamingWarning": "",
"stopRecordingWarning": "",
"stopLiveStreaming": "",
"stopRecording": "",
"doNotShowWarningAgain": "",
"doNotShowMessageAgain": "",
"permissionDenied": "",
"screenSharingPermissionDeniedError": "",
"micErrorPresent": "",
"cameraErrorPresent": "",
"cameraUnsupportedResolutionError": "",
"cameraUnknownError": "",
"cameraPermissionDeniedError": "",
"cameraNotFoundError": "",
"cameraConstraintFailedError": "",
"micUnknownError": "",
"micPermissionDeniedError": "",
"micNotFoundError": "",
"micConstraintFailedError": "",
"micNotSendingData": "",
"cameraNotSendingData": "",
"goToStore": "",
"externalInstallationTitle": "",
"externalInstallationMsg": "",
"muteParticipantTitle": "",
"muteParticipantBody": "",
"muteParticipantButton": "",
"remoteControlTitle": "",
"remoteControlDeniedMessage": "",
"remoteControlAllowedMessage": "",
"remoteControlErrorMessage": "",
"remoteControlStopMessage": ""
},
"email": {
"sharedKey": "",
"subject": "",
"body": "",
"and": ""
},
"connection": {
"ERROR": "",
"CONNECTING": "",
"RECONNECTING": "",
"CONNFAIL": "",
"AUTHENTICATING": "",
"AUTHFAIL": "",
"CONNECTED": "",
"DISCONNECTED": "",
"DISCONNECTING": "",
"ATTACHED": ""
},
"recording": {
"pending": "",
"on": "",
"off": "",
"failedToStart": "",
"buttonTooltip": "",
"error": "",
"unavailable": ""
},
"liveStreaming": {
"pending": "",
"on": "",
"off": "",
"unavailable": "",
"failedToStart": "",
"buttonTooltip": "",
"streamIdRequired": "",
"streamIdHelp": "",
"error": "",
"busy": ""
}
}

View File

@@ -5,12 +5,12 @@
"roomUnlocked": "Qual que siá amb lo ligam pòt participar",
"passwordSetRemotely": "causit per qualqu'un mai",
"connectionsettings": "Paramètres de connexion",
"poweredby": "Produit per",
"feedback": "Donatz-nos vòstre vejaire",
"poweredby": "produit per",
"feedback": "Donatz-nos lo vòstre vejaire",
"inviteUrlDefaultMsg": "Vòstra conferéncia es en cors de creacion...",
"me": "ieu",
"speaker": "Nautparlaire",
"raisedHand": "Volriatz charrar",
"raisedHand": "Volriá charrar",
"defaultNickname": "ex. Joan Delpuèch",
"defaultLink": "ex. __url__",
"callingName": "__name__",
@@ -39,41 +39,46 @@
"videoMute": "Aviar o arrestar vòstra camerà"
},
"welcomepage": {
"go": "Crear",
"roomname": "Sasissètz un nom de sala",
"disable": "Afichar pas mai aquesta pagina",
"feature1": {
"title": "De bon utilizar",
"content": "Cap de telecargament pas requesit. __app__ s'utiliza dirèctament dempuèi vòstre navigador. Partejar simplament l'URL de vòstra conferéncia amb los autres per començar."
"content": "Cap de telecargament pas requesit. __app__ s'utiliza dirèctament dempuèi vòstre navigador. Partejar simplament l'URL de vòstra conferéncia amb los autres per començar.",
"title": "De bon utilizar"
},
"feature2": {
"title": "Benda passanta febla",
"content": "Las videoconferéncias de mantun participant necessitan mens de 128 kbps. Lo partiment d'ecran e las conferéncias amb solament d'àudio son possiblas amb plan mens de debit."
"content": "Las videoconferéncias de mantun participant necessitan mens de 128 kbps. Lo partiment d'ecran e las conferéncias amb solament d'àudio son possiblas amb plan mens de debit.",
"title": "Benda passanta febla"
},
"feature3": {
"title": "Open source",
"content": "__app__ es jos licéncia Apache. Sètz liure de telecargar, d'utilizar, de modificar e de partejar __app__ segon aquesta licéncia liura."
"content": "__app__ es jos licéncia Apache. Sètz liure de telecargar, d'utilizar, de modificar e de partejar __app__ segon aquesta licéncia liura.",
"title": "Open source"
},
"feature4": {
"title": "Nombre d'utilizaires illimitat",
"content": "I a pas de restriccions artificialas a prepaus del nombre d'utilizaires o de participants a una conferéncia. La poténcia del servidor e la benda passanta son los sols factors limitants."
"content": "I a pas de restriccions artificialas a prepaus del nombre d'utilizaires o de participants a una conferéncia. La poténcia del servidor e la benda passanta son los sols factors limitants.",
"title": "Nombre d'utilizaires illimitat"
},
"feature5": {
"title": "Partiment d'ecran",
"content": "Es aisit de partejar vòstre ecran amb d'autras personas. __app__ es ideal per las presentacions en linha, los corses, e las sessions de supòrt tecnic."
"content": "Es aisit de partejar vòstre ecran amb d'autras personas. __app__ es ideal per las presentacions en linha, los corses, e las sessions de supòrt tecnic.",
"title": "Partiment d'ecran"
},
"feature6": {
"title": "Salas securizadas",
"content": "Besonh de confidencialitat ? Las salas de conferéncia __app__ pòdon èsser securizadas per un senhal per exclure los convidats pas desirats, e prevenir de las interrupcions."
"content": "Besonh de confidencialitat ? Las salas de conferéncia __app__ pòdon èsser securizadas per un senhal per exclure los convidats pas desirats, e prevenir de las interrupcions.",
"title": "Salas securizadas"
},
"feature7": {
"title": "Nòtas partejadas",
"content": "__app__ prepausa Etherpad, un editor de tèxte collaboratiu en temps real qu'es parfèit pels procèsses verbals, l'edicion d'articles e plan mai encara."
"content": "__app__ prepausa Etherpad, un editor de tèxte collaboratiu en temps real qu'es parfèit pels procèsses verbals, l'edicion d'articles e plan mai encara.",
"title": "Nòtas partejadas"
},
"feature8": {
"title": "Estatisticas d'utilizacion",
"content": "Aprenètz mai a prepaus de vòstres utilizaires amb una integracion aisida de Piwik, Google Analytics e d'autres sistèmas d'estatisticas e supervision d'utilizacion."
}
"content": "Aprenètz mai a prepaus de vòstres utilizaires amb una integracion aisida de Piwik, Google Analytics e d'autres sistèmas d'estatisticas e supervision d'utilizacion.",
"title": "Estatisticas d'utilizacion"
},
"go": "Crear",
"join": "PARTICIPATZ",
"privacy": "Vida privada",
"roomname": "Sasissètz un nom de sala",
"roomnamePlaceHolder": "nom de la sala",
"sendFeedback": "Mandar vòstra opinion",
"terms": "Tèrmes"
},
"startupoverlay": {
"policyText": "&nbsp;",
@@ -108,7 +113,14 @@
"micDisabled": "Lo microfòn es pas disponible",
"filmstrip": "Mostrar / escondre vidèos",
"profile": "Modificar vòstre perfil",
"raiseHand": "Demandar / Demandar pas mai la paraula"
"raiseHand": "o se l'avètz ja<br /><strong>alara</strong>"
},
"unsupportedBrowser": {
"appInstalled": "o se ja l'avètz<br /><strong>alara</strong>",
"appNotInstalled": "Vos cal <strong>__app__</strong> per participar a la conversacion a partir de vòstre mobil",
"downloadApp": "Telecargar l'aplicacion",
"joinConversation": "Participar a la conversacion",
"startConference": "Començar una conferéncia"
},
"bottomtoolbar": {
"chat": "Dobrir / tampar lo chat",
@@ -126,15 +138,15 @@
"title": "Paramètres",
"update": "Mesa a jorn",
"name": "Nom",
"startAudioMuted": "Tot lo mond comença sens son",
"startVideoMuted": "Tot lo mond comença escondut",
"selectCamera": "Camèra",
"startAudioMuted": "Començan totes sens son",
"startVideoMuted": "Començan totes sens vidèo",
"selectCamera": "Camerà",
"selectMic": "Microfòn",
"selectAudioOutput": "Sortida àudio",
"followMe": "Tot lo mond me sèc",
"noDevice": "Pas cap",
"noPermission": "La permission d'utilizar l'aparelh es pas estada donada",
"cameraAndMic": "Camèra e microfòn",
"cameraAndMic": "Camerà e microfòn",
"moderator": "MODERATOR",
"password": "DEFINIR UN SENHAL",
"audioVideo": "ÀUDIO E VIDÈO",
@@ -149,12 +161,13 @@
"videothumbnail": {
"editnickname": "Clicatz per modificar<br/>vòstre nom",
"moderator": "Lo proprietari de<br/>aquesta conferéncia",
"videomute": "Un participant a<br/>arrestat sa camèra.",
"videomute": "Un participant a<br/>arrestat sa camerà.",
"mute": "Un participant a copat son micro",
"kick": "Exclure",
"muted": "Mut",
"domute": "Copar lo son",
"flip": "Revirar"
"flip": "Revirar",
"remoteControl": "Contraròtle alonhat"
},
"connectionindicator": {
"header": "Donadas de connexion",
@@ -162,7 +175,7 @@
"packetloss": "Pèrda de paquets :",
"resolution": "Resolucion :",
"less": "Amagar lo detalh",
"more": "Mostrar lo deta1h",
"more": "Ne veire mai",
"address": "Adreça :",
"remoteport": "Pòrt distant :",
"remoteport_plural": "Pòrts distants :",
@@ -208,7 +221,7 @@
"createPassword": "Crear un senhal",
"detectext": "Una error s'es produita pendent la deteccion de l'extension de partiment d'ecran.",
"failtoinstall": "Fracàs de l'installacion de l'extension de partiment d'ecran",
"failedpermissions": "Fracàs d'obtencion de las permissions per utilizar lo micro e/o la camèra.",
"failedpermissions": "Fracàs d'obtencion de las permissions per utilizar lo micro e/o la camerà.",
"conferenceReloadTitle": "Malurosament, quicòm truquèt",
"conferenceReloadMsg": "Sèm a trobar una solucion per aquò",
"conferenceDisconnectTitle": "Sètz estat desconnectat. Vos cal benlèu verificar vòstra connexion.",
@@ -224,7 +237,7 @@
"internalError": "Ops ! Quicòm a pas fonccionat. L'error seguenta s'es produsida : [setRemoteDescription]",
"unableToSwitch": "Impossible de cambiar lo flux vidèo.",
"SLDFailure": "Ops! Quicòm a trucat e lo micro es pas estat copat! (Fracàs SLD)",
"SRDFailure": "Ops! Quicòm a trucat e la camèra es pas estada copada! (Fracàs SRD)",
"SRDFailure": "Ops ! Quicòm a trucat e la camerà es pas estada arrestada ! (Fracàs SRD)",
"oops": "Ops !",
"currentPassword": "L'actual senhal es",
"passwordLabel": "Senhal",
@@ -298,21 +311,26 @@
"cameraErrorPresent": "I a agut una error pendent la connexion a la camerà.",
"cameraUnsupportedResolutionError": "Vòstra camerà pren pas en carga la resolucion vidèo que cal.",
"cameraUnknownError": "Impossible d'emplegar la camerà per una rason desconeguda.",
"cameraPermissionDeniedError": "La camèra es pas estada trobada.",
"cameraNotFoundError": "La camèra es pas estada trobada.",
"cameraPermissionDeniedError": "La camerà es pas estada trobada.",
"cameraNotFoundError": "La camerà es pas estada trobada.",
"cameraConstraintFailedError": "Vòstra camerà satisfà pas totas las constrentas necessàrias.",
"micUnknownError": "Impossible d'utilizar lo microfòn per una rason desconeguda.",
"micPermissionDeniedError": "Avètz pas donat l'autorizacion d'utilizar vòstre microfòn. Podètz encara participar a la conferéncia mai los demai vos ausiràn pas. Utilizatz lo boton del microfòn dins la barra d'adreça per resòlvre aquò.",
"micNotFoundError": "Lo microfòn es pas estat trobat.",
"micConstraintFailedError": "Vòstre microfòn satisfà pas totas las constrentas necessàrias.",
"micNotSendingData": "Podèm pas aver l'accès a vòstre microfòn. Mercés de ne causir un autre dins lo menú de paramètres o ensajatz de tornar dubrir l'aplicacion.",
"cameraNotSendingData": "Podèm pas aver l'accès a vòstra camèra. Mercés de verificar se una autra aplicacion es pas a l'utilizar, causissètz una autra camèra dins lo menú de paramètres o ensajatz de tornar dubrir l'aplicacion.",
"cameraNotSendingData": "Podèm pas aver l'accès a vòstra camerà. Mercés de verificar se una autra aplicacion es pas a l'utilizar, causissètz una autra camerà dins lo menú de paramètres o ensajatz de tornar dubrir l'aplicacion.",
"goToStore": "Anar sul webstore",
"externalInstallationTitle": "Extension requesida :",
"externalInstallationMsg": "Avètz d'installar nòstra extension de partiment d'ecran.",
"muteParticipantTitle": "Copar lo micro als participants ?",
"muteParticipantBody": "Poiretz pas lo tornar activar lo microfòn, mai eles pòdon o far quand vòlon.",
"muteParticipantButton": "Copar lo son"
"muteParticipantButton": "Copar lo son",
"remoteControlTitle": "Contraròtle alonhat",
"remoteControlDeniedMessage": "__user__ a refusat vòstra demanda de contraròtle alonhat !",
"remoteControlAllowedMessage": "__user__ a acceptat vòstra demanda de contraròtle alonhat !",
"remoteControlErrorMessage": "Error al moment de demandar lo contraròtle alonhat a __user__ !",
"remoteControlStopMessage": "La session de contraròtle alonhat es acabada !"
},
"email": {
"sharedKey": [
@@ -371,6 +389,7 @@
"failedToStart": "Lo dirècte a pas capitat de s'aviar",
"buttonTooltip": "Aviar / arrestar lo dirècte",
"streamIdRequired": "Mercé de completar lo stream id per aviar lo dirècte.",
"streamIdHelp": "Ont tròbi aquò ?",
"error": "Fracàs de la transmission en dirècte. Mercés de tornar ensajar.",
"busy": "Tots los enresgistraires son ocupats. Mercés de tornar ensajar mai tard."
}

View File

@@ -39,41 +39,46 @@
"videoMute": "Iniciar ou parar sua câmera"
},
"welcomepage": {
"go": "IR",
"roomname": "Digite o nome da sala",
"disable": "Não mostre esta página novamente",
"feature1": {
"title": "Simples para usar",
"content": "Não precisa baixar nada. __app__ trabalha diretamente no seu navegador. Simplesmente compartilhe sua URL da conferência com outros para começar."
"content": "Não precisa baixar nada. __app__ trabalha diretamente no seu navegador. Simplesmente compartilhe sua URL da conferência com outros para começar.",
"title": "Simples para usar"
},
"feature2": {
"title": "Largura de banda baixa",
"content": "Conferências de vídeo de multipartes trabalha com tão pouco quanto 128 kbps. Compartilhamento de tela e conferências de áudio somente são possíveis com muito menos."
"content": "Conferências de vídeo de multipartes trabalha com tão pouco quanto 128 kbps. Compartilhamento de tela e conferências de áudio somente são possíveis com muito menos.",
"title": "Largura de banda baixa"
},
"feature3": {
"title": "Código aberto",
"content": "__app__ é licenciado sob a Licença Apache. Você é livre para baixar, usar, modificar e compartilhar ela com a mesma licença."
"content": "__app__ é licenciado sob a Licença Apache. Você é livre para baixar, usar, modificar e compartilhar ela com a mesma licença.",
"title": "Código aberto"
},
"feature4": {
"title": "Usuários ilimitados",
"content": "Aqui não há restrições artificiais no número de usuários ou participantes da conferência. A potência do servidor e largura de banda são os únicos fatores limitantes."
"content": "Aqui não há restrições artificiais no número de usuários ou participantes da conferência. A potência do servidor e largura de banda são os únicos fatores limitantes.",
"title": "Usuários ilimitados"
},
"feature5": {
"title": "Compartilhamento de tela",
"content": "É fácil compartilhar sua tela com outros. __app__ é ideal para apresentações online, leituras, e sessões de suporte técnico."
"content": "É fácil compartilhar sua tela com outros. __app__ é ideal para apresentações online, leituras, e sessões de suporte técnico.",
"title": "Compartilhamento de tela"
},
"feature6": {
"title": "Salas seguras",
"content": "Precisa alguma privacidade? Salas de conferência do __app__ podem ser seguras com uma senha para excluir visitantes indesejados e prevenir interrupções."
"content": "Precisa alguma privacidade? Salas de conferência do __app__ podem ser seguras com uma senha para excluir visitantes indesejados e prevenir interrupções.",
"title": "Salas seguras"
},
"feature7": {
"title": "Notas compartilhadas",
"content": "__app_ disponibiliza o Etherpad, um editor de texto colaborativo em tempo real, que é ótimo para reuniões rápidas, escrevendo artigos, e mais."
"content": "__app_ disponibiliza o Etherpad, um editor de texto colaborativo em tempo real, que é ótimo para reuniões rápidas, escrevendo artigos, e mais.",
"title": "Notas compartilhadas"
},
"feature8": {
"title": "Estatísticas de uso",
"content": "Aprenda sobre seus usuários através de integração fácil com o Piwik, Google Analytics, e outros sistemas de monitoramento e estatísticas."
}
"content": "Aprenda sobre seus usuários através de integração fácil com o Piwik, Google Analytics, e outros sistemas de monitoramento e estatísticas.",
"title": "Estatísticas de uso"
},
"go": "IR",
"join": "Entrar",
"privacy": "Política de Privacidade",
"roomname": "Digite o nome da sala",
"roomnamePlaceHolder": "Nome da sala",
"sendFeedback": "Enviar comentários",
"terms": "Termos"
},
"startupoverlay": {
"policyText": "&nbsp;",
@@ -110,6 +115,13 @@
"profile": "Editar seu perfil",
"raiseHand": "Erguer o baixar sua mão"
},
"unsupportedBrowser": {
"appInstalled": "ou se você já tenha isso<br /> <strong>então</strong>",
"appNotInstalled": "Você precisa do <strong>__app__</strong> para começar um bate-papo no seu celular",
"downloadApp": "Baixe o Aplicativo",
"joinConversation": "Entre no bate-papo",
"startConference": "Comece uma conferência"
},
"bottomtoolbar": {
"chat": "Abrir / fechar bate-papo",
"filmstrip": "Mostrar/ocultar vídeos",
@@ -154,7 +166,8 @@
"kick": "Chutar fora",
"muted": "Mudo",
"domute": "Mudo",
"flip": "Inverter"
"flip": "Inverter",
"remoteControl": "Controle remoto"
},
"connectionindicator": {
"header": "Dados da conexão",
@@ -312,7 +325,12 @@
"externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela.",
"muteParticipantTitle": "Deixar mudo este participante?",
"muteParticipantBody": "Você não está habilitado para tirar o mudo deles, mas eles podem tirar o mudo deles mesmos a qualquer tempo.",
"muteParticipantButton": "Mudo"
"muteParticipantButton": "Mudo",
"remoteControlTitle": "Controle remoto",
"remoteControlDeniedMessage": "__user__ rejeitou sua requisição de controle remoto!",
"remoteControlAllowedMessage": "__user__ aceitou sua requisição de controle remoto!",
"remoteControlErrorMessage": "Um erro ocorreu enquanto tentava requerer a permissão de controle remoto de __user__!",
"remoteControlStopMessage": "A sessão de controle remoto terminou!"
},
"email": {
"sharedKey": [
@@ -371,6 +389,7 @@
"failedToStart": "Falha ao iniciar a transmissão ao vivo",
"buttonTooltip": "Iniciar / parar live stream",
"streamIdRequired": "Por favor digite o id da transmissão ao vivo para começar a transmitir.",
"streamIdHelp": "Aonde eu encontro isto?",
"error": "Falha na transmissão ao vivo. Tente novamente",
"busy": "Todos os gravadores estão ocupados no momento. Por favor, tente mais tarde."
}

View File

@@ -1,355 +1,396 @@
{
"contactlist": "",
"addParticipants": "",
"roomLocked": "",
"roomUnlocked": "",
"passwordSetRemotely": "",
"connectionsettings": "",
"poweredby": "",
"feedback": "",
"inviteUrlDefaultMsg": "",
"me": "",
"speaker": "",
"raisedHand": "",
"defaultNickname": "",
"defaultLink": "",
"callingName": "",
"contactlist": "与会者 (__pcount__)",
"addParticipants": "分享链接",
"roomLocked": "与会者必须输入密码",
"roomUnlocked": "任何人都可以通过此链接参加会议",
"passwordSetRemotely": "由其他与会者设置",
"connectionsettings": "连接设置",
"poweredby": "技术支持",
"feedback": "请给我们您的反馈",
"inviteUrlDefaultMsg": "您的会议正在被创建。。。",
"me": "",
"speaker": "发言人",
"raisedHand": "请求发言",
"defaultNickname": "例如 星视通",
"defaultLink": "例如 __url__",
"callingName": "__name__",
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"firefoxGrantPermissions": "",
"operaGrantPermissions": "",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "",
"nwjsGrantPermissions": ""
"react-nativeGrantPermissions": "请点击<b><i>允许</i></b>按钮授权使用您的摄像头和麦克风",
"chromeGrantPermissions": "请点击<b><i>允许</i></b>按钮授权使用您的摄像头和麦克风",
"androidGrantPermissions": "请点击<b><i>允许</i></b>按钮授权使用您的摄像头和麦克风",
"firefoxGrantPermissions": "请点击<b><i>共享选择的设备</i></b>按钮授权使用您的摄像头和麦克风",
"operaGrantPermissions": "请点击<b><i>允许</i></b>按钮授权使用您的摄像头和麦克风",
"iexplorerGrantPermissions": "请点击<b><i>确认</i></b>按钮授权使用您的摄像头和麦克风",
"safariGrantPermissions": "请点击<b><i>确认</i></b>按钮授权使用您的摄像头和麦克风",
"nwjsGrantPermissions": "请授权使用您的摄像头和麦克风"
},
"keyboardShortcuts": {
"keyboardShortcuts": "",
"raiseHand": "",
"pushToTalk": "",
"toggleScreensharing": "",
"toggleFilmstrip": "",
"toggleShortcuts": "",
"focusLocal": "",
"focusRemote": "",
"toggleChat": "",
"mute": "",
"fullScreen": "",
"videoMute": ""
"keyboardShortcuts": "快捷键",
"raiseHand": "申请或取消发言",
"pushToTalk": "按住说话",
"toggleScreensharing": "在摄像头和屏幕共享之间切换",
"toggleFilmstrip": "显示或隐藏视频",
"toggleShortcuts": "显示或隐藏帮助菜单",
"focusLocal": "切换到本地视频上",
"focusRemote": "切换到远端视频上",
"toggleChat": "打开或关闭聊天",
"mute": "静音或取消静音",
"fullScreen": "全屏或退出全屏",
"videoMute": "开启或关闭视频"
},
"welcomepage": {
"go": "",
"roomname": "",
"disable": "",
"disable": "不再显示该页",
"feature1": {
"title": "",
"content": ""
"content": "无需下载. __app__ 直接通过浏览器使用。 分享您的会议链接给其他人即可参与会议。",
"title": "简单易用"
},
"feature2": {
"title": "",
"content": ""
"content": "多方视频会议所需带宽仅需128Kbps。 屏幕共享和语音会议所需的带宽更少。",
"title": "低带宽"
},
"feature3": {
"title": "",
"content": ""
"content": "__app__ 有Apache许可. 在此许可下,您可以免费下载,使用,修改和分享该代码",
"title": "开源"
},
"feature4": {
"title": "",
"content": ""
"content": "对于使用者和与会者没有人数的限制。 服务器的性能和带宽是唯一的限制因素。",
"title": "不限用户数"
},
"feature5": {
"title": "",
"content": ""
"content": "和他人共享屏幕非常简单。 __app__ 对于在线演示、讲座和技术支持会议再合适不过了。",
"title": "屏幕共享"
},
"feature6": {
"title": "",
"content": ""
"content": "是否担心隐私安全? __app__ 可以设定会议室密码防止他人进入会议。",
"title": "安全"
},
"feature7": {
"title": "",
"content": ""
"content": "__app__ 的一大特色是Etherpad——一个完美适用于会议、写作等场景可实时协作的文本编辑器。",
"title": "共享笔记"
},
"feature8": {
"title": "",
"content": ""
}
"content": "通过简单地整合Piwik, Google Analytics或者其他使用监控和统计系统来了解您的使用者。",
"title": "使用统计"
},
"go": "开始",
"join": "",
"privacy": "",
"roomname": "请输入房间名",
"roomnamePlaceHolder": "",
"sendFeedback": "",
"terms": ""
},
"startupoverlay": {
"policyText": "",
"title": ""
"policyText": "&nbsp;",
"title": "__app__ 需要使用您的麦克风和摄像头。"
},
"suspendedoverlay": {
"title": "",
"rejoinKeyTitle": ""
"title": "由于您的电脑休眠,视频通话已经中断。",
"rejoinKeyTitle": "重新加入"
},
"toolbar": {
"mute": "",
"videomute": "",
"authenticate": "",
"lock": "",
"invite": "",
"chat": "",
"etherpad": "",
"sharedvideo": "",
"sharescreen": "",
"fullscreen": "",
"sip": "",
"Settings": "",
"hangup": "",
"login": "",
"logout": "",
"dialpad": "",
"sharedVideoMutedPopup": "",
"micMutedPopup": "",
"talkWhileMutedPopup": "",
"unableToUnmutePopup": "",
"cameraDisabled": "",
"micDisabled": "",
"filmstrip": "",
"profile": "",
"raiseHand": ""
"mute": "静音 / 解除静音",
"videomute": "开启 / 关闭 摄像头",
"authenticate": "认证",
"lock": "锁定 / 解锁 房间",
"invite": "分享链接",
"chat": "开启 / 关闭 聊天",
"etherpad": "开启 / 关闭 共享文档",
"sharedvideo": "分享YouTube视频",
"sharescreen": "开启 / 关闭 屏幕共享",
"fullscreen": "开启 / 关闭 全屏",
"sip": "呼叫SIP号码",
"Settings": "设置",
"hangup": "离开",
"login": "登录",
"logout": "登出",
"dialpad": "开启 / 关闭 拨号盘",
"sharedVideoMutedPopup": "您共享的视频已被关闭 <br/> 您可以与其他与会者交谈。",
"micMutedPopup": "您的麦克风已被静音 您<br/>可以尽情享受您的共享视频。",
"talkWhileMutedPopup": "您在尝试发言吗? 当前您已被静音。",
"unableToUnmutePopup": "正在共享视频的时候您不能解除静音。",
"cameraDisabled": "摄像头不可用",
"micDisabled": "麦克风不可用",
"filmstrip": "显示 / 隐藏 视频",
"profile": "编辑您的简介",
"raiseHand": "请求 / 取消 发言"
},
"unsupportedBrowser": {
"appInstalled": "",
"appNotInstalled": "",
"downloadApp": "",
"joinConversation": "",
"startConference": ""
},
"bottomtoolbar": {
"chat": "",
"filmstrip": "",
"contactlist": ""
"chat": "开启 / 关闭 聊天",
"filmstrip": "显示 / 隐藏 视频",
"contactlist": "查看和邀请与会者"
},
"chat": {
"nickname": {
"title": "",
"popover": ""
"title": "请在下面的方框内输入昵称",
"popover": "选择一个昵称"
},
"messagebox": ""
"messagebox": "请输入文本..."
},
"settings": {
"title": "",
"update": "",
"name": "",
"startAudioMuted": "",
"startVideoMuted": "",
"selectCamera": "",
"selectMic": "",
"selectAudioOutput": "",
"followMe": "",
"noDevice": "",
"noPermission": "",
"cameraAndMic": "",
"moderator": "",
"password": "",
"audioVideo": "",
"setPasswordLabel": ""
"title": "设置",
"update": "更新",
"name": "名称",
"startAudioMuted": "所有人开始时静音",
"startVideoMuted": "所有人开始时隐藏视频画面",
"selectCamera": "摄像头",
"selectMic": "麦克风",
"selectAudioOutput": "音频输出",
"followMe": "所有人跟随我",
"noDevice": "未发现设备",
"noPermission": "未授权使用设备",
"cameraAndMic": "摄像头和麦克风",
"moderator": "主持人",
"password": "设定密码",
"audioVideo": "音频和视频",
"setPasswordLabel": "用密码锁定房间"
},
"profile": {
"title": "",
"setDisplayNameLabel": "",
"setEmailLabel": "",
"setEmailInput": ""
"title": "简介",
"setDisplayNameLabel": "设定您的显示名称",
"setEmailLabel": "设置您的个人全球统一标识邮箱",
"setEmailInput": "输入您的邮箱"
},
"videothumbnail": {
"editnickname": "",
"moderator": "",
"videomute": "",
"mute": "",
"kick": "",
"muted": "",
"domute": "",
"flip": ""
"editnickname": "点击编辑您的<br/>显示名称",
"moderator": "<br/>此会议的拥有者",
"videomute": "与会者已经<br/>关闭了摄像头",
"mute": "与会者已被静音",
"kick": "踢出",
"muted": "已静音",
"domute": "静音",
"flip": "翻转",
"remoteControl": "远程控制"
},
"connectionindicator": {
"header": "",
"bitrate": "",
"packetloss": "",
"resolution": "",
"less": "",
"more": "",
"address": "",
"remoteport_plural": "",
"remoteport": "",
"localport_plural": "",
"localport": "",
"localaddress_plural": "",
"localaddress": "",
"remoteaddress_plural": "",
"remoteaddress": "",
"transport": "",
"bandwidth": "",
"na": ""
"header": "连接数据",
"bitrate": "比特率",
"packetloss": "丢包",
"resolution": "分辨率",
"less": "显示更少",
"more": "显示更多",
"address": "地址",
"remoteport_plural": "远程端口:",
"remoteport": "远程端口:",
"localport_plural": "本地端口:",
"localport": "本地端口:",
"localaddress_plural": "本地地址:",
"localaddress": "本地地址:",
"remoteaddress_plural": "远程地址:",
"remoteaddress": "远程地址:",
"transport": "传输:",
"bandwidth": "估计带宽:",
"na": "会议开始可回到此处查看连接信息"
},
"notify": {
"disconnected": "",
"moderator": "",
"connected": "",
"somebody": "",
"me": "",
"focus": "",
"focusFail": "",
"grantedTo": "",
"grantedToUnknown": "",
"muted": "",
"mutedTitle": "",
"raisedHand": ""
"disconnected": "已断开连接",
"moderator": "已授权主持人权限!",
"connected": "已连接",
"somebody": "某人",
"me": "自己",
"focus": "会议聚焦",
"focusFail": "__component__ 不可用 - 在__ms__秒后重试",
"grantedTo": "主持权限已授予__to__",
"grantedToUnknown": "主持权限已授予$t(somebody)",
"muted": "您已经开始了通话,并处于静音状态。",
"mutedTitle": "您已被静音!",
"raisedHand": "请求发言"
},
"dialog": {
"add": "",
"kickMessage": "",
"popupError": "",
"passwordErrorTitle": "",
"passwordError": "",
"passwordError2": "",
"connectError": "",
"connectErrorWithMsg": "",
"incorrectPassword": "",
"connecting": "",
"copy": "",
"error": "",
"roomLocked": "",
"addPassword": "",
"createPassword": "",
"detectext": "",
"failtoinstall": "",
"failedpermissions": "",
"conferenceReloadTitle": "",
"conferenceReloadMsg": "",
"conferenceDisconnectTitle": "",
"conferenceDisconnectMsg": "",
"reconnectNow": "",
"conferenceReloadTimeLeft": "",
"maxUsersLimitReached": "",
"lockTitle": "",
"lockMessage": "",
"warning": "",
"passwordNotSupported": "",
"internalErrorTitle": "",
"internalError": "",
"unableToSwitch": "",
"SLDFailure": "",
"SRDFailure": "",
"oops": "",
"currentPassword": "",
"passwordLabel": "",
"defaultError": "",
"passwordRequired": "",
"Ok": "",
"done": "",
"Remove": "",
"removePassword": "",
"shareVideoTitle": "",
"shareVideoLinkError": "",
"removeSharedVideoTitle": "",
"removeSharedVideoMsg": "",
"alreadySharedVideoMsg": "",
"WaitingForHost": "",
"WaitForHostMsg": "",
"IamHost": "",
"Cancel": "",
"Submit": "",
"retry": "",
"logoutTitle": "",
"logoutQuestion": "",
"sessTerminated": "",
"hungUp": "",
"joinAgain": "",
"Share": "",
"Save": "",
"recording": "",
"recordingToken": "",
"Dial": "",
"sipMsg": "",
"passwordCheck": "",
"passwordMsg": "",
"shareLink": "",
"settings1": "",
"settings2": "",
"settings3": "",
"yourPassword": "",
"Back": "",
"serviceUnavailable": "",
"gracefulShutdown": "",
"Yes": "",
"reservationError": "",
"reservationErrorMsg": "",
"password": "",
"userPassword": "",
"token": "",
"tokenAuthFailedTitle": "",
"tokenAuthFailed": "",
"displayNameRequired": "",
"enterDisplayName": "",
"extensionRequired": "",
"firefoxExtensionPrompt": "",
"rateExperience": "",
"feedbackHelp": "",
"feedbackQuestion": "",
"thankYou": "",
"sorryFeedback": "",
"liveStreaming": "",
"streamKey": "",
"startLiveStreaming": "",
"stopStreamingWarning": "",
"stopRecordingWarning": "",
"stopLiveStreaming": "",
"stopRecording": "",
"doNotShowWarningAgain": "",
"doNotShowMessageAgain": "",
"permissionDenied": "",
"screenSharingPermissionDeniedError": "",
"micErrorPresent": "",
"cameraErrorPresent": "",
"cameraUnsupportedResolutionError": "",
"cameraUnknownError": "",
"cameraPermissionDeniedError": "",
"cameraNotFoundError": "",
"cameraConstraintFailedError": "",
"micUnknownError": "",
"micPermissionDeniedError": "",
"micNotFoundError": "",
"micConstraintFailedError": "",
"micNotSendingData": "",
"cameraNotSendingData": "",
"goToStore": "",
"externalInstallationTitle": "",
"externalInstallationMsg": "",
"muteParticipantTitle": "",
"muteParticipantBody": "",
"muteParticipantButton": ""
"add": "添加",
"kickMessage": "您已被踢出会议!",
"popupError": "您的浏览器禁止弹窗,请到浏览器安全设置里设定允许弹窗后重试。",
"passwordErrorTitle": "密码错误",
"passwordError": "此会议现在受密码保护。只有会议的拥有者可以设定密码。",
"passwordError2": "此会议现在受密码保护。只有会议的拥有者可以设定密码。",
"connectError": "发生错误,无法连接至会议!",
"connectErrorWithMsg": "发生错误,无法连接至会议: __msg__",
"incorrectPassword": "密码不正确",
"connecting": "连接中",
"copy": "复制",
"error": "错误",
"roomLocked": "此会话已被锁定,新与会者必须有链接地址和密码才能加入",
"addPassword": "添加密码",
"createPassword": "创建密码",
"detectext": "尝试检测桌面共享扩展时发生错误",
"failtoinstall": "安装桌面共享扩展失败",
"failedpermissions": "未能获取使用本地麦克风或摄像头的权限。",
"conferenceReloadTitle": "不好意思,出错了",
"conferenceReloadMsg": "正在尝试修复",
"conferenceDisconnectTitle": "您已断开连接,请检查您的网络连接。",
"conferenceDisconnectMsg": "重新连接中。。。",
"reconnectNow": "现在开始重新连接",
"conferenceReloadTimeLeft": "__seconds__秒。",
"maxUsersLimitReached": "已达到会议的最大人数限制,请稍后尝试!",
"lockTitle": "锁定失败",
"lockMessage": "锁定会议失败。",
"warning": "警告",
"passwordNotSupported": "当前不支持给房间加密码。",
"internalErrorTitle": "内部错误",
"internalError": "发生以下错误: [setRemoteDescription]",
"unableToSwitch": "无法转换视频流。",
"SLDFailure": "发生错误,无法静音! (SLD故障)",
"SRDFailure": "发生错误,无法关闭视频! (SRD故障)",
"oops": "哎呀!",
"currentPassword": "当前的密码是",
"passwordLabel": "密码",
"defaultError": "有某种错误",
"passwordRequired": "需要密码",
"Ok": "好的",
"done": "完成",
"Remove": "移除",
"removePassword": "移除密码",
"shareVideoTitle": "分享视频",
"shareVideoLinkError": "请提供正确的youtube链接。",
"removeSharedVideoTitle": "移除共享的视频",
"removeSharedVideoMsg": "您确定要移除共享的视频吗?",
"alreadySharedVideoMsg": "其他的与会者正在分享视频,同一时间只能有一个人分享视频。",
"WaitingForHost": "等待主持人。。。",
"WaitForHostMsg": "会议<b>__room__ </b>还没有开始。如果您是主持人请授权开始,否则请等待主持人。",
"IamHost": "我是主持人。",
"Cancel": "取消",
"Submit": "提交",
"retry": "重试",
"logoutTitle": "登出",
"logoutQuestion": "你确定要登出并停止会议吗",
"sessTerminated": "会话终止",
"hungUp": "挂断",
"joinAgain": "重新加入",
"Share": "分享",
"Save": "保存",
"recording": "录制中",
"recordingToken": "输入记录标识",
"Dial": "拨号",
"sipMsg": "输入SIP号码",
"passwordCheck": "确定要移除密码吗?",
"passwordMsg": "设定密码来锁定房间",
"shareLink": "分享此会议的链接",
"settings1": "配置您的会议",
"settings2": "与会者静音后加入",
"settings3": "需要昵称<br/><br/>设定密码来锁定房间:",
"yourPassword": "输入新的密码",
"Back": "返回",
"serviceUnavailable": "服务不可用",
"gracefulShutdown": "服务器正在维护,请稍后再试。",
"Yes": "",
"reservationError": "预定系统错误",
"reservationErrorMsg": "错误代号: __code__, 提示信息: __msg__",
"password": "输入密码",
"userPassword": "用户密码",
"token": "标识",
"tokenAuthFailedTitle": "认证问题",
"tokenAuthFailed": "对不起,您未被允许参加此会议。",
"displayNameRequired": "需要显示名称",
"enterDisplayName": "请输入您的显示名称",
"extensionRequired": "需要扩展程序",
"firefoxExtensionPrompt": "您需要安装Firefox的扩展才能使用屏幕共享功能。请从<a href='__url__'>这里获取后</a>!重试。",
"rateExperience": "请评价您的会议体验。",
"feedbackHelp": "您的反馈将帮助我们提高我们的视频体验。",
"feedbackQuestion": "告诉我们您的联系方式。",
"thankYou": "感谢使用__appName__",
"sorryFeedback": "很抱歉听到这些,能告诉我们更多详细情况吗?",
"liveStreaming": "流媒体直播中",
"streamKey": "流 名称/关键字",
"startLiveStreaming": "开始流媒体直播",
"stopStreamingWarning": "确定要停止流媒体直播吗?",
"stopRecordingWarning": "确定要停止录制吗",
"stopLiveStreaming": "停止流媒体直播",
"stopRecording": "停止录制",
"doNotShowWarningAgain": "不再显示此警告",
"doNotShowMessageAgain": "不再显示此信息",
"permissionDenied": "许可禁止",
"screenSharingPermissionDeniedError": "您并未授权分享屏幕。",
"micErrorPresent": "连接到麦克风时发生错误。",
"cameraErrorPresent": "连接到摄像头时发生错误。",
"cameraUnsupportedResolutionError": "您的摄像头不支持所需分辨率。",
"cameraUnknownError": "由于不可预知的错误,无法使用摄像头。",
"cameraPermissionDeniedError": "您未授权使用您的摄像头。您仍可参加会议但是其他人无法看到,使用地址栏里的摄像头按钮来启动摄像头。",
"cameraNotFoundError": "未发现摄像头",
"cameraConstraintFailedError": "摄像头不可用",
"micUnknownError": "未知错误,麦克风不可用",
"micPermissionDeniedError": "您未授权使用麦克风,您仍可参加会议但是其他人无法听到,使用地址栏里的摄像头按钮来启动麦克风",
"micNotFoundError": "未发现麦克风",
"micConstraintFailedError": "麦克风不满足所要求的限制",
"micNotSendingData": "麦克风无法使用,请从设置菜单选择其他设备或者重启应用",
"cameraNotSendingData": "摄像头无法使用,请检查摄像头是否被占用,从设置菜单选择其他设备或者重启应用。",
"goToStore": "跳转至应用商店",
"externalInstallationTitle": "需要扩展程序",
"externalInstallationMsg": "您需要安装桌面共享扩展",
"muteParticipantTitle": "静音该与会者吗?",
"muteParticipantBody": "您无法对他们解除静音,但是他们自己可以随时解除静音。",
"muteParticipantButton": "静音",
"remoteControlTitle": "远程控制",
"remoteControlDeniedMessage": "__user__ 拒绝了您的远程控制请求",
"remoteControlAllowedMessage": "__user__ 接受了您的远程控制请求",
"remoteControlErrorMessage": "在尝试向__user__请求远程控制权限时发生了一个错误",
"remoteControlStopMessage": "远程控制结束!"
},
"email": {
"sharedKey": "",
"subject": "",
"body": "",
"and": ""
"sharedKey": [
"该会议受密码保护,请在加入会议时使用下列密码:",
"",
"",
"__sharedKey__",
"",
""
],
"subject": "邀请至__appName__ (__conferenceName__)",
"body": [
"嗨, 我想请你加入刚建立的__appName__这个会议。",
"",
"",
"请点击下面的链接来加入会议。",
"",
"",
"__roomUrl__",
"",
"",
"__sharedKeyText__",
" 请注意__appName__现在只支持下列浏览器__supportedBrowsers__。",
"",
"",
"马上就可以和你交流了!"
],
"and": "添加"
},
"connection": {
"ERROR": "",
"CONNECTING": "",
"RECONNECTING": "",
"CONNFAIL": "",
"AUTHENTICATING": "",
"AUTHFAIL": "",
"CONNECTED": "",
"DISCONNECTED": "",
"DISCONNECTING": "",
"ATTACHED": ""
"ERROR": "错误",
"CONNECTING": "连接中",
"RECONNECTING": "网络错误,重连中。。。",
"CONNFAIL": "连接失败",
"AUTHENTICATING": "认证中",
"AUTHFAIL": "认证失败",
"CONNECTED": "已连接",
"DISCONNECTED": "已断开连接",
"DISCONNECTING": "断开连接中",
"ATTACHED": "已接入"
},
"recording": {
"pending": "",
"on": "",
"off": "",
"failedToStart": "",
"buttonTooltip": "",
"error": "",
"unavailable": ""
"pending": "录制中,等待一位与会者加入",
"on": "录制中",
"off": "录制已停止",
"failedToStart": "录制启动失败",
"buttonTooltip": "开始 / 结束录制",
"error": "录制失败。请重新尝试。",
"unavailable": "录制服务目前不可用。请稍后再试。"
},
"liveStreaming": {
"pending": "",
"on": "",
"off": "",
"unavailable": "",
"failedToStart": "",
"buttonTooltip": "",
"streamIdRequired": "",
"error": "",
"busy": ""
"pending": "启动流媒体。。。",
"on": "流媒体直播中",
"off": "流媒体直播已结束",
"unavailable": "流媒体直播服务当前不可用,请稍后再试。",
"failedToStart": "流媒体直播启动失败",
"buttonTooltip": "启动 / 停止流媒体直播",
"streamIdRequired": "请填写媒体流ID来启动流媒体直播。",
"streamIdHelp": "在哪里找到这个",
"error": "流媒体直播失败。请重试。",
"busy": "所有的录制器都在忙。请稍后重试。"
}
}

View File

@@ -15,14 +15,15 @@
"defaultLink": "e.g. __url__",
"callingName": "__name__",
"userMedia": {
"react-nativeGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Allow</i></b> button",
"chromeGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Allow</i></b> button",
"androidGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Allow</i></b> button",
"firefoxGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Share Selected Device</i></b> button",
"operaGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Allow</i></b> button",
"iexplorerGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>OK</i></b> button",
"safariGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>OK</i></b> button",
"nwjsGrantPermissions": "Please grant permissions to use your camera and microphone"
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
"operaGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
"safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
"nwjsGrantPermissions": "Please grant permissions to use your camera and microphone",
"edgeGrantPermissions": "Select <b><i>Yes</i></b> when your browser asks for permissions."
},
"keyboardShortcuts": {
"keyboardShortcuts": "Keyboard shortcuts",
@@ -36,7 +37,8 @@
"toggleChat": "Open or close the chat",
"mute": "Mute or unmute your microphone",
"fullScreen": "Enter or exit full screen",
"videoMute": "Start or stop your camera"
"videoMute": "Start or stop your camera",
"showSpeakerStats": "Show speaker stats"
},
"welcomepage":{
"disable": "Don't show this page again",
@@ -81,11 +83,12 @@
"terms": "Terms"
},
"startupoverlay": {
"policyText": "&nbsp;",
"policyText": " ",
"title": "__app__ needs to use your microphone and camera."
},
"suspendedoverlay": {
"title": "Your video call was interrupted, because this computer went to sleep.",
"text": "Press <i>Rejoin</i> button to connect back to your conversation.",
"rejoinKeyTitle": "Rejoin"
},
"toolbar": {
@@ -177,6 +180,7 @@
"bitrate": "Bitrate:",
"packetloss": "Packet loss:",
"resolution": "Resolution:",
"framerate": "Frame rate:",
"less": "Show less",
"more": "Show more",
"address": "Address:",
@@ -189,8 +193,10 @@
"remoteaddress": "Remote address:",
"remoteaddress_plural": "Remote addresses:",
"transport": "Transport:",
"transport_plural": "Transports:",
"bandwidth": "Estimated bandwidth:",
"na": "Come back here for connection information once the conference starts"
"na": "Come back here for connection information once the conference starts",
"direct": " (direct)"
},
"notify": {
"disconnected": "disconnected",
@@ -201,7 +207,7 @@
"focus": "Conference focus",
"focusFail": "__component__ not available - retry in __ms__ sec",
"grantedTo": "Moderator rights granted to __to__!",
"grantedToUnknown": "Moderator rights granted to $t(somebody)!",
"grantedToUnknown": "Moderator rights granted to $t(notify.somebody)!",
"muted": "You have started the conversation muted.",
"mutedTitle": "You're muted!",
"raisedHand": "Would like to speak."
@@ -225,12 +231,11 @@
"detectext": "Error when trying to detect desktopsharing extension.",
"failtoinstall": "Failed to install desktop sharing extension",
"failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.",
"conferenceReloadTitle": "Unfortunately, something went wrong",
"conferenceReloadMsg": "We're trying to fix this",
"conferenceDisconnectTitle": "You have been disconnected. You may want to check your network connection.",
"conferenceDisconnectMsg": "Reconnecting in...",
"reconnectNow": "Reconnect now",
"conferenceReloadTimeLeft": "__seconds__ sec.",
"conferenceReloadTitle": "Unfortunately, something went wrong.",
"conferenceReloadMsg": "We're trying to fix this. Reconnecting in __seconds__ sec...",
"conferenceDisconnectTitle": "You have been disconnected.",
"conferenceDisconnectMsg": "You may want to check your network connection. Reconnecting in __seconds__ sec...",
"rejoinNow": "Rejoin now",
"maxUsersLimitReached": "The limit for maximum number of participants in the conference has been reached. The conference is full. Please try again later!",
"lockTitle": "Lock failed",
"lockMessage": "Failed to lock the conference.",
@@ -333,7 +338,11 @@
"remoteControlDeniedMessage": "__user__ rejected your remote control request!",
"remoteControlAllowedMessage": "__user__ accepted your remote control request!",
"remoteControlErrorMessage": "An error occurred while trying to request remote control permissions from __user__!",
"remoteControlStopMessage": "The remote control session ended!"
"remoteControlStopMessage": "The remote control session ended!",
"close": "Close",
"shareYourScreen": "Share your screen",
"yourEntireScreen": "Your entire screen",
"applicationWindow": "Application window"
},
"email":
{
@@ -378,7 +387,9 @@
"FETCH_SESSION_ID": "Obtaining session-id...",
"GOT_SESSION_ID": "Obtaining session-id... Done",
"GET_SESSION_ID_ERROR": "Get session-id error: __code__",
"USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..."
"USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues...",
"LOW_BANDWIDTH": "Video for __displayName__ has been turned off to save bandwidth"
},
"recording":
{
@@ -402,5 +413,14 @@
"streamIdHelp": "Where do I find this?",
"error": "Live streaming failed. Please try again.",
"busy": "All recorders are currently busy. Please try again later."
},
"speakerStats":
{
"hours": "__count__h",
"minutes": "__count__m",
"name": "Name",
"seconds": "__count__s",
"speakerStats": "Speaker Stats",
"speakerTime": "Speaker Time"
}
}

View File

@@ -93,10 +93,12 @@ function changeParticipantNumber(APIInstance, number) {
* @param interfaceConfigOverwrite object containing configuration options
* defined in interface_config.js to be overridden.
* @param noSsl if the value is true https won't be used
* @param {string} [jwt] the JWT token if needed by jitsi-meet for
* authentication.
* @constructor
*/
function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode,
configOverwrite, interfaceConfigOverwrite, noSsl) {
configOverwrite, interfaceConfigOverwrite, noSsl, jwt) {
if (!width || width < MIN_WIDTH)
width = MIN_WIDTH;
if (!height || height < MIN_HEIGHT)
@@ -121,6 +123,11 @@ function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode,
this.url = (noSsl) ? "http" : "https" +"://" + domain + "/";
if(room_name)
this.url += room_name;
if (jwt) {
this.url += '?jwt=' + jwt;
}
this.url += "#jitsi_meet_external_api_id=" + id;
var key;

View File

@@ -315,12 +315,6 @@ UI.start = function () {
// Initialise the recording module.
if (config.enableRecording)
Recording.init(eventEmitter, config.recordingType);
// Display notice message at the top of the toolbar
if (config.noticeMessage) {
$('#noticeText').text(config.noticeMessage);
UIUtil.setVisible('notice', true);
}
} else {
$("body").addClass("filmstrip-only");
UIUtil.setVisible('mainToolbarContainer', false);
@@ -564,8 +558,7 @@ UI.updateUserRole = user => {
} else {
messageHandler.notify(
'', 'notify.somebody',
'connected', 'notify.grantedToUnknown', {}
);
'connected', 'notify.grantedToUnknown');
}
};
@@ -844,8 +837,8 @@ UI.markDominantSpeaker = function (id) {
VideoLayout.onDominantSpeakerChanged(id);
};
UI.handleLastNEndpoints = function (ids, enteringIds) {
VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
UI.handleLastNEndpoints = function (leavingIds, enteringIds) {
VideoLayout.onLastNEndpointsChanged(leavingIds, enteringIds);
};
/**

View File

@@ -1,4 +1,5 @@
/* global $, APP, config */
/* global $, APP, config, JitsiMeetJS */
const ConnectionErrors = JitsiMeetJS.errors.connection;
/**
* Build html for "password required" dialog.
@@ -127,14 +128,30 @@ function LoginDialog(successCallback, cancelCallback) {
/**
* Displays error message in 'finished' state which allows either to cancel
* or retry.
* @param messageKey the key to the message to be displayed.
* @param error the key to the error to be displayed.
* @param options the options to the error message (optional)
*/
this.displayError = function (messageKey, options) {
this.displayError = function (error, options) {
let finishedState = connDialog.getState('finished');
let errorMessageElem = finishedState.find('#errorMessage');
let messageKey;
if (error === ConnectionErrors.PASSWORD_REQUIRED) {
// this is a password required error, as login window was already
// open once, this means username or password is wrong
messageKey = 'dialog.incorrectPassword';
}
else {
messageKey = 'dialog.connectErrorWithMsg';
if (!options)
options = {};
options.msg = error;
}
errorMessageElem.attr("data-i18n", messageKey);
APP.translation.translateElement($(errorMessageElem), options);

View File

@@ -61,6 +61,9 @@ const htmlStr = `
function initHTML() {
$(`#${sidePanelsContainerId}`)
.append(htmlStr);
// make sure we translate the panel, as adding it can be after i18n
// library had initialized and translated already present html
APP.translation.translateElement($(`#${sidePanelsContainerId}`));
}
/**
@@ -162,6 +165,9 @@ export default {
selectInput[0].dataset.i18n =
`languages:${APP.translation.getCurrentLanguage()}`;
// translate selectInput, which is the currently selected language
// otherwise there will be no selected option
APP.translation.translateElement(selectInput);
APP.translation.translateElement(selectEl);
APP.translation.addLanguageChangedListener(

View File

@@ -36,6 +36,7 @@ function ConnectionIndicator(videoContainer, videoId) {
this.resolution = null;
this.isResolutionHD = null;
this.transport = [];
this.framerate = null;
this.popover = null;
this.id = videoId;
this.create();
@@ -88,12 +89,17 @@ ConnectionIndicator.prototype.generateText = function () {
}
// GENERATE RESOLUTIONS STRING
let resolutions = this.resolution || {};
let resolutionStr = Object.keys(resolutions).map(function (ssrc) {
const resolutions = this.resolution || {};
const resolutionStr = Object.keys(resolutions).map(ssrc => {
let {width, height} = resolutions[ssrc];
return `${width}x${height}`;
}).join(', ') || 'N/A';
const framerates = this.framerate || {};
const frameRateStr = Object.keys(framerates).map(ssrc =>
framerates[ssrc]
).join(', ') || 'N/A';
let result = (
`<table class="connection-info__container" style='width:100%'>
<tr>
@@ -119,6 +125,14 @@ ConnectionIndicator.prototype.generateText = function () {
${resolutionStr}
</td>
</tr>
<tr>
<td>
<span data-i18n='connectionindicator.framerate'></span>
</td>
<td>
${frameRateStr}
</td>
</tr>
</table>`);
if(this.videoContainer.videoSpanId == "localVideoContainer") {
@@ -150,7 +164,12 @@ ConnectionIndicator.prototype.generateText = function () {
"data-i18n='connectionindicator.address'></span></td>" +
"<td> N/A</td></tr>";
} else {
var data = {remoteIP: [], localIP:[], remotePort:[], localPort:[]};
var data = {
remoteIP: [],
localIP:[],
remotePort:[],
localPort:[],
transportType:[]};
for(i = 0; i < this.transport.length; i++) {
var ip = ConnectionIndicator.getIP(this.transport[i].ip);
var port = ConnectionIndicator.getPort(this.transport[i].ip);
@@ -173,8 +192,15 @@ ConnectionIndicator.prototype.generateText = function () {
if(data.localPort.indexOf(localPort) == -1) {
data.localPort.push(localPort);
}
if(data.transportType.indexOf(this.transport[i].type) == -1) {
data.transportType.push(this.transport[i].type);
}
}
// All of the transports should be either P2P or JVB
const isP2P = this.transport.length ? this.transport[0].p2p : false;
var local_address_key = "connectionindicator.localaddress";
var remote_address_key = "connectionindicator.remoteaddress";
var localTransport =
@@ -215,9 +241,18 @@ ConnectionIndicator.prototype.generateText = function () {
transport += "</td></tr>";
transport += localTransport + "</td></tr>";
transport +="<tr>" +
"<td><span data-i18n='connectionindicator.transport'>" +
"</span></td>" +
"<td>" + this.transport[0].type + "</td></tr>";
"<td><span data-i18n='connectionindicator.transport' "
+ " data-i18n-options='" +
JSON.stringify({count: data.transportType.length})
+ "'></span></td>" +
"<td>"
+ ConnectionIndicator.getStringFromArray(data.transportType);
// Append (direct) to indicate the P2P type of transport
if (isP2P) {
transport += "<span data-i18n='connectionindicator.direct'>";
}
// Close "type" column and end table row
transport += "</td></tr>";
}
@@ -321,17 +356,17 @@ ConnectionIndicator.prototype.remove = function() {
* the user is having connectivity issues.
*/
ConnectionIndicator.prototype.updateConnectionStatusIndicator
= function (isActive) {
this.isConnectionActive = isActive;
if (this.isConnectionActive) {
$(this.interruptedIndicator).hide();
$(this.emptyIcon).show();
$(this.fullIcon).show();
} else {
$(this.interruptedIndicator).show();
$(this.emptyIcon).hide();
$(this.fullIcon).hide();
}
= function (isActive) {
this.isConnectionActive = isActive;
if (this.isConnectionActive) {
$(this.interruptedIndicator).hide();
$(this.emptyIcon).show();
$(this.fullIcon).show();
} else {
$(this.interruptedIndicator).show();
$(this.emptyIcon).hide();
$(this.fullIcon).hide();
}
};
/**
@@ -358,6 +393,8 @@ ConnectionIndicator.prototype.updateConnectionQuality =
if (object.resolution) {
this.resolution = object.resolution;
}
if (object.framerate)
this.framerate = object.framerate;
}
let width = qualityToWidth.find(x => percent >= x.percent);
@@ -380,6 +417,15 @@ ConnectionIndicator.prototype.updateResolution = function (resolution) {
this.updatePopoverData();
};
/**
* Updates the framerate
* @param framerate the new resolution
*/
ConnectionIndicator.prototype.updateFramerate = function (framerate) {
this.framerate = framerate;
this.updatePopoverData();
};
/**
* Updates the content of the popover if its visible
* @param force to work even if popover is not visible

View File

@@ -127,7 +127,9 @@ export default class LargeVideoManager {
// the video was not rendered, before the connection has failed.
const isHavingConnectivityIssues
= APP.conference.isParticipantConnectionActive(id) === false;
if (isHavingConnectivityIssues
if (videoType === VIDEO_CONTAINER_TYPE
&& isHavingConnectivityIssues
&& (isUserSwitch || !container.wasVideoRendered)) {
showAvatar = true;
}
@@ -155,10 +157,15 @@ export default class LargeVideoManager {
// Make sure no notification about remote failure is shown as
// its UI conflicts with the one for local connection interrupted.
const isConnected = APP.conference.isConnectionInterrupted()
|| !isHavingConnectivityIssues;
this.updateParticipantConnStatusIndication(
id,
APP.conference.isConnectionInterrupted()
|| !isHavingConnectivityIssues);
isConnected,
(isHavingConnectivityIssues)
? "connection.USER_CONNECTION_INTERRUPTED"
: "connection.LOW_BANDWIDTH");
// resolve updateLargeVideo promise after everything is done
promise.then(resolve);
@@ -180,10 +187,11 @@ export default class LargeVideoManager {
* @param {string} id the id of remote participant(MUC nickname)
* @param {boolean} isConnected true if the connection is active or false
* when the user is having connectivity issues.
* @param {string} messageKey the i18n key of the message
*
* @private
*/
updateParticipantConnStatusIndication (id, isConnected) {
updateParticipantConnStatusIndication (id, isConnected, messageKey) {
// Apply grey filter on the large video
this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected);
@@ -196,7 +204,7 @@ export default class LargeVideoManager {
let displayName
= APP.conference.getParticipantDisplayName(id);
this._setRemoteConnectionMessage(
"connection.USER_CONNECTION_INTERRUPTED",
messageKey,
{ displayName: displayName });
// Show it now only if the VideoContainer is on top
@@ -332,7 +340,7 @@ export default class LargeVideoManager {
*/
showRemoteConnectionMessage (show) {
if (typeof show !== 'boolean') {
show = APP.conference.isParticipantConnectionActive(this.id);
show = !APP.conference.isParticipantConnectionActive(this.id);
}
if (show) {
@@ -458,7 +466,7 @@ export default class LargeVideoManager {
// "avatar" and "video connection" can not be displayed both
// at the same time, but the latter is of higher priority and it
// will hide the avatar one if will be displayed.
this.showRemoteConnectionMessage(/* fet the current state */);
this.showRemoteConnectionMessage(/* fetch the current state */);
this.showLocalConnectionMessage(/* fetch the current state */);
}
});

View File

@@ -460,12 +460,12 @@ RemoteVideo.prototype.setMutedView = function(isMuted) {
* @private
*/
RemoteVideo.prototype._figureOutMutedWhileDisconnected
= function(isDisconnected) {
if (isDisconnected && this.isVideoMuted) {
this.mutedWhileDisconnected = true;
} else if (!isDisconnected && !this.isVideoMuted) {
this.mutedWhileDisconnected = false;
}
= function(isDisconnected) {
if (isDisconnected && this.isVideoMuted) {
this.mutedWhileDisconnected = true;
} else if (!isDisconnected && !this.isVideoMuted) {
this.mutedWhileDisconnected = false;
}
};
/**
@@ -554,8 +554,7 @@ RemoteVideo.prototype.isVideoPlayable = function () {
*/
RemoteVideo.prototype.updateView = function () {
this.updateConnectionStatusIndicator(
null /* will obtain the status from 'conference' */);
this.updateConnectionStatusIndicator();
// This must be called after 'updateConnectionStatusIndicator' because it
// affects the display mode by modifying 'mutedWhileDisconnected' flag
@@ -564,19 +563,13 @@ RemoteVideo.prototype.updateView = function () {
/**
* Updates the UI to reflect user's connectivity status.
* @param isActive {boolean|null} 'true' if user's connection is active or
* 'false' when the use is having some connectivity issues and a warning
* should be displayed. When 'null' is passed then the current value will be
* obtained from the conference instance.
*/
RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) {
// Check for initial value if 'isActive' is not defined
if (typeof isActive !== "boolean") {
isActive = this.isConnectionActive();
if (isActive === null) {
// Cancel processing at this point - no update
return;
}
RemoteVideo.prototype.updateConnectionStatusIndicator = function () {
const isActive = this.isConnectionActive();
if (isActive === null) {
// Cancel processing at this point - no update
return;
}
logger.debug(this.id + " thumbnail is connection active ? " + isActive);
@@ -700,50 +693,22 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
}
};
/**
* Show/hide peer container for the given id.
*/
RemoteVideo.prototype.showPeerContainer = function (state) {
if (!this.container)
return;
var isHide = state === 'hide';
var resizeThumbnails = false;
if (!isHide) {
if (!$(this.container).is(':visible')) {
resizeThumbnails = true;
$(this.container).show();
}
// Call updateView, so that we'll figure out if avatar
// should be displayed based on video muted status and whether or not
// it's in the lastN set
this.updateView();
}
else if ($(this.container).is(':visible') && isHide)
{
resizeThumbnails = true;
$(this.container).hide();
if(this.connectionIndicator)
this.connectionIndicator.hide();
}
if (resizeThumbnails) {
this.VideoLayout.resizeThumbnails();
}
// We want to be able to pin a participant from the contact list, even
// if he's not in the lastN set!
// ContactList.setClickable(id, !isHide);
};
RemoteVideo.prototype.updateResolution = function (resolution) {
if (this.connectionIndicator) {
this.connectionIndicator.updateResolution(resolution);
}
};
/**
* Updates this video framerate indication.
* @param framerate the value to update
*/
RemoteVideo.prototype.updateFramerate = function (framerate) {
if (this.connectionIndicator) {
this.connectionIndicator.updateFramerate(framerate);
}
};
RemoteVideo.prototype.removeConnectionIndicator = function () {
if (this.connectionIndicator)
this.connectionIndicator.remove();

View File

@@ -1,4 +1,4 @@
/* global $, JitsiMeetJS, interfaceConfig */
/* global $, APP, JitsiMeetJS, interfaceConfig */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import Avatar from "../avatar/Avatar";
@@ -446,7 +446,7 @@ SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
SmallVideo.prototype.isVideoPlayable = function() {
return this.videoStream // Is there anything to display ?
&& !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
&& (this.isLocal || this.VideoLayout.isInLastN(this.id));
&& (this.isLocal || APP.conference.isInLastN(this.id));
};
/**

View File

@@ -1,4 +1,4 @@
/* global config, APP, $, interfaceConfig */
/* global APP, $, interfaceConfig */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import FilmStrip from "./FilmStrip";
@@ -14,10 +14,6 @@ var remoteVideos = {};
var localVideoThumbnail = null;
var currentDominantSpeaker = null;
var localLastNCount = config.channelLastN;
var localLastNSet = [];
var lastNEndpointsCache = [];
var lastNPickupId = null;
var eventEmitter = null;
@@ -60,8 +56,6 @@ function onContactClicked (id) {
// let the bridge adjust its lastN set for myjid and store
// the pinned user in the lastNPickupId variable to be
// picked up later by the lastN changed event handler.
lastNPickupId = id;
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true);
}
}
@@ -114,8 +108,6 @@ var VideoLayout = {
// the local video thumb maybe one pixel
this.resizeThumbnails(false, true);
this.lastNCount = config.channelLastN;
this.registerListeners();
},
@@ -162,14 +154,6 @@ var VideoLayout = {
largeVideo.updateLargeVideoAudioLevel(lvl);
},
isInLastN (resource) {
return this.lastNCount < 0 || // lastN is disabled
// lastNEndpoints cache not built yet
(this.lastNCount > 0 && !lastNEndpointsCache.length) ||
(lastNEndpointsCache &&
lastNEndpointsCache.indexOf(resource) !== -1);
},
changeLocalAudio (stream) {
let localAudio = document.getElementById('localAudio');
localAudio = stream.attach(localAudio);
@@ -457,13 +441,8 @@ var VideoLayout = {
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
}
// In case this is not currently in the last n we don't show it.
if (localLastNCount && localLastNCount > 0 &&
FilmStrip.getThumbs().remoteThumbs.length >= localLastNCount + 2) {
remoteVideo.showPeerContainer('hide');
} else {
VideoLayout.resizeThumbnails(false, true);
}
VideoLayout.resizeThumbnails(false, true);
// Initialize the view
remoteVideo.updateView();
},
@@ -708,145 +687,32 @@ var VideoLayout = {
/**
* On last N change event.
*
* @param lastNEndpoints the list of last N endpoints
* @param endpointsLeavingLastN the list currently leaving last N
* endpoints
* @param endpointsEnteringLastN the list currently entering last N
* endpoints
*/
onLastNEndpointsChanged (lastNEndpoints, endpointsEnteringLastN) {
if (this.lastNCount !== lastNEndpoints.length)
this.lastNCount = lastNEndpoints.length;
lastNEndpointsCache = lastNEndpoints;
// Say A, B, C, D, E, and F are in a conference and LastN = 3.
//
// If LastN drops to, say, 2, because of adaptivity, then E should see
// thumbnails for A, B and C. A and B are in E's server side LastN set,
// so E sees them. C is only in E's local LastN set.
//
// If F starts talking and LastN = 3, then E should see thumbnails for
// F, A, B. B gets "ejected" from E's server side LastN set, but it
// enters E's local LastN ejecting C.
// Increase the local LastN set size, if necessary.
if (this.lastNCount > localLastNCount) {
localLastNCount = this.lastNCount;
onLastNEndpointsChanged (endpointsLeavingLastN, endpointsEnteringLastN) {
if (endpointsLeavingLastN) {
endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
}
// Update the local LastN set preserving the order in which the
// endpoints appeared in the LastN/local LastN set.
var nextLocalLastNSet = lastNEndpoints.slice(0);
for (var i = 0; i < localLastNSet.length; i++) {
if (nextLocalLastNSet.length >= localLastNCount) {
break;
}
var resourceJid = localLastNSet[i];
if (nextLocalLastNSet.indexOf(resourceJid) === -1) {
nextLocalLastNSet.push(resourceJid);
}
if (endpointsEnteringLastN) {
endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
}
},
localLastNSet = nextLocalLastNSet;
var updateLargeVideo = false;
// Handle LastN/local LastN changes.
FilmStrip.getThumbs().remoteThumbs.each(( index, element ) => {
var resourceJid = getPeerContainerResourceId(element);
var smallVideo = remoteVideos[resourceJid];
// We do not want to process any logic for our own(local) video
// because the local participant is never in the lastN set.
// The code of this function might detect that the local participant
// has been dropped out of the lastN set and will update the large
// video
// Detected from avatar tests, where lastN event override
// local video pinning
if(APP.conference.isLocalId(resourceJid))
return;
var isReceived = true;
if (resourceJid &&
lastNEndpoints.indexOf(resourceJid) < 0 &&
localLastNSet.indexOf(resourceJid) < 0) {
logger.log("Remove from last N", resourceJid);
if (smallVideo)
smallVideo.showPeerContainer('hide');
else if (!APP.conference.isLocalId(resourceJid))
logger.error("No remote video for: " + resourceJid);
isReceived = false;
} else if (resourceJid &&
//TOFIX: smallVideo may be undefined
smallVideo.isVisible() &&
lastNEndpoints.indexOf(resourceJid) < 0 &&
localLastNSet.indexOf(resourceJid) >= 0) {
// TOFIX: if we're here we already know that the smallVideo
// exists. Look at the previous FIX above.
if (smallVideo)
smallVideo.showPeerContainer('avatar');
else if (!APP.conference.isLocalId(resourceJid))
logger.error("No remote video for: " + resourceJid);
isReceived = false;
}
if (!isReceived) {
// resourceJid has dropped out of the server side lastN set, so
// it is no longer being received. If resourceJid was being
// displayed in the large video we have to switch to another
// user.
if (!updateLargeVideo &&
this.isCurrentlyOnLarge(resourceJid)) {
updateLargeVideo = true;
}
}
});
if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
endpointsEnteringLastN = lastNEndpoints;
if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
endpointsEnteringLastN.forEach(function (resourceJid) {
var remoteVideo = remoteVideos[resourceJid];
if (remoteVideo)
remoteVideo.showPeerContainer('show');
if (!remoteVideo.isVisible()) {
logger.log("Add to last N", resourceJid);
remoteVideo.addRemoteStreamElement(remoteVideo.videoStream);
if (lastNPickupId == resourceJid) {
// Clean up the lastN pickup id.
lastNPickupId = null;
VideoLayout.handleVideoThumbClicked(resourceJid);
updateLargeVideo = false;
}
remoteVideo.waitForPlayback(
remoteVideo.selectVideoElement()[0],
remoteVideo.videoStream);
}
});
}
// The endpoint that was being shown in the large video has dropped out
// of the lastN set and there was no lastN pickup jid. We need to update
// the large video now.
if (updateLargeVideo) {
var resource;
// Find out which endpoint to show in the large video.
for (i = 0; i < lastNEndpoints.length; i++) {
resource = lastNEndpoints[i];
if (!resource || APP.conference.isLocalId(resource))
continue;
// videoSrcToSsrc needs to be update for this call to succeed.
this.updateLargeVideo(resource);
break;
/**
* Updates remote video by id if it exists.
* @param {string} id of the remote video
* @private
*/
_updateRemoteVideo(id) {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateView();
if (remoteVideo.isCurrentlyOnLargeVideo()) {
this.updateLargeVideo(id);
}
}
},
@@ -857,21 +723,37 @@ var VideoLayout = {
* @param object
*/
updateLocalConnectionStats (percent, object) {
let resolutions = object.resolution;
const { framerate, resolution } = object;
object.resolution = resolutions[APP.conference.getMyUserId()];
// FIXME overwrites 'lib-jitsi-meet' internal object
// Why library internal objects are passed as event's args ?
object.resolution = resolution[APP.conference.getMyUserId()];
object.framerate = framerate[APP.conference.getMyUserId()];
localVideoThumbnail.updateStatsIndicator(percent, object);
Object.keys(resolutions).forEach(function (id) {
Object.keys(resolution).forEach(function (id) {
if (APP.conference.isLocalId(id)) {
return;
}
let resolution = resolutions[id];
let resolutionValue = resolution[id];
let remoteVideo = remoteVideos[id];
if (resolution && remoteVideo) {
remoteVideo.updateResolution(resolution);
if (resolutionValue && remoteVideo) {
remoteVideo.updateResolution(resolutionValue);
}
});
Object.keys(framerate).forEach(function (id) {
if (APP.conference.isLocalId(id)) {
return;
}
const framerateValue = framerate[id];
const remoteVideo = remoteVideos[id];
if (framerateValue && remoteVideo) {
remoteVideo.updateFramerate(framerateValue);
}
});
},

View File

@@ -1,6 +1,6 @@
const logger = require("jitsi-meet-logger").getLogger(__filename);
import { replace } from '../util/helpers';
import { reload, replace } from '../util/helpers';
/**
* The modules stores information about the URL used to start the conference and
@@ -67,7 +67,14 @@ export default class ConferenceUrl {
* Reloads the conference using original URL with all of the parameters.
*/
reload() {
logger.info("Reloading the conference using URL: " + this.originalURL);
replace(this.originalURL);
logger.info(`Reloading the conference using URL: ${this.originalURL}`);
// Check if we are in an iframe and reload with the reload() utility
// because replace() is not working on an iframe.
if(window.self !== window.top) {
reload();
} else {
replace(this.originalURL);
}
}
}

View File

@@ -1,5 +1,10 @@
/* global APP, $, JitsiMeetJS */
import {
toggleDialog
} from '../../react/features/base/dialog';
import { SpeakerStats } from '../../react/features/speaker-stats';
/**
* The reference to the shortcut dialogs when opened.
*/
@@ -29,6 +34,13 @@ function initGlobalShortcuts() {
});
KeyboardShortcut._addShortcutToHelp("SPACE","keyboardShortcuts.pushToTalk");
KeyboardShortcut.registerShortcut("T", null, () => {
JitsiMeetJS.analytics.sendEvent("shortcut.speakerStats.clicked");
APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}, "keyboardShortcuts.showSpeakerStats");
/**
* FIXME: Currently focus keys are directly implemented below in onkeyup.
* They should be moved to the SmallVideo instead.

View File

@@ -17,18 +17,22 @@
"//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
"dependencies": {
"@atlassian/aui": "6.0.6",
"@atlaskit/button": "1.0.3",
"@atlaskit/button-group": "1.0.0",
"@atlaskit/modal-dialog": "1.2.4",
"@atlaskit/tabs": "1.2.5",
"async": "0.9.0",
"autosize": "1.18.13",
"bootstrap": "3.1.1",
"es6-iterator": "2.0.0",
"es6-symbol": "3.1.0",
"i18next": "7.0.0",
"es6-iterator": "2.0.1",
"es6-symbol": "3.1.1",
"i18next": "7.1.3",
"i18next-browser-languagedetector": "1.0.1",
"i18next-xhr-backend": "1.3.0",
"i18next-xhr-backend": "1.4.1",
"jitsi-meet-logger": "jitsi/jitsi-meet-logger",
"jquery": "2.1.4",
"jquery-contextmenu": "2.4.3",
"jquery-i18next": "1.1.0",
"jquery-i18next": "1.2.0",
"jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0",
"jquery-ui": "1.10.5",
"jssha": "1.5.0",
@@ -37,11 +41,11 @@
"postis": "2.2.0",
"react": "15.4.2",
"react-dom": "15.4.2",
"react-i18next": "2.2.0",
"react-native": "0.42.0",
"react-i18next": "2.2.3",
"react-native": "0.42.3",
"react-native-background-timer": "1.0.0",
"react-native-immersive": "0.0.4",
"react-native-keep-awake": "2.0.2",
"react-native-keep-awake": "2.0.3",
"react-native-locale-detector": "1.0.1",
"react-native-prompt": "1.0.0",
"react-native-vector-icons": "4.0.0",
@@ -57,22 +61,22 @@
"xmldom": "0.1.27"
},
"devDependencies": {
"babel-core": "6.23.1",
"babel-eslint": "7.1.1",
"babel-loader": "6.3.2",
"babel-core": "6.24.0",
"babel-eslint": "7.2.1",
"babel-loader": "6.4.1",
"babel-polyfill": "6.23.0",
"babel-preset-es2015": "6.22.0",
"babel-preset-es2015": "6.24.0",
"babel-preset-react": "6.23.0",
"babel-preset-stage-1": "6.22.0",
"clean-css": "3.4.25",
"css-loader": "0.26.2",
"eslint": "3.16.1",
"eslint-plugin-flowtype": "2.30.0",
"css-loader": "0.28.0",
"eslint": "3.18.0",
"eslint-plugin-flowtype": "2.30.4",
"eslint-plugin-import": "2.2.0",
"eslint-plugin-jsdoc": "2.4.0",
"eslint-plugin-react": "6.10.0",
"eslint-plugin-react-native": "2.2.1",
"expose-loader": "0.7.1",
"eslint-plugin-jsdoc": "3.0.0",
"eslint-plugin-react": "6.10.3",
"eslint-plugin-react-native": "2.3.1",
"expose-loader": "0.7.3",
"file-loader": "0.10.1",
"flow-bin": "0.38.0",
"haste-resolver-webpack-plugin": "0.2.2",
@@ -81,8 +85,8 @@
"json-loader": "0.5.4",
"node-sass": "3.13.1",
"precommit-hook": "3.0.0",
"string-replace-loader": "1.0.5",
"style-loader": "0.13.2",
"string-replace-loader": "1.1.0",
"style-loader": "0.16.1",
"webpack": "1.14.0",
"webpack-dev-server": "1.16.3"
},

View File

@@ -2,11 +2,11 @@
import { Linking } from 'react-native';
import '../../audio-mode';
import '../../background';
import { Platform } from '../../base/react';
import '../../full-screen';
import '../../wake-lock';
import '../../mobile/audio-mode';
import '../../mobile/background';
import '../../mobile/full-screen';
import '../../mobile/wake-lock';
import { AbstractApp } from './AbstractApp';
@@ -47,8 +47,8 @@ export class App extends AbstractApp {
* by this app.
*
* @inheritdoc
* @see https://facebook.github.io/react-native/docs/linking.html
* @returns {void}
* @see https://facebook.github.io/react-native/docs/linking.html
*/
componentWillMount() {
super.componentWillMount();
@@ -61,8 +61,8 @@ export class App extends AbstractApp {
* handled by this app.
*
* @inheritdoc
* @see https://facebook.github.io/react-native/docs/linking.html
* @returns {void}
* @see https://facebook.github.io/react-native/docs/linking.html
*/
componentWillUnmount() {
Linking.removeEventListener('url', this._onLinkingURL);

View File

@@ -0,0 +1,22 @@
import { Symbol } from '../react';
/**
* The type of Redux action which closes a dialog
*
* {
* type: HIDE_DIALOG
* }
*/
export const HIDE_DIALOG = Symbol('HIDE_DIALOG');
/**
* The type of Redux action which begins a request to open a dialog.
*
* {
* type: OPEN_DIALOG,
* component: React.Component,
* props: React.PropTypes.object
* }
*
*/
export const OPEN_DIALOG = Symbol('OPEN_DIALOG');

View File

@@ -0,0 +1,54 @@
import {
HIDE_DIALOG,
OPEN_DIALOG
} from './actionTypes';
/**
* Signals Dialog to close its dialog.
*
* @returns {{
* type: HIDE_DIALOG
* }}
*/
export function hideDialog() {
return {
type: HIDE_DIALOG
};
}
/**
* Signals Dialog to open dialog.
*
* @param {Object} component - The component to display as dialog.
* @param {Object} componentProps - The properties needed for that component.
* @returns {Object}
*/
export function openDialog(component, componentProps) {
return {
type: OPEN_DIALOG,
component,
componentProps
};
}
/**
* Signals Dialog to open a dialog with the specified component if the component
* is not already open. If it is open, then Dialog is signaled to close
* its dialog.
*
* @param {Object} component - The component to display as dialog.
* @param {Object} componentProps - The properties needed for that component.
* @returns {Object}
*/
export function toggleDialog(component, componentProps) {
return (dispatch, getState) => {
const state = getState();
const dialogState = state['features/base/dialog'];
if (dialogState.component === component) {
dispatch(hideDialog());
} else {
dispatch(openDialog(component, componentProps));
}
};
}

View File

@@ -0,0 +1,110 @@
import React, { Component } from 'react';
import { hideDialog } from '../actions';
/**
* Abstract dialog to display dialogs.
*/
export default class AbstractDialog extends Component {
/**
* Abstract Dialog component's property types.
*
* @static
*/
static propTypes = {
/**
* Whether cancel button is disabled. Enabled by default.
*/
cancelDisabled: React.PropTypes.bool,
/**
* Optional i18n key to change the cancel button title.
*/
cancelTitleKey: React.PropTypes.string,
/**
* Used to show hide the dialog on cancel.
*/
dispatch: React.PropTypes.func,
/**
* Is ok button enabled/disabled. Enabled by default.
*/
okDisabled: React.PropTypes.bool,
/**
* Optional i18n key to change the ok button title.
*/
okTitleKey: React.PropTypes.string,
/**
* The handler for onCancel event.
*/
onCancel: React.PropTypes.func,
/**
* The handler for the event when submitting the dialog.
*/
onSubmit: React.PropTypes.func,
/**
* Used to obtain translations in children classes.
*/
t: React.PropTypes.func,
/**
* Key to use for showing a title.
*/
titleKey: React.PropTypes.string
}
/**
* Initializes a new Dialog instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Dispatches action to hide the dialog.
*
* @returns {void}
*/
_onCancel() {
let hide = true;
if (this.props.onCancel) {
hide = this.props.onCancel();
}
if (hide) {
this.props.dispatch(hideDialog());
}
}
/**
* Dispatches the action when submitting the dialog.
*
* @private
* @param {string} value - The submitted value if any.
* @returns {void}
*/
_onSubmit(value) {
let hide = true;
if (this.props.onSubmit) {
hide = this.props.onSubmit(value);
}
if (hide) {
this.props.dispatch(hideDialog());
}
}
}

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { connect } from 'react-redux';
import Prompt from 'react-native-prompt';
import { translate } from '../../i18n';
import AbstractDialog from './AbstractDialog';
/**
* Native dialog using Prompt.
*/
class Dialog extends AbstractDialog {
/**
* Native sialog component's property types.
*
* @static
*/
static propTypes = {
/**
* I18n key to put as body title.
*/
bodyKey: React.PropTypes.string
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
cancelDisabled,
cancelTitleKey,
bodyKey,
okDisabled,
okTitleKey,
t,
titleKey
} = this.props;
return (
<Prompt
cancelText = { cancelDisabled
? undefined : t(cancelTitleKey || 'dialog.Cancel') }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
placeholder = { t(bodyKey) }
submitText = { okDisabled
? undefined : t(okTitleKey || 'dialog.Ok') }
title = { t(titleKey) }
visible = { true } />
);
}
}
export default translate(connect()(Dialog));

View File

@@ -0,0 +1,159 @@
import AKButton from '@atlaskit/button';
import AKButtonGroup from '@atlaskit/button-group';
import ModalDialog from '@atlaskit/modal-dialog';
import React from 'react';
import { connect } from 'react-redux';
import { translate } from '../../i18n';
import AbstractDialog from './AbstractDialog';
/**
* Web dialog that uses atlaskit modal-dialog to display dialogs.
*/
class Dialog extends AbstractDialog {
/**
* Web dialog component's property types.
*
* @static
*/
static propTypes = {
/**
* This is the body of the dialog, the component children.
*/
children: React.PropTypes.node,
/**
* Whether the dialog is modal. This means clicking on the blanket will
* leave the dialog open. No cancel button.
*/
isModal: React.PropTypes.bool,
/**
* Width of the dialog, can be:
* - 'small' (400px), 'medium' (600px), 'large' (800px),
* 'x-large' (968px)
* - integer value for pixel width
* - string value for percentage
*/
width: React.PropTypes.string
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<ModalDialog
footer = { this._renderFooter() }
header = { this._renderHeader() }
isOpen = { true }
onDialogDismissed = { this._onCancel }
width = { this.props.width || 'medium' }>
<div>
<form
id = 'modal-dialog-form'
onSubmit = { this._onSubmit }>
{ this.props.children }
</form>
</div>
</ModalDialog>);
}
/**
* Render cancel button.
*
* @returns {*} The cancel button if enabled and dialog is not modal.
* @private
*/
_renderCancelButton() {
if (this.props.cancelDisabled || this.props.isModal) {
return null;
}
return (
<AKButton
appearance = 'subtle'
id = 'modal-dialog-cancel-button'
onClick = { this._onCancel }>
{ this.props.t(this.props.cancelTitleKey || 'dialog.Cancel') }
</AKButton>
);
}
/**
* Render component in dialog footer.
*
* @returns {ReactElement}
* @private
*/
_renderFooter() {
return (
<footer>
<AKButtonGroup>
{ this._renderCancelButton() }
{ this._renderOKButton() }
</AKButtonGroup>
</footer>
);
}
/**
* Render component in dialog header.
*
* @returns {ReactElement}
* @private
*/
_renderHeader() {
const { t } = this.props;
return (
<header>
<h2>
{ t(this.props.titleKey) }
</h2>
</header>
);
}
/**
* Render ok button.
*
* @returns {*} The ok button if enabled.
* @private
*/
_renderOKButton() {
if (this.props.submitDisabled) {
return null;
}
return (
<AKButton
appearance = 'primary'
form = 'modal-dialog-form'
id = 'modal-dialog-ok-button'
onClick = { this._onSubmit }>
{ this.props.t(this.props.okTitleKey || 'dialog.Ok') }
</AKButton>
);
}
/**
* Dispatches action to hide the dialog.
*
* @returns {void}
*/
_onCancel() {
if (this.props.isModal) {
return;
}
super._onCancel();
}
}
export default translate(connect()(Dialog));

View File

@@ -0,0 +1,61 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
/**
* Implements a DialogContainer that will be responsible for
* showing all dialogs. We will need a separate container so we can handle
* multiple dialogs, showing them simultaneously or queueing them.
*/
export class DialogContainer extends Component {
/**
* DialogContainer component's property types.
*
* @static
*/
static propTypes = {
/**
* The component to render.
*/
_component: React.PropTypes.func,
/**
* The props to pass to the component that will be rendered.
*/
_componentProps: React.PropTypes.object
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
if (!this.props._component) {
return null;
}
return React.createElement(
this.props._component, this.props._componentProps);
}
}
/**
* Maps (parts of) the Redux state to the associated Dialog's props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _component: React.Component,
* _props: React.PropTypes.object
* }}
*/
function _mapStateToProps(state) {
return {
_component: state['features/base/dialog'].component,
_componentProps: state['features/base/dialog'].componentProps
};
}
export default connect(_mapStateToProps)(DialogContainer);

View File

@@ -0,0 +1,2 @@
export { default as DialogContainer } from './DialogContainer';
export { default as Dialog } from './Dialog';

View File

@@ -0,0 +1,4 @@
export * from './actions';
export * from './components';
import './reducer';

View File

@@ -0,0 +1,31 @@
import { ReducerRegistry, setStateProperties } from '../redux';
import {
HIDE_DIALOG,
OPEN_DIALOG
} from './actionTypes';
/**
* Listen for actions which show or hide dialogs.
*
* @param {Object[]} state - Current state.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @returns {{}}
*/
ReducerRegistry.register('features/base/dialog', (state = {}, action) => {
switch (action.type) {
case HIDE_DIALOG:
return setStateProperties(state, {
component: undefined,
componentProps: undefined
});
case OPEN_DIALOG:
return setStateProperties(state, {
component: action.component,
componentProps: action.componentProps
});
}
return state;
});

View File

@@ -65,9 +65,9 @@ export class Video extends Component {
componentDidMount() {
// RTCView currently does not support media events, so just fire
// onPlaying callback when <RTCView> is rendered.
if (this.props.onPlaying) {
this.props.onPlaying();
}
const { onPlaying } = this.props;
onPlaying && onPlaying();
}
/**
@@ -77,7 +77,7 @@ export class Video extends Component {
* @returns {ReactElement|null}
*/
render() {
const stream = this.props.stream;
const { stream } = this.props;
if (stream) {
const streamURL = stream.toURL();
@@ -91,18 +91,18 @@ export class Video extends Component {
const style = styles.video;
const objectFit = (style && style.objectFit) || 'cover';
const mirror = this.props.mirror;
const { mirror } = this.props;
// XXX RTCView doesn't currently support mirroring, even when
// providing a transform style property. As a workaround, wrap the
// RTCView inside another View and apply the transform style
// property to that View instead.
// XXX RTCView may not support support mirroring, even when
// providing a transform style property (e.g. iOS) . As a
// workaround, wrap the RTCView inside another View and apply the
// transform style property to that View instead.
const mirrorWorkaround = mirror && !RTCVIEW_SUPPORTS_MIRROR;
// eslint-disable-next-line no-extra-parens
const video = (
<RTCView
mirror = { !mirrorWorkaround }
mirror = { mirrorWorkaround ? false : mirror }
objectFit = { objectFit }
streamURL = { streamURL }
style = { style }

View File

@@ -1,2 +1 @@
export * from './_';
export * from './styles';

View File

@@ -0,0 +1,29 @@
/**
* Counts how many whole hours are included in the given time total.
*
* @param {number} milliseconds - The millisecond total to get hours from.
* @returns {number}
*/
export function getHoursCount(milliseconds) {
return Math.floor(milliseconds / (60 * 60 * 1000));
}
/**
* Counts how many whole minutes are included in the given time total.
*
* @param {number} milliseconds - The millisecond total to get minutes from.
* @returns {number}
*/
export function getMinutesCount(milliseconds) {
return Math.floor(milliseconds / (60 * 1000) % 60);
}
/**
* Counts how many whole seconds are included in the given time total.
*
* @param {number} milliseconds - The millisecond total to get seconds from.
* @returns {number}
*/
export function getSecondsCount(milliseconds) {
return Math.floor(milliseconds / 1000 % 60);
}

View File

@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { connect as reactReduxConnect } from 'react-redux';
import { connect, disconnect } from '../../base/connection';
import { DialogContainer } from '../../base/dialog';
import { Container } from '../../base/react';
import { FilmStrip } from '../../film-strip';
import { LargeVideo } from '../../large-video';
@@ -125,6 +126,8 @@ class Conference extends Component {
<Toolbar visible = { toolbarVisible } />
<FilmStrip visible = { !toolbarVisible } />
<DialogContainer />
{
this._renderPrompt()
}

View File

@@ -4,26 +4,16 @@ import React, { Component } from 'react';
import { connect as reactReduxConnect } from 'react-redux';
import { connect, disconnect } from '../../base/connection';
import { DialogContainer } from '../../base/dialog';
import { Watermarks } from '../../base/react';
import { FeedbackButton } from '../../feedback';
import { OverlayContainer } from '../../overlay';
import { Notice } from '../../toolbar';
import { HideNotificationBarStyle } from '../../unsupported-browser';
declare var $: Function;
declare var APP: Object;
/**
* For legacy reasons, inline style for display none.
*
* @private
* @type {{
* display: string
* }}
*/
const _DISPLAY_NONE_STYLE = {
display: 'none'
};
/**
* The conference page of the Web application.
*/
@@ -77,14 +67,8 @@ class Conference extends Component {
return (
<div id = 'videoconference_page'>
<div id = 'mainToolbarContainer'>
<div
className = 'notice'
id = 'notice'
style = { _DISPLAY_NONE_STYLE }>
<span
className = 'noticeText'
id = 'noticeText' />
</div>
<Notice />
<div
className = 'toolbar'
id = 'mainToolbar' />
@@ -170,7 +154,7 @@ class Conference extends Component {
</div>
</div>
</div>
<DialogContainer />
<OverlayContainer />
<HideNotificationBarStyle />
</div>

View File

@@ -0,0 +1,20 @@
import { Symbol } from '../base/react';
/**
* Action to remove known DesktopCapturerSources.
*
* {
* type: RESET_DESKTOP_SOURCES,
* }
*/
export const RESET_DESKTOP_SOURCES = Symbol('RESET_DESKTOP_SOURCES');
/**
* Action to replace stored DesktopCapturerSources with new sources.
*
* {
* type: UPDATE_DESKTOP_SOURCES,
* sources: {Array}
* }
*/
export const UPDATE_DESKTOP_SOURCES = Symbol('UPDATE_DESKTOP_SOURCES');

View File

@@ -0,0 +1,87 @@
import { getLogger } from 'jitsi-meet-logger';
import { openDialog } from '../base/dialog';
import {
RESET_DESKTOP_SOURCES,
UPDATE_DESKTOP_SOURCES
} from './actionTypes';
import { DesktopPicker } from './components';
const logger = getLogger(__filename);
/**
* Signals to remove all stored DesktopCapturerSources.
*
* @returns {{
* type: RESET_DESKTOP_SOURCES
* }}
*/
export function resetDesktopSources() {
return {
type: RESET_DESKTOP_SOURCES
};
}
/**
* Begins a request to get available DesktopCapturerSources.
*
* @param {Array} types - An array with DesktopCapturerSource type strings.
* @param {Object} options - Additional configuration for getting a list
* of sources.
* @param {Object} options.thumbnailSize - The desired height and width
* of the return native image object used for the preview image of the source.
* @returns {Function}
*/
export function obtainDesktopSources(types, options = {}) {
const capturerOptions = {
types
};
if (options.thumbnailSize) {
capturerOptions.thumbnailSize = options.thumbnailSize;
}
return dispatch => {
if (window.JitsiMeetElectron
&& window.JitsiMeetElectron.obtainDesktopStreams) {
window.JitsiMeetElectron.obtainDesktopStreams(
sources => dispatch(updateDesktopSources(sources)),
error => logger.error(
`Error while obtaining desktop sources: ${error}`),
capturerOptions
);
} else {
logger.error('Called JitsiMeetElectron.obtainDesktopStreams '
+ 'but it is not defined');
}
};
}
/**
* Signals to open a dialog with the DesktopPicker component.
*
* @param {Function} onSourceChoose - The callback to invoke when
* a DesktopCapturerSource has been chosen.
* @returns {Object}
*/
export function showDesktopPicker(onSourceChoose) {
return openDialog(DesktopPicker, {
onSourceChoose
});
}
/**
* Signals new DesktopCapturerSources have been received.
*
* @param {Object} sources - Arrays with DesktopCapturerSources.
* @returns {{
* type: UPDATE_DESKTOP_SOURCES,
* sources: Array
* }}
*/
export function updateDesktopSources(sources) {
return {
type: UPDATE_DESKTOP_SOURCES,
sources
};
}

View File

@@ -0,0 +1,264 @@
/* global config */
import Tabs from '@atlaskit/tabs';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import {
resetDesktopSources,
obtainDesktopSources
} from '../actions';
import DesktopPickerPane from './DesktopPickerPane';
const updateInterval = 1000;
const thumbnailSize = {
height: 300,
width: 300
};
const tabConfigurations = [
{
label: 'dialog.yourEntireScreen',
type: 'screen',
isDefault: true
},
{
label: 'dialog.applicationWindow',
type: 'window'
}
];
const validTypes = tabConfigurations.map(configuration => configuration.type);
const configuredTypes = config.desktopSharingChromeSources || [];
const tabsToPopulate = tabConfigurations.filter(configuration =>
configuredTypes.includes(configuration.type)
&& validTypes.includes(configuration.type)
);
const typesToFetch = tabsToPopulate.map(configuration => configuration.type);
/**
* React component for DesktopPicker.
*
* @extends Component
*/
class DesktopPicker extends Component {
/**
* DesktopPicker component's property types.
*
* @static
*/
static propTypes = {
/**
* Used to request DesktopCapturerSources.
*/
dispatch: React.PropTypes.func,
/**
* The callback to be invoked when the component is closed or
* when a DesktopCapturerSource has been chosen.
*/
onSourceChoose: React.PropTypes.func,
/**
* An object with arrays of DesktopCapturerSources. The key
* should be the source type.
*/
sources: React.PropTypes.object,
/**
* Used to obtain translations.
*/
t: React.PropTypes.func
}
/**
* Initializes a new DesktopPicker instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
selectedSourceId: ''
};
this._poller = null;
this._onCloseModal = this._onCloseModal.bind(this);
this._onPreviewClick = this._onPreviewClick.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._updateSources = this._updateSources.bind(this);
}
/**
* Perform an immediate update request for DesktopCapturerSources and
* begin requesting updates at an interval.
*
* @inheritdoc
*/
componentWillMount() {
this._updateSources();
this._startPolling();
}
/**
* Clean up component and DesktopCapturerSource store state.
*
* @inheritdoc
*/
componentWillUnmount() {
this._stopPolling();
this.props.dispatch(resetDesktopSources());
}
/**
* Notifies this mounted React Component that it will receive new props.
* Sets a default selected source if one is not already set.
*
* @inheritdoc
* @param {Object} nextProps - The read-only React Component props that this
* instance will receive.
* @returns {void}
*/
componentWillReceiveProps(nextProps) {
if (!this.state.selectedSourceId
&& nextProps.sources.screen.length) {
this.setState({ selectedSourceId: nextProps.sources.screen[0].id });
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<Dialog
isModal = { false }
okTitleKey = 'dialog.Share'
onCancel = { this._onCloseModal }
onSubmit = { this._onSubmit }
titleKey = 'dialog.shareYourScreen'
width = 'medium' >
{ this._renderTabs() }
</Dialog>);
}
/**
* Dispatches an action to get currently available DesktopCapturerSources.
*
* @private
* @returns {void}
*/
_updateSources() {
this.props.dispatch(obtainDesktopSources(
typesToFetch,
{
thumbnailSize
}
));
}
/**
* Create an interval to update knwon available DesktopCapturerSources.
*
* @private
* @returns {void}
*/
_startPolling() {
this._stopPolling();
this._poller = window.setInterval(this._updateSources,
updateInterval);
}
/**
* Cancels the interval to update DesktopCapturerSources.
*
* @private
* @returns {void}
*/
_stopPolling() {
window.clearInterval(this._poller);
this._poller = null;
}
/**
* Sets the currently selected DesktopCapturerSource.
*
* @param {string} id - The id of DesktopCapturerSource.
* @returns {void}
*/
_onPreviewClick(id) {
this.setState({ selectedSourceId: id });
}
/**
* Request to close the modal and execute callbacks
* with the selected source id.
*
* @returns {void}
*/
_onSubmit() {
this._onCloseModal(this.state.selectedSourceId);
}
/**
* Dispatches an action to hide the DesktopPicker and invokes
* the passed in callback with a selectedSourceId, if any.
*
* @param {string} id - The id of the DesktopCapturerSource to pass into
* the onSourceChoose callback.
* @returns {void}
*/
_onCloseModal(id = '') {
this.props.onSourceChoose(id);
this.props.dispatch(hideDialog());
}
/**
* Configures and renders the tabs for display.
*
* @returns {ReactElement}
* @private
*/
_renderTabs() {
const tabs = tabsToPopulate.map(tabConfig => {
const type = tabConfig.type;
return {
label: this.props.t(tabConfig.label),
defaultSelected: tabConfig.isDefault,
content: <DesktopPickerPane
key = { type }
onClick = { this._onPreviewClick }
onDoubleClick = { this._onCloseModal }
selectedSourceId = { this.state.selectedSourceId }
sources = { this.props.sources[type] || [] }
type = { type } />
};
});
return <Tabs tabs = { tabs } />;
}
}
/**
* Maps (parts of) the Redux state to the associated DesktopPicker's props.
*
* @param {Object} state - Redux state.
* @protected
* @returns {{
* sources: Object
* }}
*/
function mapStateToProps(state) {
return {
sources: state['features/desktop-picker/sources']
};
}
export default translate(connect(mapStateToProps)(DesktopPicker));

View File

@@ -0,0 +1,69 @@
import React, { Component } from 'react';
import DesktopSourcePreview from './DesktopSourcePreview';
/**
* React component for showing a grid of DesktopSourcePreviews.
*
* @extends Component
*/
class DesktopPickerPane extends Component {
/**
* DesktopPickerPane component's property types.
*
* @static
*/
static propTypes = {
/**
* The handler to be invoked when a DesktopSourcePreview is clicked.
*/
onClick: React.PropTypes.func,
/**
* The handler to be invoked when a DesktopSourcePreview is
* double clicked.
*/
onDoubleClick: React.PropTypes.func,
/**
* The id of the DesktopCapturerSource that is currently selected.
*/
selectedSourceId: React.PropTypes.string,
/**
* An array of DesktopCapturerSources.
*/
sources: React.PropTypes.array,
/**
* The source type of the DesktopCapturerSources to display.
*/
type: React.PropTypes.string
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const previews = this.props.sources.map(source =>
<DesktopSourcePreview
isSelected = { source.id === this.props.selectedSourceId }
key = { source.id }
onClick = { this.props.onClick }
onDoubleClick = { this.props.onDoubleClick }
source = { source } />
);
const classnames = 'desktop-picker-pane default-scrollbar '
+ `source-type-${this.props.type}`;
return (
<div className = { classnames }>
{ previews }
</div>
);
}
}
export default DesktopPickerPane;

View File

@@ -0,0 +1,97 @@
import React, { Component } from 'react';
/**
* React component for displaying a preview of a DesktopCapturerSource.
*
* @extends Component
*/
class DesktopSourcePreview extends Component {
/**
* DesktopSourcePreview component's property types.
*
* @static
*/
static propTypes = {
/**
* If true the 'is-selected' class will be added to the component.
*/
isSelected: React.PropTypes.bool,
/**
* The callback to invoke when the component is clicked.
* The id of the DesktopCapturerSource will be passed in.
*/
onClick: React.PropTypes.func,
/**
* The callback to invoke when the component is double clicked.
* The id of the DesktopCapturerSource will be passed in.
*/
onDoubleClick: React.PropTypes.func,
/**
* The DesktopCapturerSource to display.
*/
source: React.PropTypes.object
}
/**
* Initializes a new DesktopSourcePreview instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this._onClick = this._onClick.bind(this);
this._onDoubleClick = this._onDoubleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const isSelectedClass = this.props.isSelected ? 'is-selected' : '';
const displayClasses = `desktop-picker-source ${isSelectedClass}`;
return (
<div
className = { displayClasses }
onClick = { this._onClick }
onDoubleClick = { this._onDoubleClick }>
<div className = 'desktop-source-preview-image-container'>
<img
className = 'desktop-source-preview-thumbnail'
src = { this.props.source.thumbnail.toDataURL() } />
</div>
<div className = 'desktop-source-preview-label'>
{ this.props.source.name }
</div>
</div>
);
}
/**
* Invokes the passed in onClick callback.
*
* @returns {void}
*/
_onClick() {
this.props.onClick(this.props.source.id);
}
/**
* Invokes the passed in onDoubleClick callback.
*
* @returns {void}
*/
_onDoubleClick() {
this.props.onDoubleClick(this.props.source.id);
}
}
export default DesktopSourcePreview;

View File

@@ -0,0 +1 @@
export { default as DesktopPicker } from './DesktopPicker';

View File

@@ -0,0 +1,5 @@
export * from './actionTypes';
export * from './actions';
export * from './components';
import './reducer';

View File

@@ -0,0 +1,59 @@
import { ReducerRegistry } from '../base/redux';
import {
RESET_DESKTOP_SOURCES,
UPDATE_DESKTOP_SOURCES
} from './actionTypes';
const defaultState = {
screen: [],
window: []
};
/**
* Listen for actions that mutate the known available DesktopCapturerSources.
*
* @param {Object[]} state - Current state.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @param {Array} action.sources - DesktopCapturerSources.
* @returns {Object}
*/
ReducerRegistry.register(
'features/desktop-picker/sources',
(state = defaultState, action) => {
switch (action.type) {
case RESET_DESKTOP_SOURCES:
return { ...defaultState };
case UPDATE_DESKTOP_SOURCES:
return seperateSourcesByType(action.sources);
default:
return state;
}
});
/**
* Converts an array of DesktopCapturerSources to an object with types
* for keys and values being an array with sources of the key's type.
*
* @param {Array} sources - DesktopCapturerSources.
* @returns {Object} An object with the sources split into seperate arrays
* based on source type.
* @private
*/
function seperateSourcesByType(sources = []) {
const sourcesByType = {
screen: [],
window: []
};
sources.forEach(source => {
const sourceIdParts = source.id.split(':');
const sourceType = sourceIdParts[0];
if (sourcesByType[sourceType]) {
sourcesByType[sourceType].push(source);
}
});
return sourcesByType;
}

View File

@@ -2,13 +2,13 @@
import { NativeModules } from 'react-native';
import { APP_WILL_MOUNT } from '../app';
import { APP_WILL_MOUNT } from '../../app';
import {
CONFERENCE_FAILED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN
} from '../base/conference';
import { MiddlewareRegistry } from '../base/redux';
} from '../../base/conference';
import { MiddlewareRegistry } from '../../base/redux';
/**
* Middleware that captures conference actions and sets the correct audio mode

View File

@@ -1,4 +1,4 @@
import { Symbol } from '../base/react';
import { Symbol } from '../../base/react';
/**
* The type of redux action to set the AppState API change event listener.
@@ -10,8 +10,7 @@ import { Symbol } from '../base/react';
*
* @protected
*/
export const _SET_APP_STATE_LISTENER
= Symbol('_SET_APP_STATE_LISTENER');
export const _SET_APP_STATE_LISTENER = Symbol('_SET_APP_STATE_LISTENER');
/**
* The type of redux action which signals that video will be muted because the
@@ -27,6 +26,18 @@ export const _SET_APP_STATE_LISTENER
export const _SET_BACKGROUND_VIDEO_MUTED
= Symbol('_SET_BACKGROUND_VIDEO_MUTED');
/**
* The type of redux action which sets the video channel's lastN (value).
*
* {
* type: _SET_LASTN,
* lastN: boolean
* }
*
* @protected
*/
export const _SET_LASTN = Symbol('_SET_LASTN');
/**
* The type of redux action which signals that the app state has changed (in
* terms of execution mode). The app state can be one of 'active', 'inactive',

View File

@@ -1,30 +1,12 @@
import { setVideoMuted } from '../base/media';
import { setVideoMuted } from '../../base/media';
import {
_SET_APP_STATE_LISTENER,
_SET_BACKGROUND_VIDEO_MUTED,
_SET_LASTN,
APP_STATE_CHANGED
} from './actionTypes';
/**
* Signals that the App state has changed (in terms of execution state). The
* application can be in 3 states: 'active', 'inactive' and 'background'.
*
* @param {string} appState - The new App state.
* @public
* @returns {{
* type: APP_STATE_CHANGED,
* appState: string
* }}
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
*/
export function appStateChanged(appState: string) {
return {
type: APP_STATE_CHANGED,
appState
};
}
/**
* Sets the listener to be used with React Native's AppState API.
*
@@ -54,24 +36,49 @@ export function _setAppStateListener(listener: ?Function) {
*/
export function _setBackgroundVideoMuted(muted: boolean) {
return (dispatch, getState) => {
if (muted) {
const mediaState = getState()['features/base/media'];
// Disable remote video when we mute by setting lastN to 0.
// Skip it if the conference is in audio only mode, as it's
// already configured to have no video.
const { audioOnly } = getState()['features/base/conference'];
if (mediaState.video.muted) {
if (!audioOnly) {
let lastN;
if (muted) {
lastN = 0;
} else {
const { config } = getState()['features/base/lib-jitsi-meet'];
lastN = config.channelLastN;
if (typeof lastN === 'undefined') {
lastN = -1;
}
}
dispatch({
type: _SET_LASTN,
lastN
});
}
if (muted) {
const { video } = getState()['features/base/media'];
if (video.muted) {
// Video is already muted, do nothing.
return;
}
} else {
const bgState = getState()['features/background'];
const { videoMuted } = getState()['features/background'];
if (!bgState.videoMuted) {
if (!videoMuted) {
// We didn't mute video, do nothing.
return;
}
}
// Remember that video was muted due to the app going to the background
// vs user's choice.
// Remember that local video was muted due to the app going to the
// background vs user's choice.
dispatch({
type: _SET_BACKGROUND_VIDEO_MUTED,
muted
@@ -79,3 +86,22 @@ export function _setBackgroundVideoMuted(muted: boolean) {
dispatch(setVideoMuted(muted));
};
}
/**
* Signals that the App state has changed (in terms of execution state). The
* application can be in 3 states: 'active', 'inactive' and 'background'.
*
* @param {string} appState - The new App state.
* @public
* @returns {{
* type: APP_STATE_CHANGED,
* appState: string
* }}
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
*/
export function appStateChanged(appState: string) {
return {
type: APP_STATE_CHANGED,
appState
};
}

View File

@@ -6,8 +6,8 @@ import type { Dispatch } from 'redux';
import {
APP_WILL_MOUNT,
APP_WILL_UNMOUNT
} from '../app';
import { MiddlewareRegistry } from '../base/redux';
} from '../../app';
import { MiddlewareRegistry } from '../../base/redux';
import {
_setAppStateListener,
@@ -16,6 +16,7 @@ import {
} from './actions';
import {
_SET_APP_STATE_LISTENER,
_SET_LASTN,
APP_STATE_CHANGED
} from './actionTypes';
@@ -46,6 +47,20 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case _SET_LASTN: {
const { conference } = store.getState()['features/base/conference'];
if (conference) {
try {
conference.setLastN(action.lastN);
} catch (err) {
console.warn(`Failed to set lastN: ${err}`);
}
}
break;
}
case APP_STATE_CHANGED:
_appStateChanged(store.dispatch, action.appState);
break;

View File

@@ -1,4 +1,4 @@
import { ReducerRegistry } from '../base/redux';
import { ReducerRegistry } from '../../base/redux';
import {
_SET_APP_STATE_LISTENER,

View File

@@ -8,9 +8,9 @@ import {
CONFERENCE_FAILED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN
} from '../base/conference';
import { Platform } from '../base/react';
import { MiddlewareRegistry } from '../base/redux';
} from '../../base/conference';
import { Platform } from '../../base/react';
import { MiddlewareRegistry } from '../../base/redux';
/**
* Middleware that captures conference actions and activates or deactivates the

View File

@@ -4,8 +4,8 @@ import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference';
import { MiddlewareRegistry } from '../base/redux';
} from '../../base/conference';
import { MiddlewareRegistry } from '../../base/redux';
/**
* Middleware that captures conference actions and activates or deactivates the

View File

@@ -1,80 +0,0 @@
/* global APP */
import React, { Component } from 'react';
/**
* Implements an abstract React Component for overlay - the components which are
* displayed on top of the application covering the whole screen.
*
* @abstract
*/
export default class AbstractOverlay extends Component {
/**
* Initializes a new AbstractOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props) {
super(props);
this.state = {
/**
* Indicates the CSS style of the overlay. If true, then ighter;
* darker, otherwise.
*
* @type {boolean}
*/
isLightOverlay: false
};
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
const containerClass
= this.state.isLightOverlay
? 'overlay__container-light'
: 'overlay__container';
return (
<div
className = { containerClass }
id = 'overlay'>
<div className = 'overlay__content'>
{
this._renderOverlayContent()
}
</div>
</div>
);
}
/**
* Reloads the page.
*
* @returns {void}
* @protected
*/
_reconnectNow() {
// FIXME: In future we should dispatch an action here that will result
// in reload.
APP.ConferenceUrl.reload();
}
/**
* Abstract method which should be used by subclasses to provide the overlay
* content.
*
* @returns {ReactElement|null}
* @protected
*/
_renderOverlayContent() {
return null;
}
}

View File

@@ -0,0 +1,192 @@
import React, { Component } from 'react';
import { randomInt } from '../../base/util';
import { reconnectNow } from '../functions';
import ReloadButton from './ReloadButton';
declare var AJS: Object;
declare var APP: Object;
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Implements abstract React Component for the page reload overlays.
*/
export default class AbstractPageReloadOverlay extends Component {
/**
* AbstractPageReloadOverlay component's property types.
*
* @static
*/
static propTypes = {
/**
* The indicator which determines whether the reload was caused by
* network failure.
*
* @public
* @type {boolean}
*/
isNetworkFailure: React.PropTypes.bool,
/**
* The reason for the error that will cause the reload.
* NOTE: Used by PageReloadOverlay only.
*
* @public
* @type {string}
*/
reason: React.PropTypes.string
}
/**
* Initializes a new AbstractPageReloadOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props) {
super(props);
/**
* How long the overlay dialog will be displayed, before the conference
* will be reloaded.
*
* @type {number}
*/
const timeoutSeconds = 10 + randomInt(0, 20);
let message, title;
if (this.props.isNetworkFailure) {
title = 'dialog.conferenceDisconnectTitle';
message = 'dialog.conferenceDisconnectMsg';
} else {
title = 'dialog.conferenceReloadTitle';
message = 'dialog.conferenceReloadMsg';
}
this.state = {
/**
* The translation key for the title of the overlay.
*
* @type {string}
*/
message,
/**
* Current value(time) of the timer.
*
* @type {number}
*/
timeLeft: timeoutSeconds,
/**
* How long the overlay dialog will be displayed before the
* conference will be reloaded.
*
* @type {number}
*/
timeoutSeconds,
/**
* The translation key for the title of the overlay.
*
* @type {string}
*/
title
};
}
/**
* Renders the button for relaod the page if necessary.
*
* @returns {ReactElement|null}
* @private
*/
_renderButton() {
if (this.props.isNetworkFailure) {
return (
<ReloadButton textKey = 'dialog.rejoinNow' />
);
}
return null;
}
/**
* Renders the progress bar.
*
* @returns {ReactElement|null}
* @protected
*/
_renderProgressBar() {
return (
<div
className = 'aui-progress-indicator'
id = 'reloadProgressBar'>
<span className = 'aui-progress-indicator-value' />
</div>
);
}
/**
* React Component method that executes once component is mounted.
*
* @inheritdoc
* @returns {void}
* @protected
*/
componentDidMount() {
// FIXME (CallStats - issue) This event will not make it to CallStats
// because the log queue is not flushed before "fabric terminated" is
// sent to the backed.
// FIXME: We should dispatch action for this.
APP.conference.logEvent(
'page.reload',
/* value */ undefined,
/* label */ this.props.reason);
logger.info(
'The conference will be reloaded after '
+ `${this.state.timeoutSeconds} seconds.`);
AJS.progressBars.update('#reloadProgressBar', 0);
this.intervalId = setInterval(() => {
if (this.state.timeLeft === 0) {
clearInterval(this.intervalId);
reconnectNow();
} else {
this.setState(prevState => {
return {
timeLeft: prevState.timeLeft - 1
};
});
}
}, 1000);
}
/**
* React Component method that executes once component is updated.
*
* @inheritdoc
* @returns {void}
* @protected
*/
componentDidUpdate() {
AJS.progressBars.update('#reloadProgressBar',
(this.state.timeoutSeconds - this.state.timeLeft)
/ this.state.timeoutSeconds);
}
/**
* Clears the timer interval.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
clearInterval(this.intervalId);
}
}

View File

@@ -0,0 +1,133 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
Avatar,
getAvatarURL,
getLocalParticipant
} from '../../base/participants';
import OverlayFrame from './OverlayFrame';
/**
* Implements a React Component for the frame of the overlays in filmstrip only
* mode.
*/
class FilmStripOnlyOverlayFrame extends Component {
/**
* FilmStripOnlyOverlayFrame component's property types.
*
* @static
*/
static propTypes = {
/**
* The source (e.g. URI, URL) of the avatar image of the local
* participant.
*
* @private
*/
_avatar: React.PropTypes.string,
/**
* The children components to be displayed into the overlay frame for
* filmstrip only mode.
*
* @type {ReactElement}
*/
children: React.PropTypes.node.isRequired,
/**
* The css class name for the icon that will be displayed over the
* avatar.
*
* @type {string}
*/
icon: React.PropTypes.string,
/**
* Indicates the css style of the overlay. If true, then lighter;
* darker, otherwise.
*
* @type {boolean}
*/
isLightOverlay: React.PropTypes.bool
}
/**
* Renders content related to the icon.
*
* @returns {ReactElement|null}
* @private
*/
_renderIcon() {
if (!this.props.icon) {
return null;
}
const iconClass = `inlay-filmstrip-only__icon ${this.props.icon}`;
const iconBGClass = 'inlay-filmstrip-only__icon-background';
return (
<div>
<div className = { iconBGClass } />
<div className = 'inlay-filmstrip-only__icon-container'>
<span className = { iconClass } />
</div>
</div>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
return (
<OverlayFrame isLightOverlay = { this.props.isLightOverlay }>
<div className = 'inlay-filmstrip-only'>
<div className = 'inlay-filmstrip-only__content'>
{
this.props.children
}
</div>
<div className = 'inlay-filmstrip-only__avatar-container'>
<Avatar uri = { this.props._avatar } />
{
this._renderIcon()
}
</div>
</div>
</OverlayFrame>
);
}
}
/**
* Maps (parts of) the Redux state to the associated FilmStripOnlyOverlayFrame
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _avatar: string
* }}
*/
function _mapStateToProps(state) {
const participant
= getLocalParticipant(
state['features/base/participants']);
const { avatarId, avatarUrl, email } = participant || {};
return {
_avatar: getAvatarURL({
avatarId,
avatarUrl,
email,
participantId: participant.id
})
};
}
export default connect(_mapStateToProps)(FilmStripOnlyOverlayFrame);

View File

@@ -1,12 +1,17 @@
/* global APP */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PageReloadFilmStripOnlyOverlay from './PageReloadFilmStripOnlyOverlay';
import PageReloadOverlay from './PageReloadOverlay';
import SuspendedFilmStripOnlyOverlay from './SuspendedFilmStripOnlyOverlay';
import SuspendedOverlay from './SuspendedOverlay';
import UserMediaPermissionsFilmStripOnlyOverlay
from './UserMediaPermissionsFilmStripOnlyOverlay';
import UserMediaPermissionsOverlay from './UserMediaPermissionsOverlay';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* Implements a React Component that will display the correct overlay when
* needed.
@@ -94,6 +99,25 @@ class OverlayContainer extends Component {
_suspendDetected: React.PropTypes.bool
}
/**
* Initializes a new ReloadTimer instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props) {
super(props);
this.state = {
/**
* Indicates whether the film strip only mode is enabled or not.
*
* @type {boolean}
*/
filmStripOnly: interfaceConfig.filmStripOnly
};
}
/**
* React Component method that executes once component is updated.
*
@@ -117,25 +141,28 @@ class OverlayContainer extends Component {
* @public
*/
render() {
const filmStripOnlyMode = this.state.filmStripOnly;
let overlayComponent, props;
if (this.props._connectionEstablished && this.props._haveToReload) {
return (
<PageReloadOverlay
isNetworkFailure = { this.props._isNetworkFailure }
reason = { this.props._reason } />
);
overlayComponent = filmStripOnlyMode
? PageReloadFilmStripOnlyOverlay : PageReloadOverlay;
props = {
isNetworkFailure: this.props._isNetworkFailure,
reason: this.props._reason
};
} else if (this.props._suspendDetected) {
overlayComponent = filmStripOnlyMode
? SuspendedFilmStripOnlyOverlay : SuspendedOverlay;
} else if (this.props._isMediaPermissionPromptVisible) {
overlayComponent = filmStripOnlyMode
? UserMediaPermissionsFilmStripOnlyOverlay
: UserMediaPermissionsOverlay;
props = { browser: this.props._browser };
}
if (this.props._suspendDetected) {
return (
<SuspendedOverlay />
);
}
if (this.props._isMediaPermissionPromptVisible) {
return (
<UserMediaPermissionsOverlay
browser = { this.props._browser } />
);
if (overlayComponent) {
return React.createElement(overlayComponent, props);
}
return null;

View File

@@ -0,0 +1,77 @@
import React, { Component } from 'react';
declare var interfaceConfig: Object;
/**
* Implements a React Component for the frame of the overlays.
*/
export default class OverlayFrame extends Component {
/**
* OverlayFrame component's property types.
*
* @static
*/
static propTypes = {
/**
* The children components to be displayed into the overlay frame.
*/
children: React.PropTypes.node.isRequired,
/**
* Indicates the css style of the overlay. If true, then lighter;
* darker, otherwise.
*
* @type {boolean}
*/
isLightOverlay: React.PropTypes.bool
}
/**
* Initializes a new AbstractOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props) {
super(props);
this.state = {
/**
* Indicates whether the film strip only mode is enabled or not.
*
* @type {boolean}
*/
filmStripOnly: interfaceConfig.filmStripOnly
};
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
let containerClass = this.props.isLightOverlay
? 'overlay__container-light' : 'overlay__container';
let contentClass = 'overlay__content';
if (this.state.filmStripOnly) {
containerClass += ' filmstrip-only';
contentClass += ' filmstrip-only';
}
return (
<div
className = { containerClass }
id = 'overlay'>
<div className = { contentClass }>
{
this.props.children
}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,62 @@
import React from 'react';
import { translate } from '../../base/i18n';
import AbstractPageReloadOverlay from './AbstractPageReloadOverlay';
import FilmStripOnlyOverlayFrame from './FilmStripOnlyOverlayFrame';
/**
* Implements a React Component for page reload overlay for filmstrip only
* mode. Shown before the conference is reloaded. Shows a warning message and
* counts down towards the reload.
*/
class PageReloadFilmStripOnlyOverlay extends AbstractPageReloadOverlay {
/**
* PageReloadFilmStripOnlyOverlay component's property types.
*
* @static
*/
static propTypes = {
...AbstractPageReloadOverlay.propTypes,
/**
* The function to translate human-readable text.
*
* @public
* @type {Function}
*/
t: React.PropTypes.func
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
const { t } = this.props;
const { message, timeLeft, title } = this.state;
return (
<FilmStripOnlyOverlayFrame>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{ t(title) }
</div>
<div className = 'inlay-filmstrip-only__text'>
{ t(message, { seconds: timeLeft }) }
</div>
</div>
{
this._renderButton()
}
{
this._renderProgressBar()
}
</FilmStripOnlyOverlayFrame>
);
}
}
export default translate(PageReloadFilmStripOnlyOverlay);

View File

@@ -1,194 +1,58 @@
import React from 'react';
import { translate } from '../../base/i18n';
import { randomInt } from '../../base/util';
import AbstractOverlay from './AbstractOverlay';
import ReloadTimer from './ReloadTimer';
declare var APP: Object;
const logger = require('jitsi-meet-logger').getLogger(__filename);
import AbstractPageReloadOverlay from './AbstractPageReloadOverlay';
import OverlayFrame from './OverlayFrame';
/**
* Implements a React Component for page reload overlay. Shown before the
* conference is reloaded. Shows a warning message and counts down towards the
* reload.
*/
class PageReloadOverlay extends AbstractOverlay {
class PageReloadOverlay extends AbstractPageReloadOverlay {
/**
* PageReloadOverlay component's property types.
*
* @static
*/
static propTypes = {
/**
* The indicator which determines whether the reload was caused by
* network failure.
* @public
* @type {boolean}
*/
isNetworkFailure: React.PropTypes.bool,
...AbstractPageReloadOverlay.propTypes,
/**
* The reason for the error that will cause the reload.
* NOTE: Used by PageReloadOverlay only.
* The function to translate human-readable text.
*
* @public
* @type {string}
* @type {Function}
*/
reason: React.PropTypes.string
t: React.PropTypes.func
}
/**
* Initializes a new PageReloadOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props) {
super(props);
/**
* How long the overlay dialog will be displayed, before the conference
* will be reloaded.
* @type {number}
*/
const timeoutSeconds = 10 + randomInt(0, 20);
let isLightOverlay, message, title;
if (this.props.isNetworkFailure) {
title = 'dialog.conferenceDisconnectTitle';
message = 'dialog.conferenceDisconnectMsg';
isLightOverlay = true;
} else {
title = 'dialog.conferenceReloadTitle';
message = 'dialog.conferenceReloadMsg';
isLightOverlay = false;
}
this.state = {
...this.state,
/**
* Indicates the css style of the overlay. If true, then lighter;
* darker, otherwise.
*
* @type {boolean}
*/
isLightOverlay,
/**
* The translation key for the title of the overlay.
*
* @type {string}
*/
message,
/**
* How long the overlay dialog will be displayed before the
* conference will be reloaded.
*
* @type {number}
*/
timeoutSeconds,
/**
* The translation key for the title of the overlay.
*
* @type {string}
*/
title
};
}
/**
* This method is executed when comonent is mounted.
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
super.componentDidMount();
// FIXME (CallStats - issue) This event will not make it to CallStats
// because the log queue is not flushed before "fabric terminated" is
// sent to the backed.
// FIXME: We should dispatch action for this.
APP.conference.logEvent(
'page.reload',
/* value */ undefined,
/* label */ this.props.reason);
logger.info(
'The conference will be reloaded after '
+ `${this.state.timeoutSeconds} seconds.`);
}
/**
* Renders the button for relaod the page if necessary.
*
* @returns {ReactElement|null}
* @private
*/
_renderButton() {
if (this.props.isNetworkFailure) {
const className
= 'button-control button-control_primary button-control_center';
const { t } = this.props;
/* eslint-disable react/jsx-handler-names */
return (
<button
className = { className }
id = 'reconnectNow'
onClick = { this._reconnectNow }>
{ t('dialog.reconnectNow') }
</button>
);
/* eslint-enable react/jsx-handler-names */
}
return null;
}
/**
* Constructs overlay body with the warning message and count down towards
* the conference reload.
*
* @returns {ReactElement|null}
* @override
* @protected
*/
_renderOverlayContent() {
const { t } = this.props;
/* eslint-disable react/jsx-handler-names */
render() {
const { isNetworkFailure, t } = this.props;
const { message, timeLeft, title } = this.state;
return (
<div className = 'inlay'>
<span
className = 'reload_overlay_title'>
{ t(this.state.title) }
</span>
<span
className = 'reload_overlay_text'>
{ t(this.state.message) }
</span>
<ReloadTimer
end = { 0 }
interval = { 1 }
onFinish = { this._reconnectNow }
start = { this.state.timeoutSeconds }
step = { -1 } />
{ this._renderButton() }
</div>
<OverlayFrame isLightOverlay = { isNetworkFailure }>
<div className = 'inlay'>
<span
className = 'reload_overlay_title'>
{ t(title) }
</span>
<span className = 'reload_overlay_text'>
{ t(message, { seconds: timeLeft }) }
</span>
{ this._renderProgressBar() }
{ this._renderButton() }
</div>
</OverlayFrame>
);
/* eslint-enable react/jsx-handler-names */
}
}

View File

@@ -0,0 +1,60 @@
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
import { reconnectNow } from '../functions';
/**
* Implements a React Component for button for the overlays that will reload
* the page.
*/
class ReloadButton extends Component {
/**
* PageReloadOverlay component's property types.
*
* @static
*/
static propTypes = {
/**
* The function to translate human-readable text.
*
* @public
* @type {Function}
*/
t: React.PropTypes.func,
/**
* The translation key for the text in the button.
*
* @type {string}
*/
textKey: React.PropTypes.string.isRequired
}
/**
* Renders the button for relaod the page if necessary.
*
* @returns {ReactElement|null}
* @private
*/
render() {
const className
= 'button-control button-control_overlay button-control_center';
const { t } = this.props;
/* eslint-disable react/jsx-handler-names */
return (
<button
className = { className }
onClick = { reconnectNow }>
{ t(this.props.textKey) }
</button>
);
/* eslint-enable react/jsx-handler-names */
}
}
export default translate(ReloadButton);

View File

@@ -0,0 +1,53 @@
import React, { Component } from 'react';
import { translate, translateToHTML } from '../../base/i18n';
import FilmStripOnlyOverlayFrame from './FilmStripOnlyOverlayFrame';
import ReloadButton from './ReloadButton';
/**
* Implements a React Component for suspended overlay for filmstrip only mode.
* Shown when suspended is detected.
*/
class SuspendedFilmStripOnlyOverlay extends Component {
/**
* SuspendedFilmStripOnlyOverlay component's property types.
*
* @static
*/
static propTypes = {
/**
* The function to translate human-readable text.
*
* @public
* @type {Function}
*/
t: React.PropTypes.func
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
const { t } = this.props;
return (
<FilmStripOnlyOverlayFrame isLightOverlay = { true }>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{ t('suspendedoverlay.title') }
</div>
<div className = 'inlay-filmstrip-only__text'>
{ translateToHTML(t, 'suspendedoverlay.text') }
</div>
</div>
<ReloadButton textKey = 'suspendedoverlay.rejoinKeyTitle' />
</FilmStripOnlyOverlayFrame>
);
}
}
export default translate(SuspendedFilmStripOnlyOverlay);

Some files were not shown because too many files have changed in this diff Show More