Compare commits

...

13 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
Saúl Ibarra Corretgé
dc2c49f4a9 doc: improve docs on external API constructor parameters 2017-03-27 12:17:32 +02:00
43 changed files with 1280 additions and 447 deletions

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.
*/

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

@@ -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,6 +1,6 @@
.notice {
position: relative;
z-index: 3;
z-index: $zindex3;
margin-top: 6px;
&__message {

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

@@ -37,6 +37,7 @@
@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';

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

@@ -10,45 +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);
```
You can also pass jwt token to Jitsi Meet:
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);
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
### Controlling the embedded Jitsi Meet Conference
You can control the embedded Jitsi Meet conference using the `JitsiMeetExternalAPI` object by using `executeCommand`:

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

@@ -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

@@ -22,7 +22,8 @@
"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"
"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",
@@ -338,7 +339,10 @@
"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!",
"close": "Close"
"close": "Close",
"shareYourScreen": "Share your screen",
"yourEntireScreen": "Your entire screen",
"applicationWindow": "Application window"
},
"email":
{

View File

@@ -20,14 +20,15 @@
"@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.1",
"es6-symbol": "3.1.1",
"i18next": "7.1.2",
"i18next": "7.1.3",
"i18next-browser-languagedetector": "1.0.1",
"i18next-xhr-backend": "1.4.0",
"i18next-xhr-backend": "1.4.1",
"jitsi-meet-logger": "jitsi/jitsi-meet-logger",
"jquery": "2.1.4",
"jquery-contextmenu": "2.4.3",
@@ -40,7 +41,7 @@
"postis": "2.2.0",
"react": "15.4.2",
"react-dom": "15.4.2",
"react-i18next": "2.2.0",
"react-i18next": "2.2.3",
"react-native": "0.42.3",
"react-native-background-timer": "1.0.0",
"react-native-immersive": "0.0.4",
@@ -61,16 +62,16 @@
},
"devDependencies": {
"babel-core": "6.24.0",
"babel-eslint": "7.2.0",
"babel-eslint": "7.2.1",
"babel-loader": "6.4.1",
"babel-polyfill": "6.23.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",
"css-loader": "0.28.0",
"eslint": "3.18.0",
"eslint-plugin-flowtype": "2.30.3",
"eslint-plugin-flowtype": "2.30.4",
"eslint-plugin-import": "2.2.0",
"eslint-plugin-jsdoc": "3.0.0",
"eslint-plugin-react": "6.10.3",
@@ -84,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.15.0",
"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,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

@@ -30,7 +30,7 @@ fi
CRON_FILE="/etc/cron.weekly/letsencrypt-renew"
echo "#!/bin/bash" > $CRON_FILE
echo "/usr/local/sbin/certbot-auto renew >> /var/log/le-renew.log" >> $CRON_FILE
echo "/usr/local/sbin/certbot-auto --renew-hook '/usr/share/jitsi-meet/scripts/renew-letsencrypt-cert.sh' renew >> /var/log/le-renew.log" >> $CRON_FILE
CERT_KEY="/etc/letsencrypt/live/$DOMAIN/privkey.pem"
CERT_CRT="/etc/letsencrypt/live/$DOMAIN/fullchain.pem"
@@ -54,7 +54,6 @@ if [ -f /etc/nginx/sites-enabled/$DOMAIN.conf ] ; then
sed -i "s/ssl_certificate\ \/etc\/jitsi\/meet\/.*crt/ssl_certificate\ $CERT_CRT_ESC/g" \
$CONF_FILE
echo "service nginx reload" >> $CRON_FILE
service nginx reload
elif [ -f /etc/apache2/sites-enabled/$DOMAIN.conf ] ; then
@@ -76,13 +75,11 @@ elif [ -f /etc/apache2/sites-enabled/$DOMAIN.conf ] ; then
sed -i "s/SSLCertificateFile\ \/etc\/jitsi\/meet\/.*crt/SSLCertificateFile\ $CERT_CRT_ESC/g" \
$CONF_FILE
echo "service apache2 reload" >> $CRON_FILE
service apache2 reload
else
service jitsi-videobridge stop
./certbot-auto certonly --noninteractive \
--standalone \
--webroot --webroot-path /usr/share/jitsi-meet \
-d $DOMAIN \
--agree-tos --email $EMAIL
@@ -97,7 +94,14 @@ else
-srckeystore $CERT_P12 -srcstoretype pkcs12 \
-noprompt -storepass changeit -srcstorepass changeit
service jitsi-videobridge start
PIDFILE=/var/run/jitsi-videobridge.pid
if [ -f $PIDFILE ]; then
PID=$(cat $PIDFILE)
/usr/share/jitsi-videobridge/graceful_shutdown.sh $PID || true
fi
service jitsi-videobridge restart
fi

View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -e
#
# This script is executed once a Lets Encrypt certificate had been renewed
# we reload web servers or in case of jetty we restart jvb
# In future we need to implement reloading jvb, which will reload the jetty
#
DEB_CONF_RESULT=`debconf-show jitsi-meet-web-config | grep jvb-hostname`
DOMAIN="${DEB_CONF_RESULT##*:}"
# remove whitespace
DOMAIN="$(echo -e "${DOMAIN}" | tr -d '[:space:]')"
if [ -f /etc/nginx/sites-enabled/$DOMAIN.conf ] ; then
service nginx reload
elif [ -f /etc/apache2/sites-enabled/$DOMAIN.conf ] ; then
service apache2 reload
else
PIDFILE=/var/run/jitsi-videobridge.pid
if [ -f $PIDFILE ]; then
PID=$(cat $PIDFILE)
/usr/share/jitsi-videobridge/graceful_shutdown.sh $PID || true
fi
service jitsi-videobridge restart
fi