mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-21 21:27:47 +00:00
Compare commits
13 Commits
1819
...
letsencryp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
029ca1753f | ||
|
|
2301732e2d | ||
|
|
24ee8eb16a | ||
|
|
57065bb274 | ||
|
|
08531ee675 | ||
|
|
e7140ffec7 | ||
|
|
c58c4b7938 | ||
|
|
4e276471e5 | ||
|
|
c5eac63da1 | ||
|
|
866c6d0cf9 | ||
|
|
165294bfb1 | ||
|
|
2d5f0479bd | ||
|
|
dc2c49f4a9 |
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1010;
|
||||
z-index: $jitsipopoverZ;
|
||||
display: none;
|
||||
max-width: 300px;
|
||||
min-width: 100px;
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
z-index: $zindex1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.notice {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
z-index: $zindex3;
|
||||
margin-top: 6px;
|
||||
|
||||
&__message {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1015;
|
||||
z-index: $popoverZ;
|
||||
display: none;
|
||||
max-width: 300px;
|
||||
min-width: 100px;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 0;
|
||||
z-index: 800;
|
||||
z-index: $sideToolbarContainerZ;
|
||||
|
||||
/**
|
||||
* Labels inside the side panel.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
59
css/modals/desktop-picker/_desktop-picker.scss
Normal file
59
css/modals/desktop-picker/_desktop-picker.scss
Normal 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;
|
||||
}
|
||||
61
doc/api.md
61
doc/api.md
@@ -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`:
|
||||
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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": " ",
|
||||
"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": "所有的录制器都在忙。请稍后重试。"
|
||||
}
|
||||
}
|
||||
@@ -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":
|
||||
{
|
||||
|
||||
17
package.json
17
package.json
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
20
react/features/desktop-picker/actionTypes.js
Normal file
20
react/features/desktop-picker/actionTypes.js
Normal 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');
|
||||
87
react/features/desktop-picker/actions.js
Normal file
87
react/features/desktop-picker/actions.js
Normal 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
|
||||
};
|
||||
}
|
||||
264
react/features/desktop-picker/components/DesktopPicker.js
Normal file
264
react/features/desktop-picker/components/DesktopPicker.js
Normal 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));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
1
react/features/desktop-picker/components/index.js
Normal file
1
react/features/desktop-picker/components/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as DesktopPicker } from './DesktopPicker';
|
||||
5
react/features/desktop-picker/index.js
Normal file
5
react/features/desktop-picker/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './actionTypes';
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './reducer';
|
||||
59
react/features/desktop-picker/reducer.js
Normal file
59
react/features/desktop-picker/reducer.js
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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',
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
import { ReducerRegistry } from '../../base/redux';
|
||||
|
||||
import {
|
||||
_SET_APP_STATE_LISTENER,
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
29
resources/renew-letsencrypt-cert.sh
Normal file
29
resources/renew-letsencrypt-cert.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
#
|
||||
# This script is executed once a Let’s 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
|
||||
Reference in New Issue
Block a user