Compare commits

...

37 Commits

Author SHA1 Message Date
Emil Ivov
8648a5a998 Redirect security section to jitsi.org/security 2020-04-22 12:58:18 -05:00
Julian1203
c83b30558d lang: fixes for German 2020-04-22 15:44:09 +02:00
Julian1203
057dfc8194 lang: fixes for German 2020-04-22 15:42:18 +02:00
Fabian Rodriguez
5eae0b58e9 lang: fixes for Spanish (US) 2020-04-22 15:40:57 +02:00
Fabian Rodriguez
1538107e93 lang: fixes for Spanish translation 2020-04-22 15:28:46 +02:00
Bettenbuk Zoltan
cd1862a2d3 fix: private message open 2020-04-22 12:09:07 +02:00
Eduard Itrich
147a076f5d doc: fixed typo in port number of jicofo 2020-04-22 10:57:34 +02:00
Hristo Terezov
9bdaea4069 fix(avatar): use text instead of foreign object.
Reason: On Safari the foreign object is not scaled correctly.
2020-04-21 10:12:51 -05:00
Jonathan Scholz
547d1547bb fix(dialog): use height behavior in KeyboardAvoidingView 2020-04-21 15:13:08 +02:00
Jonathan Scholz
39853e048b fix(dialog): fix on-screen keyboard overlapping dialog boxes 2020-04-21 15:13:08 +02:00
Saúl Ibarra Corretgé
8b454b5196 deps: update lib-jitsi-meet
f97c37d0 e2ee: also enable on p2p connections
a832b39b fix(safari): construct track constraints from getSettings (#1104)
2020-04-21 15:05:40 +02:00
Saúl Ibarra Corretgé
e2788e0fb2 e2ee: show warning if not all participants support E2EE
Refs: https://github.com/jitsi/lib-jitsi-meet/pull/1108
2020-04-21 13:41:05 +02:00
Titus-Andrei Moldovan
b1d1599a1c android: add a consistent deviceId for Amplitude from SharedPreferences 2020-04-21 11:45:42 +02:00
Saúl Ibarra Corretgé
cef98f457f ios: bump CocoaPods dependency 2020-04-21 09:58:24 +02:00
Andrei Gavrilescu
6b0e8aab11 bugfix(remote-control): Remote control not working on linux 2020-04-21 10:33:25 +03:00
Jaya Allamsetty
086889ed70 deps: update lib-jitsi-meet 2020-04-20 18:05:49 -04:00
Saúl Ibarra Corretgé
516e5af118 doc: update supported platforms for building mobile apps 2020-04-20 20:25:24 +02:00
Saúl Ibarra Corretgé
afe1b4b0f9 rn: now working on 20.3 2020-04-20 16:01:25 +02:00
Saúl Ibarra Corretgé
8790ad6013 misc: update GH issue templates
- Use config.yml to prevent creating issues with a blank template
- Don't use a template to direct people to the forum
- Create a security policy template
2020-04-20 15:06:57 +02:00
antonbks
8bbc04d4db doc: fix dev server backend default 2020-04-20 14:02:23 +02:00
Hristo Terezov
4fda428be1 fix(largeVideo): update don't depend on thumbnails 2020-04-17 15:33:13 -05:00
Hristo Terezov
f972ebfe9e fix(thumbnail): videos on safari. 2020-04-17 13:15:25 -05:00
Jaya Allamsetty
3dfcc8d80e deps(ljm): Bump lib-jitsi-meet for Firefox ESR simulcast fix 2020-04-16 21:07:22 -04:00
Saúl Ibarra Corretgé
33ebd241a9 external_api: add command to set E2EE key 2020-04-16 20:25:56 +02:00
Saúl Ibarra Corretgé
cb6fbb0f03 e2ee: add UI elements
* Add dialog to set the E2EE key
* Use the Redux action / middleware to update the key even when set through the
  hash parameter
* Cleanup URL after processing the key so it's not recorded in browser history
2020-04-16 20:25:56 +02:00
Philipp Hancke
0077ee29c5 deps: bump lib-jitsi-meet
Changelog:
101fecbb Thu Apr 16 11:23:58 2020 +0200 Philipp Hancke: e2ee: decode static black frame for decryption errors (#1098)
c3fd3431 Thu Apr 16 13:09:18 2020 +0200 Philipp Hancke: e2ee: remove encodedFrameType workaround (#1099)

git log --no-merges --reverse --pretty="%h %ad %an: %s" 70a3298914f3905297e4e9dcc200b95e9b7a73e9..c3fd3431a66556de7b2ec7632f9f6d75b64aad0a
2020-04-16 17:20:08 +02:00
Bettenbuk Zoltan
551db30cc7 fix: modal keyboard avoiding view fix 2020-04-16 15:30:01 +02:00
Philipp Hancke
1bd930a3cb deps: update lib-jitsi-meet
Changelog:
6502bc67 Wed Apr 15 10:51:35 2020 +0200 Philipp Hancke: e2ee: generate silence in case of audio decryption errors
adc87bcf Mon Apr 6 15:07:48 2020 -0500 paweldomas: fix(strophe.jingle.js): exception on ICE config modification
2020-04-15 21:24:33 +02:00
Jaya Allamsetty
e0563a743f Revert "feat(browser-support): Add safari to list of optimal browsers"
This reverts commit 4824c8714a.
2020-04-15 11:24:04 -05:00
Saúl Ibarra Corretgé
a4ab7ea95f deps: upddate lib-jitsi-meet 2020-04-15 11:04:23 -05:00
Vlad Piersec
b50d6e43d0 fix(settings_buttons): Persist audio/video settings across sessions. 2020-04-15 08:33:19 -05:00
Saúl Ibarra Corretgé
f9fcb46036 pip: disable PiP on Android < 26
Fixes: https://github.com/jitsi/jitsi-meet/issues/6008
2020-04-15 12:12:58 +02:00
Jaya Allamsetty
4824c8714a feat(browser-support): Add safari to list of optimal browsers 2020-04-14 13:42:27 -04:00
damencho
be56521267 debian: Skips deploy-hook when there is no hook available. 2020-04-14 11:54:58 -05:00
Jaya Allamsetty
b7eda8df7a fix(safari): set playsInline attribute to true on the video element
The playsInline attribute needs to be set to true for video to be rendered on Safari on iOS
2020-04-14 12:28:09 -04:00
Jaya Allamsetty
d632b6e13e deps: Update ljm for FF simulcast, js-utils for detecting Edge on Android 2020-04-14 11:07:09 -04:00
Bettenbuk Zoltan
dff7d661ca doc: add documentation to some url params 2020-04-14 16:05:04 +02:00
60 changed files with 661 additions and 353 deletions

View File

@@ -1,10 +0,0 @@
---
name: Need help with Jitsi Meet?
about: Please ask it in our community at https://community.jitsi.org
---
If you have a question about Jitsi Meet that is not a bug report or feature
request, please post it in https://community.jitsi.org
Questions posted to this repository will be closed.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Need help with Jitsi Meet?
url: https://community.jitsi.org
about: Please ask it in our community.

View File

@@ -1,6 +1,6 @@
# Jitsi Meet - Secure, Simple and Scalable Video Conferences
Jitsi Meet is an open-source (Apache) WebRTC JavaScript application that uses [Jitsi Videobridge](https://jitsi.org/videobridge) to provide high quality, [secure](#security) and scalable video conferences. Jitsi Meet in action can be seen at [here at the session #482 of the VoIP Users Conference](http://youtu.be/7vFUVClsNh0).
Jitsi Meet is an open-source (Apache) WebRTC JavaScript application that uses [Jitsi Videobridge](https://jitsi.org/videobridge) to provide high quality, [secure](https://jitsi.org/security) and scalable video conferences. Jitsi Meet in action can be seen at [here at the session #482 of the VoIP Users Conference](http://youtu.be/7vFUVClsNh0).
The Jitsi Meet client runs in your browser, without installing anything else on your computer. You can try it out at https://meet.jit.si.
@@ -61,25 +61,13 @@ Jitsi Meet provides a very flexible way of embedding in external applications by
## Security
WebRTC does not (yet) provide a way of conducting multi-party conversations with end-to-end encryption.
Unless you consistently compare DTLS fingerprints with your peers vocally, the same goes for one-to-one calls.
As a result, your stream is encrypted on the network but decrypted on the machine that hosts the bridge when using Jitsi Meet.
The security section here was starting to feel a bit too succinct for the complexity of the topic, so we created a post that covers the topic much more broadly here: https://jitsi.org/security
The Jitsi Meet architecture allows you to deploy your own version, including
all server components. In that case, your security guarantees will be roughly
equivalent to a direct one-to-one WebRTC call. This is the uniqueness of
Jitsi Meet in terms of security.
The [meet.jit.si](https://meet.jit.si) service is maintained by the Jitsi team
at [8x8](https://8x8.com).
The section on end-to-end encryption in that document is likely going to be one of the key points of interest: https://jitsi.org/security/#e2ee
## Security issues
We take security very seriously and develop all Jitsi projects to be secure and safe.
If you find (or simply suspect) a security issue in any of the Jitsi projects, please send us an email to security@jitsi.org.
**We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.**
For information on reporting security vulnerabilities in Jitsi Meet, see [SECURITY.md](./SECURITY.md).
## Acknowledgements

View File

@@ -1,11 +1,9 @@
---
name: Security issues
about: Please email security@jitsi.org
# Security
---
## Reporting security issuess
We take security very seriously and develop all Jitsi projects to be secure and safe.
If you find (or simply suspect) a security issue in any of the Jitsi projects, please send us an email to security@jitsi.org.
We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.
**We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.**

View File

@@ -20,5 +20,5 @@
android.useAndroidX=true
android.enableJetifier=true
appVersion=20.2.0
appVersion=20.3.0
sdkVersion=2.8.0

View File

@@ -17,6 +17,8 @@
package org.jitsi.meet.sdk;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.provider.Settings;
import android.text.TextUtils;
@@ -40,6 +42,8 @@ class AmplitudeModule
extends ReactContextBaseJavaModule {
public static final String NAME = "Amplitude";
public static final String JITSI_PREFERENCES = "jitsi-preferences";
public static final String AMPLITUDE_DEVICE_ID_KEY = "amplitudeDeviceId";
public AmplitudeModule(ReactApplicationContext reactContext) {
super(reactContext);
@@ -58,8 +62,8 @@ class AmplitudeModule
Amplitude.getInstance(instanceName).initialize(getCurrentActivity(), apiKey);
// Set the device ID to something consistent.
String android_id
= Settings.Secure.getString(getReactApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
SharedPreferences sharedPreferences = getReactApplicationContext().getSharedPreferences(JITSI_PREFERENCES, Context.MODE_PRIVATE);
String android_id = sharedPreferences.getString(AMPLITUDE_DEVICE_ID_KEY, "");
if (!TextUtils.isEmpty(android_id)) {
Amplitude.getInstance(instanceName).setDeviceId(android_id);
}

View File

@@ -19,7 +19,6 @@ import {
createDeviceChangedEvent,
createStartSilentEvent,
createScreenSharingEvent,
createStreamSwitchDelayEvent,
createTrackMutedEvent,
sendAnalytics
} from './react/features/analytics';
@@ -113,6 +112,7 @@ import {
import { getJitsiMeetGlobalNS } from './react/features/base/util';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
import { setE2EEKey } from './react/features/e2ee';
import {
maybeOpenFeedbackDialog,
submitFeedback
@@ -470,11 +470,6 @@ export default {
*/
localVideo: null,
/**
* The key used for End-To-End Encryption.
*/
e2eeKey: undefined,
/**
* Creates local media tracks and connects to a room. Will show error
* dialogs in case accessing the local microphone and/or camera failed. Will
@@ -1202,11 +1197,14 @@ export default {
items[key] = param[1];
}
this.e2eeKey = items.e2eekey;
if (typeof items.e2eekey !== undefined) {
APP.store.dispatch(setE2EEKey(items.e2eekey));
logger.debug(`New E2EE key: ${this.e2eeKey}`);
// Clean URL in browser history.
const cleanUrl = window.location.href.split('#')[0];
this._room.setE2EEKey(this.e2eeKey);
history.replaceState(history.state, document.title, cleanUrl);
}
},
/**
@@ -1818,7 +1816,7 @@ export default {
const desktopVideoStream = streams.find(stream => stream.getType() === MEDIA_TYPE.VIDEO);
if (desktopVideoStream) {
this.useVideoStream(desktopVideoStream);
await this.useVideoStream(desktopVideoStream);
}
this._desktopAudioStream = streams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
@@ -2264,18 +2262,6 @@ export default {
});
});
/* eslint-disable max-params */
APP.UI.addListener(
UIEvents.RESOLUTION_CHANGED,
(id, oldResolution, newResolution, delay) => {
sendAnalytics(createStreamSwitchDelayEvent(
{
'old_resolution': oldResolution,
'new_resolution': newResolution,
value: delay
}));
});
APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
AuthHandler.authenticate(room);
});

View File

@@ -44,9 +44,6 @@ var config = {
//
testing: {
// Enables experimental simulcast support on Firefox.
enableFirefoxSimulcast: false,
// P2P test mode disables automatic switching to P2P when there are 2
// participants in the conference.
p2pTestMode: false
@@ -451,6 +448,14 @@ var config = {
// the menu has option to flip the locally seen video for local presentations
// disableLocalVideoFlip: false,
// Mainly privacy related settings
// Disables all invite functions from the app (share, invite, dial out...etc)
// disableInviteFunctions: true,
// Disables storing the room name to the recents list
// doNotStoreRoom: true,
// Deployment specific URLs.
// deploymentUrls: {
// // If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for

View File

@@ -20,18 +20,6 @@
}
}
.avatar-foreign {
align-items: center;
bottom: 0;
display: flex;
font-size: 40pt;
justify-content: center;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.avatar-svg {
height: 100%;
width: 100%;
@@ -63,4 +51,4 @@
@include avatarBadge;
background-color: $presence-idle;
}
}
}

View File

@@ -85,14 +85,15 @@ const options = {
const api = new JitsiMeetExternalAPI(domain, options);
```
You can set the userInfo(email) for the call:
You can set the userInfo(email, display name) for the call:
```javascript
var domain = "meet.jit.si";
var options = {
...
userInfo: {
email: 'email@jitsiexamplemail.com'
email: 'email@jitsiexamplemail.com',
displayName: 'John Doe'
}
}
var api = new JitsiMeetExternalAPI(domain, options);

View File

@@ -69,7 +69,7 @@ Use it at the CLI, type
make dev
```
By default the backend deployment used is `beta.meet.jit.si`. You can point the Jitsi-Meet app at a different backend by using a proxy server. To do this, set the WEBPACK_DEV_SERVER_PROXY_TARGET variable:
By default the backend deployment used is `alpha.jitsi.net`. You can point the Jitsi-Meet app at a different backend by using a proxy server. To do this, set the WEBPACK_DEV_SERVER_PROXY_TARGET variable:
```
export WEBPACK_DEV_SERVER_PROXY_TARGET=https://your-example-server.com
make dev

View File

@@ -7,8 +7,10 @@ Jitsi Meet can be built as a standalone app for Android or iOS. It uses the
First make sure the [React Native dependencies] are installed.
**NOTE**: This document assumes the app is being built on a macOS system.
**NOTE**: Node 10.X and npm 6.X are recommended for building.
**NOTE**: This document assumes the app is being built on a macOS system. GNU/Linux is also
supported for building the Android app and Windows **is not supported at alll**.
**NOTE**: Node 12.X and npm 6.X are recommended for building.
## iOS

View File

@@ -107,7 +107,7 @@ Open to world:
Open to the videobridges only
* 5222 TCP (for Prosody)
* 5437 TCP (for Jicofo)
* 5347 TCP (for Jicofo)
#### NGINX

View File

@@ -51,7 +51,8 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone'
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
'e2ee'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

View File

@@ -586,4 +586,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: f615794fb9184757b00cd16e534824ba6ee2fc98
COCOAPODS: 1.8.4
COCOAPODS: 1.9.1

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>20.2.0</string>
<string>20.3.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>20.2.0</string>
<string>20.3.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>20.2.0</string>
<string>20.3.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

@@ -25,7 +25,7 @@
"none": "Keine Audiogeräte verfügbar"
},
"audioOnly": {
"audioOnly": "Nur Audio"
"audioOnly": "Geringe Bandbreite"
},
"calendarSync": {
"addMeetingURL": "Meeting-Link hinzufügen",
@@ -69,7 +69,7 @@
},
"connection": {
"ATTACHED": "Angehängt",
"AUTHENTICATING": "Anmeldung läuft",
"AUTHENTICATING": "Authentifizierung läuft",
"AUTHFAIL": "Authentifizierung fehlgeschlagen",
"CONNECTED": "Verbunden",
"CONNECTING": "Verbindung wird hergestellt",
@@ -268,7 +268,7 @@
"stopRecording": "Aufnahme stoppen",
"stopRecordingWarning": "Sind Sie sicher, dass Sie die Aufnahme stoppen möchten?",
"stopStreamingWarning": "Sind Sie sicher, dass Sie das Livestreaming stoppen möchten?",
"streamKey": "Name/Schlüssel für den Stream",
"streamKey": "Streamschlüssel",
"Submit": "OK",
"thankYou": "Danke für die Verwendung von {{appName}}!",
"token": "Token",
@@ -352,7 +352,7 @@
"keyboardShortcuts": {
"focusLocal": "Lokales Video fokussieren",
"focusRemote": "Auf das Video eines anderen Teilnehmers fokussieren",
"fullScreen": "Vollbildmodus aktivieren / deaktivieren",
"fullScreen": "Vollbildmodus aktivieren oder deaktivieren",
"keyboardShortcuts": "Tastenkürzel",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein- oder ausblenden",
"mute": "Stummschaltung aktivieren oder deaktivieren",
@@ -372,7 +372,7 @@
"changeSignIn": "Konten wechseln.",
"choose": "Livestream auswählen",
"chooseCTA": "Streaming-Option auswählen. Sie sind aktuell als {{email}} angemeldet.",
"enterStreamKey": "Name/Schlüssel für den YouTube-Livestream hier eingeben.",
"enterStreamKey": "Streamschlüssel für den YouTube-Livestream hier eingeben.",
"error": "Das Livestreaming ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"errorAPI": "Beim Abrufen der YouTube-Livestreams ist ein Fehler aufgetreten. Bitte versuchen Sie, sich erneut anzumelden.",
"errorLiveStreamNotEnabled": "Livestreaming ist für {{email}} nicht aktiviert. Aktivieren Sie das Livestreaming oder melden Sie sich bei einem Konto mit aktiviertem Livestreaming an.",
@@ -390,7 +390,7 @@
"serviceName": "Livestreaming-Dienst",
"signedInAs": "Sie sind derzeit angemeldet als:",
"signIn": "Mit Google anmelden",
"signInCTA": "Anmelden oder den Namen/Schlüssel des YouTube-Livestreams eingeben.",
"signInCTA": "Anmelden oder den Streamschlüssel des YouTube-Livestreams eingeben.",
"signOut": "Abmelden",
"start": "Einen Livestream starten",
"streamIdHelp": "Was ist das?",

View File

@@ -82,10 +82,10 @@
"connectionindicator": {
"address": "Dirección:",
"bandwidth": "Ancho de banda estimado:",
"bitrate": "Tasa de bits:",
"bitrate": "Tasa de transferencia:",
"bridgeCount": "Contador del servidor: ",
"connectedTo": "Conectado a:",
"framerate": "Tasa de cuadros:",
"framerate": "Fotogramas por segundo:",
"less": "Mostrar menos",
"localaddress": "Dirección local:",
"localaddress_plural": "Direcciones locales:",
@@ -96,8 +96,8 @@
"quality": {
"good": "Bueno",
"inactive": "Inactivo",
"lost": "Perdida",
"nonoptimal": "No óptima",
"lost": "Perdido",
"nonoptimal": "No óptimo",
"poor": "Pobre"
},
"remoteaddress": "Dirección remota:",
@@ -105,7 +105,7 @@
"remoteport": "Puerto remoto:",
"remoteport_plural": "Puertos remotos:",
"resolution": "Resolución:",
"status": "Conexión:",
"status": "Estado:",
"transport": "Transporte:",
"transport_plural": "Transportes:"
},
@@ -322,7 +322,7 @@
"numbers": "Números de marcado",
"password": "$t(lockRoomPasswordUppercase):",
"title": "Compartir",
"tooltip": "Compartir el enlace y la información de acceso telefónico para esta reunión",
"tooltip": "Compartir el enlace y acceso telefónico para esta reunión",
"label": "Información de la reunión"
},
"inviteDialog": {
@@ -345,10 +345,10 @@
"fullScreen": "Ver o salir de pantalla completa",
"keyboardShortcuts": "Atajos de teclado",
"localRecording": "Mostrar u ocultar controles de grabación locales",
"mute": "Activar o desactivar micrófono",
"mute": "Activar o silenciar el micrófono",
"pushToTalk": "Presione para hablar",
"raiseHand": "Levantar o bajar la mano",
"showSpeakerStats": "Mostrar estadísticas del locutor",
"showSpeakerStats": "Estadísticas de participantes",
"toggleChat": "Abrir o cerrar panel de chat",
"toggleFilmstrip": "Mostrar/Ocultar miniaturas de video",
"toggleScreensharing": "Cambiar entre cámara y compartir pantalla",
@@ -522,8 +522,8 @@
"selectAudioOutput": "Salida de audio",
"selectCamera": "Cámara",
"selectMic": "Micrófono",
"startAudioMuted": "Todos inician en silencio",
"startVideoMuted": "Todos inician ocultos",
"startAudioMuted": "Todos inician silenciados",
"startVideoMuted": "Todos inician con cámara desactivada",
"title": "Ajustes"
},
"settingsView": {
@@ -549,14 +549,14 @@
"dialInfoText": "\n\n===== \\ n\n¿Solo quieres marcar en tu teléfono? \\ N\n{{defaultDialInNumber}} Haga clic en este enlace para ver el marcado en los números de teléfono de esta reunión\n{{dialInfoPageUrl}}",
"mainText": "Presione en el siguiente enlace para unirse a la reunión:\n{{roomUrl}}"
},
"speaker": "Orador",
"speaker": "Participante",
"speakerStats": {
"hours": "{{count}}h",
"minutes": "{{count}}m",
"name": "Nombre",
"seconds": "{{count}}s",
"speakerStats": "Estadísticas del locutor",
"speakerTime": "Tiempo del locutor"
"speakerStats": "Estadísticas de participantes",
"speakerTime": "Tiempo hablado"
},
"startupoverlay": {
"policyText": " ",

View File

@@ -89,7 +89,7 @@
"bitrate": "Tasa de transferencia:",
"bridgeCount": "Contador del servidor: ",
"connectedTo": "Conectado a:",
"framerate": "Cuadros por segundo:",
"framerate": "Fotogramas por segundo:",
"less": "Mostrar menos",
"localaddress": "Dirección local:",
"localaddress_plural": "Direcciones locales:",
@@ -109,7 +109,7 @@
"remoteport": "Puerto remoto:",
"remoteport_plural": "Puertos remotos:",
"resolution": "Resolución:",
"status": "Conexión:",
"status": "Estado:",
"transport": "Transporte:",
"transport_plural": "Transportes:"
},
@@ -332,8 +332,8 @@
"numbers": "Números de marcación desde afuera",
"password": "$t(lockRoomPasswordUppercase):",
"title": "Compartir",
"tooltip": "Compartir el enlace y la información de marcación desde afuera para esta reunión",
"label": "Información de reunión"
"tooltip": "Compartir el enlace y acceso telefónico para esta reunión",
"label": "Información de la reunión"
},
"inviteDialog": {
"alertText": "No se pudieron invitar a algunos participantes.",
@@ -532,8 +532,8 @@
"selectAudioOutput": "Salida de audio",
"selectCamera": "Cámara",
"selectMic": "Micrófono",
"startAudioMuted": "Todos comienzan con el silencio activado",
"startVideoMuted": "Todos comienzan ocultos",
"startAudioMuted": "Todos comienzan con silenciados",
"startVideoMuted": "Todos comienzan con cámara desactivada",
"title": "Configuración"
},
"settingsView": {
@@ -559,14 +559,14 @@
"dialInfoText": "\n\n=====\n\n¿Desea llamar desde su teléfono?\n\n{{defaultDialInNumber}}La lista de números disponibles para la reunión está disponible aquí : \n{{dialInfoPageUrl}}",
"mainText": "Haz clic en el enlace siguiente para unirte a la reunión:\n{{roomUrl}}"
},
"speaker": "Altavoz",
"speaker": "Participante",
"speakerStats": {
"hours": "{{count}} h",
"minutes": "{{count}} min",
"name": "Nombre",
"seconds": "{{count}} s",
"speakerStats": "Estadísticas del altavoz",
"speakerTime": "Hora del altavoz"
"speakerStats": "Estadísticas de participantes",
"speakerTime": "Tiempo hablado"
},
"startupoverlay": {
"policyText": " ",
@@ -641,7 +641,7 @@
"lowerYourHand": "Bajar la mano",
"moreActions": "Más acciones",
"moreOptions": "Más opciones",
"mute": "Silenciar/anular silencio",
"mute": "Activar o silenciar el micrófono",
"muteEveryone": "Silenciar a todos",
"noAudioSignalTitle": "¡No hay ninguna entrada que provenga de su micrófono!",
"noAudioSignalDesc": "Si no lo silenció intencionalmente desde la configuración del sistema o el hardware, considere la posibilidad de cambiar el dispositivo.",
@@ -660,7 +660,7 @@
"sharedvideo": "Compartir un video de YouTube",
"shareRoom": "Invitar a alguien",
"shortcuts": "Ver accesos directos",
"speakerStats": "Estadísticas del altavoz",
"speakerStats": "Estadísticas de participantes",
"startScreenSharing": "Iniciar uso compartido de pantalla",
"startSubtitles": "Iniciar subtítulos",
"stopScreenSharing": "Detener uso compartido de pantalla",

View File

@@ -175,6 +175,10 @@
"dismiss": "Dismiss",
"displayNameRequired": "Hi! Whats your name?",
"done": "Done",
"e2eeDescription": "<p>End-to-End Encryption is currently <strong>EXPERIMENTAL</strong>. Please see <a href='https://jitsi.org/blog/e2ee/' target='_blank'>this post</a> for details.</p><br/><p>Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: recording, live streaming and phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.</p>",
"e2eeLabel": "Key",
"e2eeTitle": "End-to-End Encryption",
"e2eeWarning": "<br /><p><strong>WARNING:</strong> Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.</p>",
"enterDisplayName": "Please enter your name here",
"error": "Error",
"externalInstallationMsg": "You need to install our desktop sharing extension.",
@@ -595,6 +599,7 @@
"chat": "Toggle chat window",
"document": "Toggle shared document",
"download": "Download our apps",
"e2ee": "End-to-End Encryption",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
"hangup": "Leave the call",
@@ -638,6 +643,7 @@
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"download": "Download our apps",
"e2ee": "End-to-End Encryption",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
"exitFullScreen": "Exit full screen",

View File

@@ -11,6 +11,7 @@ import {
setSubject
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { setE2EEKey } from '../../react/features/e2ee';
import { invite } from '../../react/features/invite';
import { toggleTileView } from '../../react/features/video-layout';
import { getJitsiMeetTransport } from '../transport';
@@ -166,6 +167,10 @@ function initCommands() {
} catch (err) {
logger.error('Failed sending endpoint text message', err);
}
},
'e2ee-key': key => {
logger.debug('Set E2EE key command received');
APP.store.dispatch(setE2EEKey(key));
}
};
transport.on('event', ({ data, name }) => {

View File

@@ -29,6 +29,7 @@ const ALWAYS_ON_TOP_FILENAMES = [
const commands = {
avatarUrl: 'avatar-url',
displayName: 'display-name',
e2eeKey: 'e2ee-key',
email: 'email',
hangup: 'video-hangup',
password: 'password',
@@ -236,6 +237,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* information about the initial devices that will be used in the call.
* @param {Object} [options.userInfo] - Object containing information about
* the participant opening the meeting.
* @param {string} [options.e2eeKey] - The key used for End-to-End encryption.
* THIS IS EXPERIMENTAL.
*/
constructor(domain, ...args) {
super();
@@ -251,7 +254,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
onload = undefined,
invitees,
devices,
userInfo
userInfo,
e2eeKey
} = parseArguments(args);
this._parentNode = parentNode;
@@ -276,6 +280,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
if (Array.isArray(invitees) && invitees.length > 0) {
this.invite(invitees);
}
this._tmpE2EEKey = e2eeKey;
this._isLargeVideoVisible = true;
this._numberOfParticipants = 0;
this._participants = {};
@@ -429,11 +434,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
const userID = data.id;
switch (name) {
case 'video-conference-joined':
case 'video-conference-joined': {
if (typeof this._tmpE2EEKey !== 'undefined') {
this.executeCommand(commands.e2eeKey, this._tmpE2EEKey);
this._tmpE2EEKey = undefined;
}
this._myUserID = userID;
this._participants[userID] = {
avatarURL: data.avatarURL
};
}
// eslint-disable-next-line no-fallthrough
case 'participant-joined': {

View File

@@ -322,15 +322,6 @@ UI.inputDisplayNameHandler = function(newDisplayName) {
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
};
/**
* Return the type of the remote video.
* @param jid the jid for the remote video
* @returns the video type video or screen.
*/
UI.getRemoteVideoType = function(jid) {
return VideoLayout.getRemoteVideoType(jid);
};
// FIXME check if someone user this
UI.showLoginPopup = function(callback) {
logger.log('password is required');

View File

@@ -19,7 +19,6 @@ export default class SharedVideoThumb extends SmallVideo {
this.id = participant.id;
this.isLocal = false;
this.url = participant.id;
this.setVideoType(videoType);
this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container);

View File

@@ -27,12 +27,15 @@ const UIUtil = {
*/
prependChild(container, newChild) {
const firstChild = container.childNodes[0];
let result;
if (firstChild) {
container.insertBefore(newChild, firstChild);
result = container.insertBefore(newChild, firstChild);
} else {
container.appendChild(newChild);
result = container.appendChild(newChild);
}
return result;
},
/**

View File

@@ -90,13 +90,13 @@ export default class RemoteVideo extends SmallVideo {
this._isRemoteControlSessionActive = false;
/**
* The flag is set to <tt>true</tt> after the 'onplay' event has been
* The flag is set to <tt>true</tt> after the 'canplay' event has been
* triggered on the current video element. It goes back to <tt>false</tt>
* when the stream is removed. It is used to determine whether the video
* playback has ever started.
* @type {boolean}
*/
this.wasVideoPlayed = false;
this._canPlayEventReceived = false;
/**
* The flag is set to <tt>true</tt> if remote participant's video gets muted
@@ -366,7 +366,7 @@ export default class RemoteVideo extends SmallVideo {
select.remove();
if (isVideo) {
this.wasVideoPlayed = false;
this._canPlayEventReceived = false;
}
logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
@@ -390,13 +390,10 @@ export default class RemoteVideo extends SmallVideo {
}
/**
* The remote video is considered "playable" once the stream has started
* according to the {@link #hasVideoStarted} result.
* It will be allowed to display video also in
* {@link JitsiParticipantConnectionStatus.INTERRUPTED} if the video was ever
* played and was not muted while not in ACTIVE state. This basically means
* that there is stalled video image cached that could be displayed. It's used
* to show "grey video image" in user's thumbnail when there are connectivity
* The remote video is considered "playable" once the can play event has been received. It will be allowed to
* display video also in {@link JitsiParticipantConnectionStatus.INTERRUPTED} if the video has received the canplay
* event and was not muted while not in ACTIVE state. This basically means that there is stalled video image cached
* that could be displayed. It's used to show "grey video image" in user's thumbnail when there are connectivity
* issues.
*
* @inheritdoc
@@ -406,7 +403,7 @@ export default class RemoteVideo extends SmallVideo {
const connectionState = APP.conference.getParticipantConnectionStatus(this.id);
return super.isVideoPlayable()
&& this.hasVideoStarted()
&& this._canPlayEventReceived
&& (connectionState === JitsiParticipantConnectionStatus.ACTIVE
|| (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED && !this.mutedWhileDisconnected));
}
@@ -459,26 +456,16 @@ export default class RemoteVideo extends SmallVideo {
return;
}
streamElement.onplaying = () => {
this.wasVideoPlayed = true;
streamElement.oncanplay = () => {
this._canPlayEventReceived = true;
this.VideoLayout.remoteVideoActive(streamElement, this.id);
streamElement.onplaying = null;
streamElement.oncanplay = undefined;
// Refresh to show the video
this.updateView();
};
}
/**
* Checks whether the video stream has started for this RemoteVideo instance.
*
* @returns {boolean} true if this RemoteVideo has a video stream for which
* the playback has been started.
*/
hasVideoStarted() {
return this.wasVideoPlayed;
}
/**
*
* @param {*} stream
@@ -494,20 +481,16 @@ export default class RemoteVideo extends SmallVideo {
isVideo ? this.videoStream = stream : this.audioStream = stream;
if (isVideo) {
this.setVideoType(stream.videoType);
}
if (!stream.getOriginalStream()) {
logger.debug('Remote video stream has no original stream');
return;
}
const streamElement = SmallVideo.createStreamElement(stream);
let streamElement = SmallVideo.createStreamElement(stream);
// Put new stream element always in front
UIUtils.prependChild(this.container, streamElement);
streamElement = UIUtils.prependChild(this.container, streamElement);
$(streamElement).hide();

View File

@@ -32,8 +32,6 @@ import {
const logger = require('jitsi-meet-logger').getLogger(__filename);
import UIEvents from '../../../service/UI/UIEvents';
/**
* Display mode constant used when video is being displayed on the small video.
* @type {number}
@@ -158,28 +156,6 @@ export default class SmallVideo {
return this.$container.is(':visible');
}
/**
* Sets the type of the video displayed by this instance.
* Note that this is a string without clearly defined or checked values, and
* it is NOT one of the strings defined in service/RTC/VideoType in
* lib-jitsi-meet.
* @param videoType 'camera' or 'desktop', or 'sharedvideo'.
*/
setVideoType(videoType) {
this.videoType = videoType;
}
/**
* Returns the type of the video displayed by this instance.
* Note that this is a string without clearly defined or checked values, and
* it is NOT one of the strings defined in service/RTC/VideoType in
* lib-jitsi-meet.
* @returns {String} 'camera', 'screen', 'sharedvideo', or undefined.
*/
getVideoType() {
return this.videoType;
}
/**
* Creates an audio or video element for a particular MediaStream.
*/
@@ -189,6 +165,7 @@ export default class SmallVideo {
if (isVideo) {
element.setAttribute('muted', 'true');
element.setAttribute('playsInline', 'true'); /* for Safari on iOS to work */
} else if (config.startSilent) {
element.muted = true;
}
@@ -451,7 +428,7 @@ export default class SmallVideo {
* or <tt>false</tt> otherwise.
*/
isCurrentlyOnLargeVideo() {
return this.VideoLayout.isCurrentlyOnLarge(this.id);
return APP.store.getState()['features/large-video']?.participantId === this.id;
}
/**
@@ -499,7 +476,7 @@ export default class SmallVideo {
hasVideo: Boolean(this.selectVideoElement().length),
connectionStatus: APP.conference.getParticipantConnectionStatus(this.id),
mutedWhileDisconnected: this.mutedWhileDisconnected,
wasVideoPlayed: this.wasVideoPlayed,
canPlayEventReceived: this._canPlayEventReceived,
videoStream: Boolean(this.videoStream),
isVideoMuted: this.isVideoMuted,
videoStreamMuted: this.videoStream ? this.videoStream.isMuted() : 'no stream'
@@ -639,39 +616,6 @@ export default class SmallVideo {
this.updateIndicators();
}
/**
* Adds a listener for onresize events for this video, which will monitor for
* resolution changes, will calculate the delay since the moment the listened
* is added, and will fire a RESOLUTION_CHANGED event.
*/
waitForResolutionChange() {
const beforeChange = window.performance.now();
const videos = this.selectVideoElement();
if (!videos || !videos.length || videos.length <= 0) {
return;
}
const video = videos[0];
const oldWidth = video.videoWidth;
const oldHeight = video.videoHeight;
video.onresize = () => {
// eslint-disable-next-line eqeqeq
if (video.videoWidth != oldWidth || video.videoHeight != oldHeight) {
// Only run once.
video.onresize = null;
const delay = window.performance.now() - beforeChange;
const emitter = this.VideoLayout.getEventEmitter();
if (emitter) {
emitter.emit(UIEvents.RESOLUTION_CHANGED, this.getId(), `${oldWidth}x${oldHeight}`,
`${video.videoWidth}x${video.videoHeight}`, delay);
}
}
};
}
/**
* Initalizes any browser specific properties. Currently sets the overflow
* property for Qt browsers on Windows to hidden, thus fixing the following

View File

@@ -1,12 +1,14 @@
/* global APP, $, interfaceConfig */
const logger = require('jitsi-meet-logger').getLogger(__filename);
import { VIDEO_TYPE } from '../../../react/features/base/media';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
import {
getLocalParticipant as getLocalParticipantFromStore,
getPinnedParticipant,
getParticipantById,
pinParticipant
} from '../../../react/features/base/participants';
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
@@ -73,9 +75,6 @@ const VideoLayout = {
emitter,
this._updateLargeVideoIfDisplayed.bind(this));
// sets default video type of local video
// FIXME container type is totally different thing from the video type
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
this.registerListeners();
},
@@ -221,10 +220,16 @@ const VideoLayout = {
* @returns {String} the video type video or screen.
*/
getRemoteVideoType(id) {
const smallVideo = VideoLayout.getSmallVideo(id);
const state = APP.store.getState();
const participant = getParticipantById(state, id);
if (participant?.isFakeParticipant) {
return SHARED_VIDEO_CONTAINER_TYPE;
}
return smallVideo ? smallVideo.getVideoType() : null;
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
return videoTrack?.videoType;
},
isPinned(id) {
@@ -308,12 +313,6 @@ const VideoLayout = {
addRemoteVideoContainer(id, remoteVideo) {
remoteVideos[id] = remoteVideo;
if (!remoteVideo.getVideoType()) {
// make video type the default one (camera)
// FIXME container type is not a video type
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
}
// Initialize the view
remoteVideo.updateView();
},
@@ -491,22 +490,6 @@ const VideoLayout = {
logger.info('Peer video type changed: ', id, newVideoType);
let smallVideo;
if (APP.conference.isLocalId(id)) {
if (!localVideoThumbnail) {
logger.warn('Local video not ready yet');
return;
}
smallVideo = localVideoThumbnail;
} else if (remoteVideos[id]) {
smallVideo = remoteVideos[id];
} else {
return;
}
smallVideo.setVideoType(newVideoType);
this._updateLargeVideoIfDisplayed(id, true);
},
@@ -584,17 +567,16 @@ const VideoLayout = {
}
const currentContainer = largeVideo.getCurrentContainer();
const currentContainerType = largeVideo.getCurrentContainerType();
const currentId = largeVideo.id;
const isOnLarge = this.isCurrentlyOnLarge(id);
const smallVideo = this.getSmallVideo(id);
const state = APP.store.getState();
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const videoStream = videoTrack?.jitsiTrack;
if (isOnLarge && !forceUpdate
&& LargeVideoManager.isVideoContainer(currentContainerType)
&& smallVideo) {
&& videoStream) {
const currentStreamId = currentContainer.getStreamID();
const newStreamId
= smallVideo.videoStream
? smallVideo.videoStream.getId() : null;
const newStreamId = videoStream?.getId() || null;
// FIXME it might be possible to get rid of 'forceUpdate' argument
if (currentStreamId !== newStreamId) {
@@ -603,42 +585,17 @@ const VideoLayout = {
}
}
if ((!isOnLarge || forceUpdate) && smallVideo) {
if (!isOnLarge || forceUpdate) {
const videoType = this.getRemoteVideoType(id);
// FIXME video type is not the same thing as container type
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
APP.API.notifyOnStageParticipantChanged(id);
}
let oldSmallVideo;
if (currentId) {
oldSmallVideo = this.getSmallVideo(currentId);
}
smallVideo.waitForResolutionChange();
if (oldSmallVideo) {
oldSmallVideo.waitForResolutionChange();
}
largeVideo.updateLargeVideo(
id,
smallVideo.videoStream,
videoStream,
videoType || VIDEO_TYPE.CAMERA
).then(() => {
// update current small video and the old one
smallVideo.updateView();
oldSmallVideo && oldSmallVideo.updateView();
}, () => {
// use clicked other video during update, nothing to do.
).catch(() => {
// do nothing
});
} else if (currentId) {
const currentSmallVideo = this.getSmallVideo(currentId);
currentSmallVideo && currentSmallVideo.updateView();
}
},
@@ -723,10 +680,6 @@ const VideoLayout = {
this.localFlipX = val;
},
getEventEmitter() {
return eventEmitter;
},
/**
* Handles user's features changes.
*/

34
package-lock.json generated
View File

@@ -2833,9 +2833,9 @@
}
},
"@jitsi/sdp-interop": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@jitsi/sdp-interop/-/sdp-interop-1.0.1.tgz",
"integrity": "sha512-OJm8IYsJKCYJBlC0geRHm2VHi8ow2k/3wOZ7n0lEOBfV6RWghZQzQWHdT/qrkbXB2EHcm40Oy91a5Bfz2m6ydA==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@jitsi/sdp-interop/-/sdp-interop-1.0.2.tgz",
"integrity": "sha512-3Ivl0iuGZ+R97GzgzKneuY45cVCmpYWaOc37uyg58GJK3IiY3+/kf9fdfOweyeZeB6wrNkjI8dfn+I3XQPxbkQ==",
"requires": {
"lodash.clonedeep": "4.5.0",
"sdp-transform": "2.3.0"
@@ -10654,8 +10654,8 @@
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-utils": {
"version": "github:jitsi/js-utils#0b2cef90613a74777fefd98d4ee3eda3879809ab",
"from": "github:jitsi/js-utils#0b2cef90613a74777fefd98d4ee3eda3879809ab",
"version": "github:jitsi/js-utils#df68966e3c65b5c57fcd2670da1326a2c77518d1",
"from": "github:jitsi/js-utils#df68966e3c65b5c57fcd2670da1326a2c77518d1",
"requires": {
"bowser": "2.7.0",
"js-md5": "0.7.3",
@@ -10884,37 +10884,21 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#0d058e50633d62c7b26114418e234f4a8ae9a78e",
"from": "github:jitsi/lib-jitsi-meet#0d058e50633d62c7b26114418e234f4a8ae9a78e",
"version": "github:jitsi/lib-jitsi-meet#f97c37d0140a0f12644ae29f4dd93757b8b8610f",
"from": "github:jitsi/lib-jitsi-meet#f97c37d0140a0f12644ae29f4dd93757b8b8610f",
"requires": {
"@jitsi/sdp-interop": "1.0.1",
"@jitsi/sdp-interop": "1.0.2",
"@jitsi/sdp-simulcast": "0.3.0",
"async": "0.9.0",
"current-executing-script": "0.1.3",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
"js-utils": "github:jitsi/js-utils#8567f86ec2774ae1d1c47b22e3435928cf5d9771",
"js-utils": "github:jitsi/js-utils#df68966e3c65b5c57fcd2670da1326a2c77518d1",
"lodash.isequal": "4.5.0",
"sdp-transform": "2.3.0",
"strophe.js": "1.3.4",
"strophejs-plugin-disco": "0.0.2",
"strophejs-plugin-stream-management": "github:jitsi/strophejs-plugin-stream-management#e719a02b4f83856c1530882084a4b048ee622d45",
"webrtc-adapter": "7.5.0"
},
"dependencies": {
"js-md5": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ=="
},
"js-utils": {
"version": "github:jitsi/js-utils#8567f86ec2774ae1d1c47b22e3435928cf5d9771",
"from": "github:jitsi/js-utils#8567f86ec2774ae1d1c47b22e3435928cf5d9771",
"requires": {
"bowser": "2.7.0",
"js-md5": "0.7.3",
"postis": "2.2.0"
}
}
}
},
"libflacjs": {

View File

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

View File

@@ -257,6 +257,20 @@ export function createDeviceChangedEvent(mediaType, deviceType) {
};
}
/**
* Creates an event indicating that an action related to E2EE occurred.
*
* @param {string} action - The action which occurred.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createE2EEEvent(action) {
return {
action,
actionSubject: 'e2ee'
};
}
/**
* Creates an event which specifies that the feedback dialog has been opened.
*
@@ -674,21 +688,6 @@ export function createStartMutedConfigurationEvent(
};
}
/**
* Creates an event which indicates the delay for switching between simulcast
* streams.
*
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createStreamSwitchDelayEvent(attributes) {
return {
action: 'stream.switch.delay',
attributes
};
}
/**
* Automatically changing the mute state of a media track in order to match
* the current stored state in redux.

View File

@@ -79,14 +79,15 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
viewBox = '0 0 100 100'
xmlns = 'http://www.w3.org/2000/svg'
xmlnsXlink = 'http://www.w3.org/1999/xlink'>
<foreignObject
height = '100%'
width = '100%'>
<span
className = 'avatar-foreign'>
{ initials }
</span>
</foreignObject>
<text
dominantBaseline = 'central'
fill = 'rgba(255,255,255,.6)'
fontSize = '40pt'
textAnchor = 'middle'
x = '50'
y = '50'>
{ initials }
</text>
</svg>
</div>
);

View File

@@ -31,6 +31,7 @@ import { isRoomValid } from './functions';
const DEFAULT_STATE = {
conference: undefined,
e2eeSupported: undefined,
joining: undefined,
leaving: undefined,
locked: undefined,
@@ -175,6 +176,7 @@ function _conferenceFailed(state, { conference, error }) {
return assign(state, {
authRequired,
conference: undefined,
e2eeSupported: undefined,
error,
joining: undefined,
leaving: undefined,
@@ -226,6 +228,9 @@ function _conferenceJoined(state, { conference }) {
* @type {JitsiConference}
*/
conference,
e2eeSupported: conference.isE2EESupported(),
joining: undefined,
leaving: undefined,

View File

@@ -17,6 +17,7 @@ import {
import {
areDeviceLabelsInitialized,
getDeviceIdByLabel,
getDeviceLabelById,
getDevicesFromURL,
setAudioOutputDeviceId
} from './functions';
@@ -216,6 +217,25 @@ export function setAudioInputDevice(deviceId) {
};
}
/**
* Sets the audio input device id and updates the settings
* so they are persisted across sessions.
*
* @param {string} deviceId - The id of the new audio input device.
* @returns {Function}
*/
export function setAudioInputDeviceAndUpdateSettings(deviceId) {
return function(dispatch, getState) {
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'audioInput');
dispatch(setAudioInputDevice(deviceId));
dispatch(updateSettings({
userSelectedMicDeviceId: deviceId,
userSelectedMicDeviceLabel: deviceLabel
}));
};
}
/**
* Updates the output device id.
*
@@ -244,6 +264,25 @@ export function setVideoInputDevice(deviceId) {
};
}
/**
* Sets the video input device id and updates the settings
* so they are persisted across sessions.
*
* @param {string} deviceId - The id of the new video input device.
* @returns {Function}
*/
export function setVideoInputDeviceAndUpdateSettings(deviceId) {
return function(dispatch, getState) {
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'videoInput');
dispatch(setVideoInputDevice(deviceId));
dispatch(updateSettings({
userSelectedCameraDeviceId: deviceId,
userSelectedCameraDeviceLabel: deviceLabel
}));
};
}
/**
* Signals to update the list of known audio and video devices.
*

View File

@@ -2,6 +2,7 @@
import React from 'react';
import {
KeyboardAvoidingView,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
@@ -53,7 +54,8 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
return (
<TouchableWithoutFeedback>
<View
<KeyboardAvoidingView
behavior = 'height'
style = { [
styles.overlay,
style
@@ -73,7 +75,7 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
</TouchableOpacity>
{ this._renderContent() }
</View>
</View>
</KeyboardAvoidingView>
</TouchableWithoutFeedback>
);
}

View File

@@ -107,7 +107,7 @@ class JitsiModal extends PureComponent<Props> {
position = { position }
show = { _show }>
<KeyboardAvoidingView
behavior = 'padding'
behavior = 'height'
style = { [
_headerStyles.page,
_styles.page,

View File

@@ -196,6 +196,13 @@ StateListenerRegistry.register(
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
(participant, propertyName, oldValue, newValue) => {
switch (propertyName) {
case 'features_e2ee':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
e2eeSupported: newValue
}));
break;
case 'features_jigasi':
store.dispatch(participantUpdated({
conference,

View File

@@ -129,6 +129,8 @@ MiddlewareRegistry.register(store => next => action => {
// TODO Remove the following calls to APP.UI once components interested
// in track mute changes are moved into React and/or redux.
if (typeof APP !== 'undefined') {
const result = next(action);
const { jitsiTrack } = action.track;
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();
@@ -151,6 +153,8 @@ MiddlewareRegistry.register(store => next => action => {
} else {
APP.UI.setAudioMuted(participantID, muted);
}
return result;
}
}

View File

@@ -10,6 +10,7 @@ import {
JitsiConferenceErrors,
JitsiConferenceEvents
} from '../base/lib-jitsi-meet';
import { setActiveModalId } from '../base/modal';
import {
getLocalParticipant,
getParticipantById,
@@ -22,7 +23,13 @@ import { isButtonEnabled, showToolbox } from '../toolbox';
import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
import { addMessage, clearMessages, toggleChat } from './actions';
import { ChatPrivacyDialog } from './components';
import { INCOMING_MSG_SOUND_ID, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from './constants';
import {
CHAT_VIEW_MODAL_ID,
INCOMING_MSG_SOUND_ID,
MESSAGE_TYPE_ERROR,
MESSAGE_TYPE_LOCAL,
MESSAGE_TYPE_REMOTE
} from './constants';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
declare var APP: Object;
@@ -94,6 +101,7 @@ MiddlewareRegistry.register(store => next => action => {
}
case SET_PRIVATE_MESSAGE_RECIPIENT: {
Boolean(action.participant) && dispatch(setActiveModalId(CHAT_VIEW_MODAL_ID));
_maybeFocusField();
break;
}

View File

@@ -0,0 +1,8 @@
/**
* The type of the action which signals the E2EE key has changed.
*
* {
* type: SET_E2EE_KEY
* }
*/
export const SET_E2EE_KEY = 'SET_E2EE_KEY';

View File

@@ -0,0 +1,16 @@
// @flow
import { SET_E2EE_KEY } from './actionTypes';
/**
* Dispatches an action to set the E2EE key.
*
* @param {string|undefined} key - The new key to be used for E2EE.
* @returns {Object}
*/
export function setE2EEKey(key: ?string) {
return {
type: SET_E2EE_KEY,
key
};
}

View File

@@ -0,0 +1,76 @@
// @flow
import React from 'react';
import { createE2EEEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { IconRoomUnlock } from '../../base/icons';
import { connect } from '../../base/redux';
import { AbstractButton, BetaTag } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox';
import E2EEDialog from './E2EEDialog';
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* Button that open a dialog to set the E2EE key.
*/
class E2EEButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.e2ee';
icon = IconRoomUnlock;
label = 'toolbar.e2ee';
tooltip = 'toolbar.e2ee';
/**
* Helper function to be implemented by subclasses, which returns
* a React Element to display (a beta tag) at the end of the button.
*
* @override
* @protected
* @returns {ReactElement}
*/
_getElementAfter() {
return <BetaTag />;
}
/**
* Handles clicking / pressing the button, and opens the E2EE dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
sendAnalytics(createE2EEEvent('dialog.open'));
this.props.dispatch(openDialog(E2EEDialog));
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the Component.
* @private
* @returns {Props}
*/
export function mapStateToProps(state: Object, ownProps: Props) {
const { e2eeSupported } = state['features/base/conference'];
const { visible = Boolean(e2eeSupported) } = ownProps;
return {
visible
};
}
export default translate(connect(mapStateToProps)(E2EEButton));

View File

@@ -0,0 +1,156 @@
/* @flow */
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { FieldTextStateless as TextField } from '@atlaskit/field-text';
import { createE2EEEvent, sendAnalytics } from '../../analytics';
import { Dialog } from '../../base/dialog';
import { translate, translateToHTML } from '../../base/i18n';
import { getParticipants } from '../../base/participants';
import { connect } from '../../base/redux';
import { setE2EEKey } from '../actions';
type Props = {
/**
* Indicates whether all participants in the conference currently support E2EE.
*/
_everyoneSupportsE2EE: boolean,
/**
* The current E2EE key.
*/
_key: string,
/**
* The redux {@code dispatch} function.
*/
dispatch: Dispatch<any>,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
type State = {
/**
* The current E2EE key.
*/
key: string
};
/**
* Implements a React {@code Component} for displaying a dialog with a field
* for setting the E2EE key.
*
* @extends Component
*/
class E2EEDialog extends Component<Props, State> {
/**
* Initializes a new {@code E2EEDialog } instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
key: this.props._key
};
// Bind event handlers so they are only bound once for every instance.
this._onKeyChange = this._onKeyChange.bind(this);
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _everyoneSupportsE2EE, t } = this.props;
return (
<Dialog
isModal = { false }
onSubmit = { this._onSubmit }
titleKey = 'dialog.e2eeTitle'
width = 'small'>
<div className = 'e2ee-destription'>
{ translateToHTML(t, 'dialog.e2eeDescription') }
</div>
{
!_everyoneSupportsE2EE
&& <div className = 'e2ee-warn'>
{ translateToHTML(t, 'dialog.e2eeWarning') }
</div>
}
<TextField
autoFocus = { true }
compact = { true }
label = { t('dialog.e2eeLabel') }
name = 'e2eeKey'
onChange = { this._onKeyChange }
shouldFitContainer = { true }
type = 'password'
value = { this.state.key } />
</Dialog>);
}
_onKeyChange: (Object) => void;
/**
* Updates the entered key.
*
* @param {Object} event - The DOM event triggered from the entered value having changed.
* @private
* @returns {void}
*/
_onKeyChange(event) {
this.setState({ key: event.target.value.trim() });
}
_onSubmit: () => boolean;
/**
* Dispatches an action to update the E2EE key.
*
* @private
* @returns {boolean}
*/
_onSubmit() {
const { key } = this.state;
sendAnalytics(createE2EEEvent(`key.${key ? 'set' : 'unset'}`));
this.props.dispatch(setE2EEKey(key));
return true;
}
}
/**
* Maps (parts of) the Redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state) {
const { e2eeKey } = state['features/e2ee'];
const participants = getParticipants(state).filter(p => !p.local);
return {
_everyoneSupportsE2EE: participants.every(p => Boolean(p.e2eeSupported)),
_key: e2eeKey || ''
};
}
export default translate(connect(mapStateToProps)(E2EEDialog));

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../base/logging/functions';
export default getLogger('features/e2ee');

View File

@@ -0,0 +1,43 @@
// @flow
import { getCurrentConference } from '../base/conference';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { SET_E2EE_KEY } from './actionTypes';
import { setE2EEKey } from './actions';
import logger from './logger';
/**
* Middleware that captures actions related to E2EE.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ getState }) => next => action => {
switch (action.type) {
case SET_E2EE_KEY: {
const conference = getCurrentConference(getState);
if (conference) {
logger.debug(`New E2EE key: ${action.key}`);
conference.setE2EEKey(action.key);
}
break;
}
}
return next(action);
});
/**
* Set up state change listener to perform maintenance tasks when the conference
* is left or failed.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch }, previousConference) => {
if (previousConference) {
dispatch(setE2EEKey(undefined));
}
});

View File

@@ -0,0 +1,29 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { SET_E2EE_KEY } from './actionTypes';
const DEFAULT_STATE = {
/**
* E2EE key.
*/
e2eeKey: undefined
};
/**
* Reduces the Redux actions of the feature features/e2ee.
*/
ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_E2EE_KEY:
return {
...state,
e2eeKey: action.key
};
default:
return state;
}
});

View File

@@ -1,7 +1,9 @@
// @flow
import { MEDIA_TYPE, VIDEO_TYPE } from '../base/media';
import { getLocalParticipant } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { getTrackByMediaTypeAndParticipant } from '../base/tracks';
import { appendSuffix } from '../display-name';
import { shouldDisplayTileView } from '../video-layout';
@@ -37,3 +39,18 @@ StateListenerRegistry.register(
});
}
});
/**
* Updates the on stage participant value.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/large-video'].participantId,
/* listener */ (participantId, store) => {
const videoTrack = getTrackByMediaTypeAndParticipant(
store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
if (videoTrack && videoTrack.videoType === VIDEO_TYPE.CAMERA) {
APP.API.notifyOnStageParticipantChanged(participantId);
}
}
);

View File

@@ -2,6 +2,7 @@
import { StateListenerRegistry, equals } from '../base/redux';
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions';
@@ -56,3 +57,22 @@ StateListenerRegistry.register(
break;
}
});
/**
* Handles on stage participant updates.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/large-video'].participantId,
/* listener */ (participantId, store, oldParticipantId) => {
const newThumbnail = VideoLayout.getSmallVideo(participantId);
const oldThumbnail = VideoLayout.getSmallVideo(oldParticipantId);
if (newThumbnail) {
newThumbnail.updateView();
}
if (oldThumbnail) {
oldThumbnail.updateView();
}
}
);

View File

@@ -63,7 +63,8 @@ class LargeVideo extends Component<Props> {
<video
autoPlay = { !this.props._noAutoPlayVideo }
id = 'largeVideo'
muted = { true } />
muted = { true }
playsInline = { true } /* for Safari on iOS to work */ />
</div>
</div>
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES

View File

@@ -1,5 +1,7 @@
// @flow
import { Platform } from 'react-native';
import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { IconMenuDown } from '../../../base/icons';
@@ -62,8 +64,17 @@ class PictureInPictureButton extends AbstractButton<Props, *> {
* }}
*/
function _mapStateToProps(state): Object {
const flag = Boolean(getFeatureFlag(state, PIP_ENABLED));
let enabled = flag;
// Override flag for Android < 26, PiP was introduced in Oreo.
// https://developer.android.com/guide/topics/ui/picture-in-picture
if (Platform.OS === 'android' && Platform.Version < 26) {
enabled = false;
}
return {
_enabled: Boolean(getFeatureFlag(state, PIP_ENABLED))
_enabled: enabled
};
}

View File

@@ -8,7 +8,7 @@ import { toggleAudioSettings } from '../../../actions';
import {
getAudioInputDeviceData,
getAudioOutputDeviceData,
setAudioInputDevice as setAudioInputDeviceAction,
setAudioInputDeviceAndUpdateSettings,
setAudioOutputDevice as setAudioOutputDeviceAction
} from '../../../../base/devices';
import { connect } from '../../../../base/redux';
@@ -90,7 +90,7 @@ function mapStateToProps(state) {
const mapDispatchToProps = {
onClose: toggleAudioSettings,
setAudioInputDevice: setAudioInputDeviceAction,
setAudioInputDevice: setAudioInputDeviceAndUpdateSettings,
setAudioOutputDevice: setAudioOutputDeviceAction
};

View File

@@ -6,7 +6,7 @@ import InlineDialog from '@atlaskit/inline-dialog';
import { toggleVideoSettings } from '../../../actions';
import {
getVideoDeviceIds,
setVideoInputDevice as setVideoInputDeviceAction
setVideoInputDeviceAndUpdateSettings
} from '../../../../base/devices';
import { getVideoSettingsVisibility } from '../../../functions';
import { connect } from '../../../../base/redux';
@@ -79,7 +79,7 @@ function mapStateToProps(state) {
const mapDispatchToProps = {
onClose: toggleVideoSettings,
setVideoInputDevice: setVideoInputDeviceAction
setVideoInputDevice: setVideoInputDeviceAndUpdateSettings
};
export default connect(mapStateToProps, mapDispatchToProps)(VideoSettingsPopup);

View File

@@ -33,6 +33,7 @@ import { OverflowMenuItem } from '../../../base/toolbox';
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
import { VideoBlurButton } from '../../../blur';
import { ChatCounter, toggleChat } from '../../../chat';
import { E2EEButton } from '../../../e2ee';
import { SharedDocumentButton } from '../../../etherpad';
import { openFeedbackDialog } from '../../../feedback';
import {
@@ -1012,6 +1013,10 @@ class Toolbox extends Component<Props, State> {
key = 'stats'
onClick = { this._onToolbarOpenSpeakerStats }
text = { t('toolbar.speakerStats') } />,
this._shouldShowButton('e2ee')
&& <E2EEButton
key = 'e2ee'
showLabel = { true } />,
this._shouldShowButton('feedback')
&& _feedbackConfigured
&& <OverflowMenuItem

View File

@@ -50,13 +50,18 @@ if [ -f /etc/nginx/sites-enabled/$DOMAIN.conf ] ; then
cp /usr/share/jitsi-meet-turnserver/coturn-certbot-deploy.sh $TURN_HOOK
chmod u+x $TURN_HOOK
sed -i "s/jitsi-meet.example.com/$DOMAIN/g" $TURN_HOOK
fi
./certbot-auto certonly --noninteractive \
--webroot --webroot-path /usr/share/jitsi-meet \
-d $DOMAIN \
--agree-tos --email $EMAIL \
--deploy-hook $TURN_HOOK
./certbot-auto certonly --noninteractive \
--webroot --webroot-path /usr/share/jitsi-meet \
-d $DOMAIN \
--agree-tos --email $EMAIL \
--deploy-hook $TURN_HOOK
else
./certbot-auto certonly --noninteractive \
--webroot --webroot-path /usr/share/jitsi-meet \
-d $DOMAIN \
--agree-tos --email $EMAIL
fi
echo "Configuring nginx"

View File

@@ -53,10 +53,6 @@ export default {
*/
LOCAL_FLIPX_CHANGED: 'UI.local_flipx_changed',
// An event which indicates that the resolution of a remote video has
// changed.
RESOLUTION_CHANGED: 'UI.resolution_changed',
/**
* Notifies that the button "Cancel" is pressed on the dialog for
* external extension installation.