Compare commits

...

42 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
dede23f5b4 ios: fix SDK build after dropping iOS 10
Since we only support iOS 11, 32 bit architectures must not be built.
2019-12-13 13:04:00 +01:00
Bettenbuk Zoltan
c669075265 feat: add swipe handler to entire bottom sheet 2019-12-12 17:15:50 +01:00
Saúl Ibarra Corretgé
55983ff62a rn,welcome: update join button text 2019-12-10 15:13:37 +01:00
damencho
b4be1bcd05 Adds some checks about async.
There are modules that will not work with prosody 0.10 as they depend on util.async. Adds a safeguard and print error about it in the logs.
And others that just do not work because of the muc module API that they use.
2019-12-10 10:55:56 +01:00
damencho
2420a68be9 Enables speakerstats component and module by default. 2019-12-10 10:55:56 +01:00
damencho
ebfc5a95ff Activates multidomain by default when installing with nginx. 2019-12-10 10:55:56 +01:00
Leonard Kim
68cad276bd fix(lock): ensure lock prompt is closed on password submit
This addresses a bug, in which submitting a password
through the iframe api no longer closes RoomLockPrompt,
by explicitly closing prompts for a lock or password.
2019-12-09 08:44:18 -08:00
Bettenbuk Zoltan
e683d70a18 Add support for avatar status badge (presence) 2019-12-09 11:58:23 +01:00
Andrei Gavrilescu
9645391180 update package links 2019-12-06 11:37:08 +00:00
Andrei Gavrilescu
851b1a76a9 Address code review 2019-12-06 11:37:08 +00:00
Andrei Gavrilescu
4890390ea2 fix action uid name / remove imports 2019-12-06 11:37:08 +00:00
Andrei Gavrilescu
7828bf8d46 setNoSrcDataNotificationUid 2019-12-06 11:37:08 +00:00
Andrei Gavrilescu
191da551e3 refactor / address code review 2019-12-06 11:37:08 +00:00
Andrei Gavrilescu
55f35933e8 address code review 2019-12-06 11:37:08 +00:00
Andrei Gavrilescu
b125bff7c7 refactor / enable VAD talk while muted 2019-12-06 11:37:08 +00:00
Andrei Gavrilescu
c1d261445e Initial commit 2019-12-06 11:37:08 +00:00
Andrei Gavrilescu
c494d6c48b feat: show no audio signal notification 2019-12-06 11:37:08 +00:00
Saúl Ibarra Corretgé
4134d47f6e recording: remove beta label from LiveStreamButton 2019-12-05 16:23:27 +01:00
Jaya Allamsetty
0b25e62c5c fix: Reuse the existing JitsiLocalTrack on presenter unmute 2019-12-05 09:25:34 -05:00
damencho
4d0cbff5a1 Ignore errors when restarting services.
Sometimes conflicting or wrong configuration can leave the package in broken state and users cannot even uninstall/purge the packages, and it also breaks any other package installation.
2019-12-04 17:21:12 +00:00
damencho
c79463aaee Fixes including config.js template. 2019-12-04 17:21:12 +00:00
damencho
339e1c5fab Moves config template files out of doc folder. 2019-12-04 09:50:55 +00:00
Saúl Ibarra Corretgé
36455c24c8 auth: fix rendering error and progress messages
Also removed some no longer used styles.
2019-12-03 14:33:26 +01:00
Saúl Ibarra Corretgé
a622a4c713 android: handle ConnectionService failures more resiliently
Some Samsung devices will fail to fully engage ConnectionService if no SIM card
was ever installed on the device. We could check for it, but it would require
the CALL_PHONE permission, which is not something we want to do, so fallback to
not using ConnectionService.
2019-12-03 11:56:04 +01:00
Hristo Terezov
1aaaae24ee feat(Amplitude): enable referrer 2019-11-29 15:43:14 +00:00
Hristo Terezov
9191000da4 chore(package.json): Update lib-jitsi-meet 2019-11-29 13:41:14 +00:00
Bettenbuk Zoltan
8eb93086bd fix: set an avatar icon size relative to the container 2019-11-29 14:37:35 +01:00
Bettenbuk Zoltan
b64294af6d fix: emoji in links 2019-11-29 14:36:42 +01:00
Hristo Terezov
bbf33a8895 feat(welcome-page): Add viewed analytics event. 2019-11-28 15:36:12 +00:00
Jaya Allamsetty
bcc1289a23 feat(presenter): Modify the default behavior for presenter mode, it set to off when screensharing is turned on. Also, revert video to the original state when screensharing is turned off. 2019-11-27 11:13:36 -08:00
Saúl Ibarra Corretgé
58bd48c1ae android: disable ConnectionService if permissions are not granted
Some devices seem to have a bug in their Android versions and startCall fails
with SecurityError because the CALL_PHONE permissions is not granted. This is
not a requirement for self-managed connection services as per the official
documentation though:
https://developer.android.com/guide/topics/connectivity/telecom/selfManaged

Alas, connection services takes over audio device management too, so let's
handle the error and disable CS if we get SecurityError.
2019-11-27 14:33:25 +01:00
Saúl Ibarra Corretgé
1a3736bf98 android: unregister phone account if startCall fails 2019-11-27 14:33:25 +01:00
Saúl Ibarra Corretgé
0eec182df4 android: remove old code for accepting SDK license
It can now be automated in a CI environment as follows:

yes | sdkmanager --licenses
2019-11-27 14:24:29 +01:00
Saúl Ibarra Corretgé
c526844eb2 chore: remove unused images 2019-11-26 21:20:50 +01:00
Saúl Ibarra Corretgé
d856c1f328 ios: add apple-touch-icon icon
Ref: https://webhint.io/docs/user-guide/hints/hint-apple-touch-icons/
2019-11-26 21:13:02 +01:00
Saúl Ibarra Corretgé
15e47a9eb3 android: update native dependencies 2019-11-26 20:33:38 +01:00
Saúl Ibarra Corretgé
da98d39b61 doc: add app download badges to README 2019-11-26 14:58:35 +01:00
Bettenbuk Zoltan
411bafb5a6 feat: minimized bottom menu 2019-11-26 12:08:43 +01:00
Jaya Allamsetty
0a64bf2068 feat(presenter): add Presenter Mode
- Adds the ability to share video as a "PiP" when screenshare is in progress.
- Add a method for creating a local presenter track.
- Make sure isLocalVideoTrackMuted returns the correct mute state when only screenshare is present.
- Make sure we get the updated window size of the window being shared before painting it on the canvas.
- Make sure we check if the shared window has been resized
2019-11-26 11:57:03 +01:00
damencho
db6a2673de Handles unique Id for a meeting. 2019-11-26 10:37:19 +00:00
damencho
e11d4d3101 Installs prosody plugins with jitsi-meet-prosody package. 2019-11-26 10:37:19 +00:00
Saúl Ibarra Corretgé
8fd3bb2302 android: fallbacck to speaker in ConnectionService handler
It has been our default for a while.
2019-11-26 11:30:18 +01:00
117 changed files with 1779 additions and 1321 deletions

View File

@@ -29,11 +29,18 @@ You can download source archives (produced by ```make source-package```):
### Mobile apps
You can get our mobile versions from here:
* [Android](https://play.google.com/store/apps/details?id=org.jitsi.meet)
[<img src="resources/img/google-play-badge.png" height="50">](https://play.google.com/store/apps/details?id=org.jitsi.meet)
* [Android (F-Droid)](https://f-droid.org/en/packages/org.jitsi.meet/)
[<img src="resources/img/f-droid-badge.png" height="50">](https://f-droid.org/en/packages/org.jitsi.meet/)
* [iOS](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
[<img src="resources/img/appstore-badge.png" height="50">](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
You can also sign up for our open beta testing here:
* [Android](https://play.google.com/apps/testing/org.jitsi.meet)

View File

@@ -69,7 +69,9 @@ repositories {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.appcompat:appcompat:1.1.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-5'
if (!rootProject.ext.libreBuild) {
implementation 'com.google.android.gms:play-services-auth:16.0.1'
@@ -83,9 +85,6 @@ dependencies {
}
implementation project(':sdk')
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
}
gradle.projectsEvaluated {

View File

@@ -5,7 +5,6 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".MainApplication"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/AppTheme">
<activity

View File

@@ -1,36 +0,0 @@
/*
* Copyright @ 2018-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet;
import android.app.Application;
import com.squareup.leakcanary.LeakCanary;
/**
* Simple {@link Application} for hooking up LeakCanary:
* https://github.com/square/leakcanary
*/
public class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (!LeakCanary.isInAnalyzerProcess(this)) {
LeakCanary.install(this);
}
}
}

View File

@@ -13,8 +13,8 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.27.0'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'io.fabric.tools:gradle:1.28.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files.
@@ -165,50 +165,6 @@ ext {
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
}
// If Android SDK is not installed, accept its license so that it
// is automatically downloaded.
afterEvaluate { project ->
// Either the environment variable ANDROID_HOME or the property sdk.dir in
// local.properties identifies where Android SDK is installed.
def androidHome = System.env.ANDROID_HOME
if (!androidHome) {
// ANDROID_HOME is not set. Is sdk.dir set?
def file = file("${project.rootDir}/local.properties")
def props = new Properties()
if (file.canRead()) {
file.withInputStream {
props.load(it)
androidHome = props.'sdk.dir'
}
}
if (!androidHome && (!file.exists() || file.canWrite())) {
// Neither ANDROID_HOME nor sdk.dir is set. Set sdk.dir (because
// environment variables cannot be set).
props.'sdk.dir' = "${project.buildDir}/android-sdk".toString()
file.withOutputStream {
props.store(it, null)
androidHome = props.'sdk.dir'
}
}
}
// If the license is not accepted, accept it so that automatic downloading
// kicks in.
// The license hash can be taken from the accepted licenses, by doing this
// on your local machine the file is
// ${androidHome}/licenses/android-sdk-license
if (androidHome) {
def dir = file("${androidHome}/licenses")
dir.mkdirs()
def file = file("${dir.path}/android-sdk-license")
if (!file.exists()) {
file.withWriter {
def hash = 'd56f5187479451eabf01fb78af6dfcb131a6481e'
it.write(hash, 0, hash.length())
}
}
}
}
// Force the version of the Android build tools we have chosen on all
// subprojects. The forcing was introduced for react-native and the third-party
// modules that we utilize such as react-native-background-timer.

View File

@@ -37,10 +37,12 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.fragment:fragment:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.fragment:fragment:1.1.0'
//noinspection GradleDynamicVersion
api 'com.facebook.react:react-native:+'
//noinspection GradleDynamicVersion
implementation 'org.webkit:android-jsc:+'
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'

View File

@@ -53,7 +53,7 @@ class AudioDeviceHandlerConnectionService implements
*/
private static int audioDeviceToRouteInt(String audioDevice) {
if (audioDevice == null) {
return CallAudioState.ROUTE_EARPIECE;
return CallAudioState.ROUTE_SPEAKER;
}
switch (audioDevice) {
case AudioModeModule.DEVICE_BLUETOOTH:
@@ -66,7 +66,7 @@ class AudioDeviceHandlerConnectionService implements
return CallAudioState.ROUTE_SPEAKER;
default:
JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
return CallAudioState.ROUTE_EARPIECE;
return CallAudioState.ROUTE_SPEAKER;
}
}

View File

@@ -123,14 +123,17 @@ public class ConnectionService extends android.telecom.ConnectionService {
* {@link android.telecom.Connection#STATE_ACTIVE}.
*
* @param callUUID the call UUID which identifies the connection.
* @return Whether the connection was set as active or not.
*/
static void setConnectionActive(String callUUID) {
static boolean setConnectionActive(String callUUID) {
ConnectionImpl connection = connections.get(callUUID);
if (connection != null) {
connection.setActive();
return true;
} else {
JitsiMeetLogger.e("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
JitsiMeetLogger.w("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
return false;
}
}

View File

@@ -105,15 +105,22 @@ class RNConnectionService extends ReactContextBaseJavaModule {
ConnectionService.registerStartCallPromise(callUUID, promise);
try {
TelecomManager tm
= (TelecomManager) ctx.getSystemService(
Context.TELECOM_SERVICE);
TelecomManager tm = null;
try {
tm = (TelecomManager) ctx.getSystemService(Context.TELECOM_SERVICE);
tm.placeCall(address, extras);
} catch (Exception e) {
JitsiMeetLogger.e(e, TAG + " error in startCall");
if (tm != null) {
tm.unregisterPhoneAccount(accountHandle);
}
ConnectionService.unregisterStartCallPromise(callUUID);
promise.reject(e);
if (e instanceof SecurityException) {
promise.reject("SECURITY_ERROR", "Required permissions not granted.");
} else {
promise.reject(e);
}
}
}
@@ -151,8 +158,11 @@ class RNConnectionService extends ReactContextBaseJavaModule {
@ReactMethod
public void reportConnectedOutgoingCall(String callUUID, Promise promise) {
JitsiMeetLogger.d(TAG + " reportConnectedOutgoingCall " + callUUID);
ConnectionService.setConnectionActive(callUUID);
promise.resolve(null);
if (ConnectionService.setConnectionActive(callUUID)) {
promise.resolve(null);
} else {
promise.reject("CONNECTION_NOT_FOUND_ERROR", "Connection wasn't found.");
}
}
@Override

View File

@@ -93,10 +93,15 @@ import {
participantRoleChanged,
participantUpdated
} from './react/features/base/participants';
import { updateSettings } from './react/features/base/settings';
import {
getUserSelectedCameraDeviceId,
updateSettings
} from './react/features/base/settings';
import {
createLocalPresenterTrack,
createLocalTracksF,
destroyLocalTracks,
isLocalVideoTrackMuted,
isLocalTrackMuted,
isUserInteractionRequiredForUnmute,
replaceLocalTrack,
@@ -113,7 +118,9 @@ import {
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor';
import { setSharedVideoStatus } from './react/features/shared-video';
import { createPresenterEffect } from './react/features/stream-effects/presenter';
import { endpointMessageReceived } from './react/features/subtitles';
import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -437,6 +444,11 @@ export default {
*/
localAudio: null,
/**
* The local presenter video track (if any).
*/
localPresenterVideo: null,
/**
* The local video track (if any).
* FIXME tracks from redux store should be the single source of truth, but
@@ -468,14 +480,18 @@ export default {
audioOnlyError,
screenSharingError,
videoOnlyError;
const initialDevices = [];
let requestedAudio = false;
const initialDevices = [ 'audio' ];
const requestedAudio = true;
let requestedVideo = false;
if (!options.startWithAudioMuted) {
initialDevices.push('audio');
requestedAudio = true;
// Always get a handle on the audio input device so that we have statistics even if the user joins the
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
// only after that point.
if (options.startWithAudioMuted) {
this.muteAudio(true, true);
}
if (!options.startWithVideoMuted
&& !options.startAudioOnly
&& !options.startScreenSharing) {
@@ -722,9 +738,8 @@ export default {
isLocalVideoMuted() {
// If the tracks are not ready, read from base/media state
return this._localTracksInitialized
? isLocalTrackMuted(
APP.store.getState()['features/base/tracks'],
MEDIA_TYPE.VIDEO)
? isLocalVideoTrackMuted(
APP.store.getState()['features/base/tracks'])
: isVideoMutedByUser(APP.store);
},
@@ -798,6 +813,35 @@ export default {
this.muteAudio(!this.isLocalAudioMuted(), showUI);
},
/**
* Simulates toolbar button click for presenter video mute. Used by
* shortcuts and API.
* @param mute true for mute and false for unmute.
* @param {boolean} [showUI] when set to false will not display any error
* dialogs in case of media permissions error.
*/
async mutePresenter(mute, showUI = true) {
const maybeShowErrorDialog = error => {
showUI && APP.store.dispatch(notifyCameraError(error));
};
if (mute) {
try {
await this.localVideo.setEffect(undefined);
} catch (err) {
logger.error('Failed to remove the presenter effect', err);
maybeShowErrorDialog(err);
}
} else {
try {
await this.localVideo.setEffect(await this._createPresenterStreamEffect());
} catch (err) {
logger.error('Failed to apply the presenter effect', err);
maybeShowErrorDialog(err);
}
}
},
/**
* Simulates toolbar button click for video mute. Used by shortcuts and API.
* @param mute true for mute and false for unmute.
@@ -812,6 +856,10 @@ export default {
return;
}
if (this.isSharingScreen) {
return this._mutePresenterVideo(mute);
}
// If not ready to modify track's state yet adjust the base/media
if (!this._localTracksInitialized) {
// This will only modify base/media.video.muted which is then synced
@@ -1220,6 +1268,7 @@ export default {
options.applicationName = interfaceConfig.APP_NAME;
options.getWiFiStatsMethod = this._getWiFiStatsMethod;
options.confID = `${locationURL.host}${locationURL.pathname}`;
options.createVADProcessor = createRnnoiseProcessorPromise;
return options;
},
@@ -1351,7 +1400,7 @@ export default {
* in case it fails.
* @private
*/
_turnScreenSharingOff(didHaveVideo, wasVideoMuted) {
_turnScreenSharingOff(didHaveVideo) {
this._untoggleScreenSharing = null;
this.videoSwitchInProgress = true;
const { receiver } = APP.remoteControl;
@@ -1369,13 +1418,7 @@ export default {
.then(([ stream ]) => this.useVideoStream(stream))
.then(() => {
sendAnalytics(createScreenSharingEvent('stopped'));
logger.log('Screen sharing stopped, switching to video.');
if (!this.localVideo && wasVideoMuted) {
return Promise.reject('No local video to be muted!');
} else if (wasVideoMuted && this.localVideo) {
return this.localVideo.mute();
}
logger.log('Screen sharing stopped.');
})
.catch(error => {
logger.error('failed to switch back to local video', error);
@@ -1390,6 +1433,16 @@ export default {
promise = this.useVideoStream(null);
}
// mute the presenter track if it exists.
if (this.localPresenterVideo) {
APP.store.dispatch(
setVideoMuted(true, MEDIA_TYPE.PRESENTER));
this.localPresenterVideo.dispose();
APP.store.dispatch(
trackRemoved(this.localPresenterVideo));
this.localPresenterVideo = null;
}
return promise.then(
() => {
this.videoSwitchInProgress = false;
@@ -1415,7 +1468,7 @@ export default {
* 'window', etc.).
* @return {Promise.<T>}
*/
toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
if (this.videoSwitchInProgress) {
return Promise.reject('Switch in progress.');
}
@@ -1429,7 +1482,15 @@ export default {
}
if (toggle) {
return this._switchToScreenSharing(options);
try {
await this._switchToScreenSharing(options);
return;
} catch (err) {
logger.error('Failed to switch to screensharing', err);
return;
}
}
return this._untoggleScreenSharing
@@ -1454,8 +1515,7 @@ export default {
_createDesktopTrack(options = {}) {
let externalInstallation = false;
let DSExternalInstallationInProgress = false;
const didHaveVideo = Boolean(this.localVideo);
const wasVideoMuted = this.isLocalVideoMuted();
const didHaveVideo = !this.isLocalVideoMuted();
const getDesktopStreamPromise = options.desktopStream
? Promise.resolve([ options.desktopStream ])
@@ -1506,8 +1566,7 @@ export default {
// Stores the "untoggle" handler which remembers whether was
// there any video before and whether was it muted.
this._untoggleScreenSharing
= this._turnScreenSharingOff
.bind(this, didHaveVideo, wasVideoMuted);
= this._turnScreenSharingOff.bind(this, didHaveVideo);
desktopStream.on(
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => {
@@ -1532,6 +1591,77 @@ export default {
});
},
/**
* Creates a new instance of presenter effect. A new video track is created
* using the new set of constraints that are calculated based on
* the height of the desktop that is being currently shared.
*
* @param {number} height - The height of the desktop stream that is being
* currently shared.
* @param {string} cameraDeviceId - The device id of the camera to be used.
* @return {Promise<JitsiStreamPresenterEffect>} - A promise resolved with
* {@link JitsiStreamPresenterEffect} if it succeeds.
*/
async _createPresenterStreamEffect(height = null, cameraDeviceId = null) {
if (!this.localPresenterVideo) {
try {
this.localPresenterVideo = await createLocalPresenterTrack({ cameraDeviceId }, height);
} catch (err) {
logger.error('Failed to create a camera track for presenter', err);
return;
}
APP.store.dispatch(trackAdded(this.localPresenterVideo));
}
try {
const effect = await createPresenterEffect(this.localPresenterVideo.stream);
return effect;
} catch (err) {
logger.error('Failed to create the presenter effect', err);
}
},
/**
* Tries to turn the presenter video track on or off. If a presenter track
* doesn't exist, a new video track is created.
*
* @param mute - true for mute and false for unmute.
*
* @private
*/
async _mutePresenterVideo(mute) {
const maybeShowErrorDialog = error => {
APP.store.dispatch(notifyCameraError(error));
};
if (!this.localPresenterVideo && !mute) {
// create a new presenter track and apply the presenter effect.
const { height } = this.localVideo.track.getSettings();
const defaultCamera
= getUserSelectedCameraDeviceId(APP.store.getState());
let effect;
try {
effect = await this._createPresenterStreamEffect(height,
defaultCamera);
} catch (err) {
logger.error('Failed to unmute Presenter Video');
maybeShowErrorDialog(err);
return;
}
try {
await this.localVideo.setEffect(effect);
APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
} catch (err) {
logger.error('Failed to apply the Presenter effect', err);
}
} else {
APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
}
},
/**
* Tries to switch to the screensharing mode by disposing camera stream and
* replacing it with a desktop one.
@@ -1992,36 +2122,62 @@ export default {
const videoWasMuted = this.isLocalVideoMuted();
sendAnalytics(createDeviceChangedEvent('video', 'input'));
createLocalTracksF({
devices: [ 'video' ],
cameraDeviceId,
micDeviceId: null
})
.then(([ stream ]) => {
// if we are in audio only mode or video was muted before
// changing device, then mute
if (this.isAudioOnly() || videoWasMuted) {
return stream.mute()
.then(() => stream);
}
return stream;
})
.then(stream => {
// if we are screen sharing we do not want to stop it
if (this.isSharingScreen) {
return Promise.resolve();
}
// If both screenshare and video are in progress, restart the
// presenter mode with the new camera device.
if (this.isSharingScreen && !videoWasMuted) {
const { height } = this.localVideo.track.getSettings();
return this.useVideoStream(stream);
})
.then(() => {
// dispose the existing presenter track and create a new
// camera track.
this.localPresenterVideo.dispose();
this.localPresenterVideo = null;
return this._createPresenterStreamEffect(height, cameraDeviceId)
.then(effect => this.localVideo.setEffect(effect))
.then(() => {
this.setVideoMuteStatus(false);
logger.log('switched local video device');
this._updateVideoDeviceId();
})
.catch(err => APP.store.dispatch(notifyCameraError(err)));
// If screenshare is in progress but video is muted, update the default device
// id for video, dispose the existing presenter track and create a new effect
// that can be applied on un-mute.
} else if (this.isSharingScreen && videoWasMuted) {
logger.log('switched local video device');
const { height } = this.localVideo.track.getSettings();
this._updateVideoDeviceId();
})
.catch(err => {
APP.store.dispatch(notifyCameraError(err));
});
this.localPresenterVideo.dispose();
this.localPresenterVideo = null;
this._createPresenterStreamEffect(height, cameraDeviceId);
// if there is only video, switch to the new camera stream.
} else {
createLocalTracksF({
devices: [ 'video' ],
cameraDeviceId,
micDeviceId: null
})
.then(([ stream ]) => {
// if we are in audio only mode or video was muted before
// changing device, then mute
if (this.isAudioOnly() || videoWasMuted) {
return stream.mute()
.then(() => stream);
}
return stream;
})
.then(stream => this.useVideoStream(stream))
.then(() => {
logger.log('switched local video device');
this._updateVideoDeviceId();
})
.catch(err => APP.store.dispatch(notifyCameraError(err)));
}
}
);
@@ -2251,6 +2407,13 @@ export default {
cameraDeviceId: this.localVideo.getDeviceId()
}));
}
// If screenshare is in progress, get the device id from the presenter track.
if (this.localPresenterVideo) {
APP.store.dispatch(updateSettings({
cameraDeviceId: this.localPresenterVideo.getDeviceId()
}));
}
},
/**

View File

@@ -73,6 +73,11 @@ var config = {
// Disable measuring of audio levels.
// disableAudioLevels: false,
// Enabling this will run the lib-jitsi-meet no audio detection module which
// will notify the user if the current selected microphone has no audio
// input and will suggest another valid device if one is present.
// enableNoAudioDetection: false
// Start the conference in audio only mode (no video is being received nor
// sent).
// startAudioOnly: false,

View File

@@ -1,12 +1,23 @@
.avatar {
align-items: center;
background-color: #AAA;
display: flex;
border-radius: 50%;
color: rgba(255, 255, 255, 0.6);
font-weight: 100;
justify-content: center;
object-fit: cover;
&.avatar-small {
height: 28px !important;
width: 28px !important;
}
&.avatar-xsmall {
height: 16px !important;
width: 16px !important;
}
.jitsi-icon {
transform: translateY(50%);
}
}
.avatar-foreign {
@@ -28,4 +39,28 @@
.defaultAvatar {
opacity: 0.6
}
.avatar-badge {
position: relative;
&-available::after {
@include avatarBadge;
background-color: $presence-available;
}
&-away::after {
@include avatarBadge;
background-color: $presence-away;
}
&-busy::after {
@include avatarBadge;
background-color: $presence-busy;
}
&-idle::after {
@include avatarBadge;
background-color: $presence-idle;
}
}

View File

@@ -192,4 +192,17 @@
*/
@mixin transparentBg($color, $alpha) {
background-color: rgba(red($color), green($color), blue($color), $alpha);
}
}
/**
* Avatar status badge mixin
*/
@mixin avatarBadge {
border-radius: 50%;
content: '';
display: block;
height: 35%;
position: absolute;
bottom: 0;
width: 35%;
}

View File

@@ -29,6 +29,10 @@ $defaultSideBarFontColor: #44A5FF;
$defaultSemiDarkColor: #ACACAC;
$defaultDarkColor: #2b3d5c;
$defaultWarningColor: rgb(215, 121, 118);
$presence-available: rgb(110, 176, 5);
$presence-away: rgb(250, 201, 20);
$presence-busy: rgb(233, 0, 27);
$presence-idle: rgb(172, 172, 172);
/**
* Toolbar

1
debian/control vendored
View File

@@ -37,6 +37,7 @@ Description: Configuration for web serving of Jitsi Meet
Package: jitsi-meet-prosody
Architecture: all
Depends: openssl, prosody | prosody-trunk | prosody-0.11
Replaces: jitsi-meet-tokens
Description: Prosody configuration for Jitsi Meet
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
Videobridge to provide high quality, scalable video conferences.

View File

@@ -1,2 +1 @@
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example
doc/debian/jitsi-meet-prosody/README

2
debian/jitsi-meet-prosody.install vendored Normal file
View File

@@ -0,0 +1,2 @@
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example /usr/share/jitsi-meet-prosody/
resources/prosody-plugins/ /usr/share/jitsi-meet/

View File

@@ -93,7 +93,7 @@ case "$1" in
PROSODY_CONFIG_PRESENT="false"
mkdir -p /etc/prosody/conf.avail/
mkdir -p /etc/prosody/conf.d/
cp /usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
cp /usr/share/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
sed -i "s/jitmeetSecret/$JVB_SECRET/g" $PROSODY_HOST_CONFIG
sed -i "s/focusSecret/$JICOFO_SECRET/g" $PROSODY_HOST_CONFIG
@@ -179,7 +179,7 @@ case "$1" in
fi
if [ "$PROSODY_CONFIG_PRESENT" = "false" ]; then
invoke-rc.d prosody restart
invoke-rc.d prosody restart || true
fi
;;

View File

@@ -1 +0,0 @@
resources/prosody-plugins/ /usr/share/jitsi-meet/

View File

@@ -78,7 +78,7 @@ case "$1" in
fi
if [ -x "/etc/init.d/prosody" ]; then
invoke-rc.d prosody restart
invoke-rc.d prosody restart || true
fi
fi
else

View File

@@ -41,7 +41,7 @@ case "$1" in
sed -i 's/authentication = "token"/authentication = "anonymous"/g' $PROSODY_HOST_CONFIG
sed -i "s/ app_id=\"$APP_ID\"/ --app_id=\"example_app_id\"/g" $PROSODY_HOST_CONFIG
sed -i "s/ app_secret=\"$APP_SECRET\"/ --app_secret=\"example_app_secret\"/g" $PROSODY_HOST_CONFIG
sed -i 's/ modules_enabled = { "token_verification" }/ --modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
sed -i 's/ -- "token_verification"/ "token_verification"/g' $PROSODY_HOST_CONFIG
if [ -x "/etc/init.d/prosody" ]; then
invoke-rc.d prosody restart

View File

@@ -1,4 +1 @@
doc/debian/jitsi-meet/jitsi-meet.example
doc/debian/jitsi-meet/jitsi-meet.example-apache
doc/debian/jitsi-meet/README
config.js

3
debian/jitsi-meet-web-config.install vendored Normal file
View File

@@ -0,0 +1,3 @@
doc/debian/jitsi-meet/jitsi-meet.example /usr/share/jitsi-meet-web-config/
doc/debian/jitsi-meet/jitsi-meet.example-apache /usr/share/jitsi-meet-web-config/
config.js /usr/share/jitsi-meet-web-config/

View File

@@ -98,7 +98,9 @@ case "$1" in
# jitsi meet
JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
if [ ! -f $JITSI_MEET_CONFIG ] ; then
cp /usr/share/doc/jitsi-meet-web-config/config.js $JITSI_MEET_CONFIG
cp /usr/share/jitsi-meet-web-config/config.js $JITSI_MEET_CONFIG
# replaces needed config for multidomain as it works only with nginx
sed -i "s/conference.jitsi-meet.example.com/conference.<\!--# echo var=\"subdomain\" default=\"\" -->jitsi-meet.example.com/g" $JITSI_MEET_CONFIG
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $JITSI_MEET_CONFIG
fi
@@ -167,7 +169,7 @@ case "$1" in
db_set jitsi-meet/jvb-serve "true"
invoke-rc.d jitsi-videobridge restart
invoke-rc.d jitsi-videobridge restart || true
elif [[ "$FORCE_NGINX" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
# this is a reconfigure, lets just delete old links
if [ "$RECONFIGURING" = "true" ] ; then
@@ -177,7 +179,7 @@ case "$1" in
# nginx conf
if [ ! -f /etc/nginx/sites-available/$JVB_HOSTNAME.conf ] ; then
cp /usr/share/doc/jitsi-meet-web-config/jitsi-meet.example /etc/nginx/sites-available/$JVB_HOSTNAME.conf
cp /usr/share/jitsi-meet-web-config/jitsi-meet.example /etc/nginx/sites-available/$JVB_HOSTNAME.conf
if [ ! -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf ] ; then
ln -s /etc/nginx/sites-available/$JVB_HOSTNAME.conf /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf
fi
@@ -196,7 +198,7 @@ case "$1" in
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
fi
invoke-rc.d nginx reload
invoke-rc.d nginx reload || true
elif [[ "$FORCE_APACHE" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
# this is a reconfigure, lets just delete old links
if [ "$RECONFIGURING" = "true" ] ; then
@@ -208,7 +210,7 @@ case "$1" in
if [ ! -f /etc/apache2/sites-available/$JVB_HOSTNAME.conf ] ; then
# when creating new config, make sure all needed modules are enabled
a2enmod rewrite ssl headers proxy_http include
cp /usr/share/doc/jitsi-meet-web-config/jitsi-meet.example-apache /etc/apache2/sites-available/$JVB_HOSTNAME.conf
cp /usr/share/jitsi-meet-web-config/jitsi-meet.example-apache /etc/apache2/sites-available/$JVB_HOSTNAME.conf
a2ensite $JVB_HOSTNAME.conf
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/apache2/sites-available/$JVB_HOSTNAME.conf
fi
@@ -225,7 +227,7 @@ case "$1" in
/etc/apache2/sites-available/$JVB_HOSTNAME.conf
fi
invoke-rc.d apache2 reload
invoke-rc.d apache2 reload || true
fi
echo "----------------"

2
debian/rules vendored
View File

@@ -14,7 +14,7 @@ override_dh_auto_build:
override_dh_install: $(LANGUAGES)
dh_installdirs
dh_install -X/config.js -X/package.json
dh_install
$(LANGUAGES):
LOCALE=$$(echo $@ | cut -c1-2) ; \

View File

@@ -1,5 +1,7 @@
-- Plugins path gets uncommented during jitsi-meet-tokens package install - that's where token plugin is located
--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
-- domain mapper options, must at least have domain base set to use the mapper
muc_mapper_domain_base = "jitmeet.example.com";
VirtualHost "jitmeet.example.com"
-- enabled = false -- Remove this line to enable this host
@@ -16,18 +18,23 @@ VirtualHost "jitmeet.example.com"
key = "/etc/prosody/certs/jitmeet.example.com.key";
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
}
speakerstats_component = "speakerstats.jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"pubsub";
"ping"; -- Enable mod_ping
"speakerstats";
}
c2s_require_encryption = false
Component "conference.jitmeet.example.com" "muc"
storage = "null"
--modules_enabled = { "token_verification" }
modules_enabled = {
"muc_meeting_id";
"muc_domain_mapper";
-- "token_verification";
}
admins = { "focusUser@auth.jitmeet.example.com" }
Component "jitsi-videobridge.jitmeet.example.com"
@@ -38,3 +45,6 @@ VirtualHost "auth.jitmeet.example.com"
Component "focus.jitmeet.example.com"
component_secret = "focusSecret"
Component "speakerstats.jitmeet.example.com" "speakerstats_component"
muc_component = "conference.jitmeet.example.com"

View File

@@ -19,7 +19,11 @@ server {
ssl_certificate_key /etc/jitsi/meet/jitsi-meet.example.com.key;
root /usr/share/jitsi-meet;
# ssi on with javascript for multidomain variables in config.js
ssi on;
ssi_types application/x-javascript application/javascript;
index index.html index.htm;
error_page 404 /static/404.html;
@@ -52,4 +56,28 @@ server {
location @root_path {
rewrite ^/(.*)$ / break;
}
location ~ ^/([^/?&:'"]+)/config.js$
{
set $subdomain "$1.";
set $subdir "$1/";
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
}
#Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
location ~ ^/([^/?&:'"]+)/(.*)$ {
set $subdomain "$1.";
set $subdir "$1/";
rewrite ^/([^/?&:'"]+)/(.*)$ /$2;
}
# BOSH for subdomains
location ~ ^/([^/?&:'"]+)/http-bind {
set $subdomain "$1.";
set $subdir "$1/";
set $prefix "$1";
rewrite ^/(.*)$ /http-bind;
}
}

View File

@@ -36,10 +36,6 @@ server {
rewrite ^/(.*)$ / break;
}
location / {
ssi on;
}
location ~ ^/([^/?&:'"]+)/config.js$
{
set $subdomain "$1.";
@@ -64,4 +60,4 @@ server {
rewrite ^/(.*)$ /http-bind;
}
}
}

View File

@@ -83,8 +83,6 @@ modules_enabled = {
"adhoc";
"websocket";
"http_altconnect";
-- include domain mapper as global level module
"muc_domain_mapper";
}
-- domain mapper options, must at least have domain base set to use the mapper

BIN
images/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="324px" height="63.8px" viewBox="0 0 324 63.8" style="enable-background:new 0 0 324 63.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0061FF;}
.st1{display:none;}
.st2{display:inline;}
.st3{fill:none;}
</style>
<path class="st0" d="M37.6,12L18.8,24l18.8,12L18.8,48L0,35.9l18.8-12L0,12L18.8,0L37.6,12z M18.7,51.8l18.8-12l18.8,12l-18.8,12
L18.7,51.8z M37.6,35.9l18.8-12L37.6,12L56.3,0l18.8,12L56.3,24l18.8,12L56.3,48L37.6,35.9z"/>
<path d="M89.8,12H105c9.7,0,17.7,5.6,17.7,18.4v2.7c0,12.9-7.5,18.7-17.4,18.7H89.8V12z M98.3,19.2v25.3h6.5c5.5,0,9.2-3.6,9.2-11.6
v-2.1c0-8-3.9-11.6-9.5-11.6H98.3z M127.2,19.6h6.8l1.1,7.5c1.3-5.1,4.6-7.8,10.6-7.8h2.1v8.6h-3.5c-6.9,0-8.6,2.4-8.6,9.2v14.8
h-8.4V19.6H127.2z M149.5,36.4v-0.9c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.5,16.3-16.3,16.3
C155.4,52.6,149.5,47,149.5,36.4z M173.5,36.3v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1
C170.5,45.3,173.5,42.1,173.5,36.3z M186.5,19.6h7l0.8,6.1c1.7-4.1,5.3-6.9,10.6-6.9c8.2,0,13.6,5.9,13.6,16.8v0.9
c0,10.6-6,16.2-13.6,16.2c-5.1,0-8.6-2.3-10.3-6V63h-8.2L186.5,19.6L186.5,19.6z M210,36.3v-0.7c0-6.4-3.3-9.6-7.7-9.6
c-4.7,0-7.8,3.6-7.8,9.6v0.6c0,5.7,3,9.3,7.7,9.3C207,45.4,210,42.3,210,36.3z M230.9,45.9l-0.7,5.9H223v-43h8.2v16.5
c1.8-4.2,5.4-6.5,10.5-6.5c7.7,0.1,13.4,5.4,13.4,16.1v1c0,10.7-5.4,16.8-13.6,16.8C236.1,52.6,232.6,50.1,230.9,45.9z M246.5,35.9
v-0.8c0-5.9-3.2-9.2-7.7-9.2c-4.6,0-7.8,3.7-7.8,9.3v0.7c0,6,3.1,9.5,7.7,9.5C243.6,45.4,246.5,42.3,246.5,35.9z M258.7,36.4v-0.9
c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.6,16.3-16.3,16.3C264.6,52.6,258.7,47,258.7,36.4z M282.8,36.3
v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1C279.8,45.3,282.8,42.1,282.8,36.3z M302.3,35.1L291,19.6
h9.7l6.5,9.7l6.6-9.7h9.6L311.9,35L324,51.8h-9.5l-7.4-10.7l-7.2,10.7H290L302.3,35.1z"/>
<g id="Editble" class="st1">
<g class="st2">
<rect x="-105" y="5" class="st3" width="506" height="71.8"/>
<path d="M0.2,13.6h16.3c10.4,0,19,6.1,19,19.8v2.9c0,13.8-8,20-18.7,20H0.2V13.6z M9.4,21.3v27.2h7c5.9,0,9.9-3.9,9.9-12.5v-2.2
c0-8.6-4.1-12.5-10.2-12.5H9.4z M40.4,21.8h7.3l1.1,8c1.4-5.5,4.9-8.3,11.3-8.3h2.2v9.2h-3.7c-7.4,0-9.2,2.6-9.2,9.9v15.8h-9
C40.4,56.4,40.4,21.8,40.4,21.8z M64.3,39.8v-1c0-11.6,7.4-17.9,17.5-17.9c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7,17.5-17.5,17.5
C70.6,57.3,64.3,51.2,64.3,39.8z M90.1,39.7v-0.8c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7
C86.9,49.4,90.1,46,90.1,39.7z M104,21.8h7.6l0.9,6.6c1.9-4.4,5.7-7.4,11.4-7.4c8.8,0,14.6,6.4,14.6,18v1
c0,11.4-6.4,17.3-14.6,17.3c-5.5,0-9.2-2.5-11-6.5v17.5H104V21.8z M129.3,39.8V39c0-6.9-3.5-10.3-8.3-10.3c-5,0-8.4,3.8-8.4,10.3
v0.7c0,6.1,3.2,10,8.2,10C126,49.5,129.3,46.1,129.3,39.8z M151.7,50.1l-0.7,6.3h-7.8V10.2h8.8V28c1.9-4.5,5.8-7,11.2-7
c8.2,0.1,14.3,5.8,14.3,17.3v1c0,11.5-5.8,18-14.6,18C157.3,57.3,153.5,54.5,151.7,50.1z M168.5,39.3v-0.8c0-6.4-3.5-9.8-8.3-9.8
c-5,0-8.4,4-8.4,10v0.7c0,6.5,3.3,10.2,8.3,10.2C165.3,49.5,168.5,46.1,168.5,39.3z M181.6,39.8v-1c0-11.6,7.4-17.9,17.5-17.9
c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7.1,17.5-17.5,17.5C187.9,57.3,181.6,51.2,181.6,39.8z M207.4,39.7v-0.8
c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7C204.2,49.4,207.4,46,207.4,39.7z M228.3,38.4
l-12.1-16.7h10.4l7,10.4l7.1-10.4H251l-12.3,16.6l13,18h-10.2l-8-11.5l-7.7,11.5h-10.6L228.3,38.4z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

0
images/dropboxLogo_square.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,182 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 744.09448819 1052.3622047"
id="svg3526"
version="1.1"
inkscape:version="0.91 r13725"
inkscape:export-filename="/Users/ystamcheva/Dropbox/Designs/appLogo.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"
sodipodi:docname="logo-blue.svg">
<defs
id="defs3528" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="817.30793"
inkscape:cy="496.00851"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1440"
inkscape:window-height="851"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<metadata
id="metadata3531">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g4181">
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g33">
<path
d="m 257.311,591.057 c 27.544,0 85.707,-16.445 124.179,-27.332 5.595,-1.575 10.81,-3.049 15.549,-4.371 0.767,-0.211 1.51,-0.403 2.213,-0.579 -2.161,-2.139 -5.755,-5.387 -11.612,-10.295 -25.628,-17.369 -49.827,-25.456 -76.146,-25.456 -5.741,0 -11.707,0.352 -18.208,1.088 -22.283,2.535 -40.848,7.845 -49.767,10.39 -4.521,1.296 -5.883,1.683 -7.292,1.683 -2.688,0 -4.997,-1.599 -5.9,-4.069 -0.904,-2.483 -0.13,-5.223 1.969,-6.981 l 0.127,-0.102 c 15.379,-12.883 44.032,-36.866 98.39,-47.582 9.428,-1.853 19.514,-2.796 29.968,-2.796 24.334,0 49.53,5.026 74.869,14.925 34.511,13.474 58.094,30.771 77.062,44.67 10.211,7.489 19.03,13.959 26.705,17.516 1.961,0.912 2.979,1.169 3.453,1.236 0.349,-0.452 1.106,-1.7 2.219,-4.974 0.298,-0.867 2.453,-10.019 -13.007,-62.071 -8.985,-30.217 -19.822,-61.077 -25.465,-74.778 -10.916,-26.509 -8.237,-45.296 -4.877,-56.284 -9.248,3.399 -18.701,8.688 -28.646,15.993 l -0.62,0.458 c -4.969,3.684 -10.031,7.853 -15.482,12.725 -32.074,28.718 -56.104,43.69 -71.455,44.504 l -0.423,0.021 -0.421,-0.036 c -13.524,-1.148 -34.019,-20.834 -42.403,-30.801 -1.743,-1.169 -3.729,-1.699 -6.35,-1.699 -2.632,0 -5.583,0.553 -8.438,1.095 -2.077,0.394 -4.218,0.795 -6.341,1.01 -6.767,0.679 -16.252,2.867 -25.406,4.974 -4.413,1.014 -8.967,2.063 -13.13,2.922 -0.079,0.013 -1.866,0.382 -5.06,1.224 -22.624,6.693 -39.673,14.372 -48.012,21.628 -0.091,0.079 -0.36,0.288 -0.789,0.603 -5.64,4.009 -19.199,15.447 -23.29,34.907 l -0.043,0.162 c -8.541,35.837 4.408,80.28 21.615,105.666 8.093,11.932 16.814,19.376 23.944,20.42 1.775,0.252 3.905,0.386 6.321,0.386 z"
id="path35"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g37">
<path
d="m 383.729,400.995 c 0.549,0.108 1.191,0.162 1.9,0.162 14.785,0 47.804,-21.408 53.912,-31.205 l 0.486,-0.78 0.694,-0.611 c 2.083,-2.056 8.099,-12.885 11.019,-19.367 -31.312,-9.394 -34.767,-26.347 -37.821,-41.41 -0.355,-1.749 -0.667,-3.324 -0.946,-4.732 -0.357,-1.842 -0.731,-3.713 -1.052,-5.159 -46.646,15.471 -60.905,24.154 -68.687,30.611 -4.027,3.345 -6.398,12.858 5.215,39.189 5.932,13.422 26.386,31.591 35.28,33.302 z"
id="path39"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<path
style="fill:#17a0db;fill-opacity:1"
inkscape:connector-curvature="0"
id="path43"
d="m 183.03568,710.19014 c -5.799,-6.834 -8.258,-15.447 -7.293,-25.624 4.105,-49.397 -1.525,-61.33 -4.132,-64.162 -0.629,-0.685 -0.969,-0.685 -1.238,-0.685 -0.101,0 -0.195,0.006 -0.296,0.016 -4.84,1.157 -37.441,23.198 -44.638,89.005 -3.471,31.758 2.611,72.542 7.794,97.348 4.165,-14.646 10.742,-30.779 23.483,-47.384 11.862,-15.444 24.801,-27.623 40.852,-38.298 -4.99,-2.075 -10.346,-5.274 -14.532,-10.216 z" />
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g45">
<path
d="m 485.028,154.141 c -3.896,25.701 -10.239,50.115 -22.077,75.883 12.904,-14.609 20.445,-30.481 22.971,-48.296 1.051,-7.38 2.045,-14.439 -0.894,-27.587 z"
id="path47"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g49">
<path
d="m 413.102,273.797 c 23.135,-20.915 37.22,-55.455 43.078,-75.971 -20.149,19.407 -44.636,29.82 -60.351,36.512 -5.412,2.308 -10.08,4.295 -12.878,5.926 -1.178,0.685 -2.367,1.374 -3.571,2.069 -9.533,5.515 -23.924,13.85 -26.022,18.987 l -0.06,0.167 -0.078,0.165 c -6.529,13.72 -10.208,34.352 -11.387,46.184 15.135,-9.242 30.738,-15.41 43.699,-20.529 12.03,-4.753 22.432,-8.863 27.57,-13.51 z"
id="path51"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g53">
<path
d="m 436.439,291.877 c -0.141,0.357 -0.292,0.695 -0.455,1.017 -3.833,11.143 1.446,26.3 11.227,32.017 2.602,1.522 5.132,2.452 7.559,2.772 0.334,0.014 0.666,0.027 1.001,0.027 7.601,0 13.801,-5.56 18.4,-16.519 2.896,-8.34 3.308,-18.23 1.125,-27.158 -1.696,-6.936 -6.084,-15.215 -8.88,-19.343 -5.219,3.582 -15.533,11.462 -22.615,17.716 -4.946,4.777 -6.733,7.785 -7.362,9.471 z"
id="path55"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g57">
<path
d="m 501.845,575.103 c 8.403,-2.29 15.076,-4.165 19.998,-5.623 -10.137,-7.061 -21.871,-15.846 -37.823,-28.253 -39.096,-30.404 -81.019,-45.826 -124.587,-45.826 -23.861,0 -44.647,4.592 -61.098,10.151 4.101,-0.255 8.271,-0.377 12.554,-0.377 5.088,0 10.42,0.179 15.842,0.541 16.949,1.136 60.616,8.845 100.106,55.931 7.956,9.469 16.507,17.307 40.828,17.307 8.679,0 18.796,-0.967 30.913,-2.959 0.749,-0.209 1.882,-0.518 3.267,-0.892 z"
id="path59"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g61">
<path
d="m 557.268,369.949 c -7.755,-12.043 -17.498,-19.524 -25.41,-19.524 -1.464,0 -2.862,0.258 -4.154,0.765 -4.239,1.672 -10.952,21.042 -7.979,35.126 2.023,9.582 13.67,41.96 19.262,57.52 2.142,5.958 3.18,8.869 3.527,9.951 0.275,0.853 0.67,2.077 1.17,3.621 4.517,13.765 16.111,49.145 19.562,77.793 7.175,-30.554 11.239,-67.36 9.647,-111.409 -0.723,-20.199 -6.274,-39.323 -15.625,-53.843 z"
id="path63"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g65">
<path
d="m 412.08,575.289 c -0.153,-0.2 -0.3,-0.397 -0.445,-0.585 -0.614,0.1 -1.616,0.319 -3.185,0.776 l -0.657,0.197 c -8.011,2.95 -22.707,7.908 -39.694,13.64 -20.387,6.87 -43.477,14.659 -62.808,21.595 -24.596,9.165 -32.572,12.781 -35.073,14.048 -0.454,1.218 -0.963,2.772 -1.53,4.486 -5.817,17.705 -19.139,58.23 -84.831,86.562 13.568,13.744 43.101,38.415 101.24,38.415 5.035,0 10.258,-0.188 15.494,-0.566 43.896,-3.121 85.158,-22.544 116.206,-54.673 28.233,-29.21 44.259,-65.641 44.507,-100.76 -6.871,-0.571 -18.519,-2.281 -29.301,-7.4 -0.125,-0.061 -12.447,-6.002 -19.923,-15.735 z"
id="path67"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g69">
<path
d="m 162.104,639.109 c -0.122,10.334 -1.489,20.245 -2.82,29.907 -0.716,5.216 -1.464,10.615 -2.014,16.041 -0.746,10.914 1.612,14.717 2.659,15.829 0.571,0.629 1.513,1.346 3.536,1.346 1.558,0 3.418,-0.432 5.383,-1.251 19.507,-8.176 38.032,-22.367 46.937,-30.243 -13.668,-6.095 -34.689,-19.26 -53.681,-31.629 z"
id="path71"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g73">
<path
d="m 484.26,598.224 c -0.552,7.258 -1.737,20.949 -3.631,31.378 -2.295,12.629 -6.095,23.31 -8.305,28.889 3.945,3.648 7.878,7.228 10.429,9.488 10.265,-6.718 43.961,-32.297 67.208,-90.368 -7.447,5.03 -17.906,9.456 -31.465,13.332 -13.797,3.929 -27.204,6.229 -34.236,7.281 z"
id="path75"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
transform="translate(43.272677,-6.8248629)"
id="g85"
style="fill:#17a0db;fill-opacity:1">
<path
style="fill:#17a0db;fill-opacity:1"
inkscape:connector-curvature="0"
id="path87"
d="M 627.562,350.519 C 613.5,321.124 593.893,306.283 580.351,297.677 568.965,290.444 555.872,285.188 541.339,282 c -1.622,-10.158 -4.397,-20.542 -8.198,-30.646 24.507,-36.531 30.407,-77.605 17.008,-119.213 C 539.858,100.151 531.868,79.524 524.996,67.213 510.15,40.585 489.58,34.997 474.936,34.997 c -15.09,0 -29.538,6.412 -39.667,17.61 -10.37,11.462 -15.213,26.462 -13.634,42.228 1.349,13.446 -2.178,37.872 -4.519,46.594 -0.04,0.117 -4.202,11.776 -35.168,26.784 -0.746,0.268 -2.332,0.811 -4.773,1.629 -17.812,5.965 -50.913,17.062 -72.963,46.219 -16.847,20.407 -20.985,40.629 -25.766,64.036 -2.858,13.955 -5.846,32.187 -5.105,53.745 -55.35,12.291 -95.226,37.338 -118.609,74.54 -24.203,38.52 -28.402,86.272 -12.468,141.993 l 0.14,0.414 c 0.292,1.014 0.6,2.024 0.921,3.03 -2.718,-0.466 -5.465,-0.858 -8.285,-1.169 -2.469,-0.284 -5.015,-0.42 -7.54,-0.42 -27.636,0 -57.043,17.371 -78.666,46.474 -16.427,22.098 -36.156,61.131 -36.852,121.593 -0.523,44.905 4.279,86.306 14.283,123.054 7.461,27.381 15.784,44.202 18.979,50.09 l 67.793,127.079 31.06,-140.731 c 10.6,-47.935 21.066,-68.283 34.571,-81.732 31.425,18.938 68.541,28.901 107.941,28.901 43.919,0 89.715,-12.667 128.934,-35.662 25.477,-14.954 47.193,-33.324 64.629,-54.658 0.236,0 0.469,0 0.704,0 l 1.857,-0.038 c 10.782,-0.365 25.522,-5.697 40.434,-14.63 12.421,-7.433 31.147,-21.108 49.946,-44.064 18.945,-23.155 34.402,-51.324 45.926,-83.731 13.5,-37.939 21.717,-82.115 24.404,-131.272 3.253,-45.723 -2.078,-83.533 -15.881,-112.384 z m -31.124,109.427 c -2.415,44.805 -9.745,84.66 -21.764,118.441 -9.713,27.302 -22.502,50.739 -38.005,69.69 -26.696,32.611 -52.783,41.355 -55.551,41.465 l -0.22,0 c -2.528,0 -4.012,-1.032 -11.095,-5.988 -1.979,-1.379 -4.969,-3.467 -7.436,-5.075 -14.813,28.811 -39.145,53.701 -70.659,72.185 -32.098,18.824 -69.432,29.202 -105.1,29.202 -42.352,0 -79.532,-13.979 -107.842,-40.493 -38.621,24.556 -61.833,45.044 -80.652,130.273 l -3.562,16.157 -7.787,-14.59 C 84.8,867.621 78.058,854.32 71.708,830.982 62.852,798.444 58.598,761.384 59.071,720.842 c 0.944,-80.909 44.373,-121.518 68.427,-121.518 0.792,0 1.578,0.039 2.328,0.128 22.8,2.551 37.699,12.402 64.745,30.291 2.796,1.853 5.74,3.8 8.843,5.838 9.69,6.36 23.387,14.125 26.835,14.791 6.562,-0.381 12.986,-15.079 14.853,-28.713 0.114,-0.829 0.226,-1.598 0.334,-2.315 0.147,-1.612 0.227,-3.03 0.27,-4.194 -1.144,-0.399 -2.333,-0.869 -3.547,-1.403 l -0.27,-0.091 c -17.012,-5.857 -41.868,-34.625 -54.378,-76.385 -12.081,-42.21 -9.691,-77.122 7.099,-103.83 27.221,-43.328 86.307,-53.515 105.849,-56.861 6.109,-1.214 12.498,-2.351 18.999,-3.378 3.035,-0.762 5.11,-1.399 6.449,-1.978 0.58,-0.403 0.835,-0.833 0.439,-2.403 l 0.53,-0.148 -0.513,0.115 c -0.237,-1.065 -0.565,-2.311 -0.941,-3.753 -0.521,-1.997 -1.103,-4.256 -1.705,-6.936 -6.05,-27.141 -2.962,-49.884 0.863,-68.559 4.297,-21.019 6.678,-32.656 16.605,-44.279 13.152,-18.103 36.803,-26.025 50.953,-30.77 3.948,-1.322 7.359,-2.462 9.331,-3.412 43.344,-20.789 57.145,-42.646 61.091,-57.318 3.127,-11.642 8.084,-42.253 5.931,-63.63 -0.239,-2.425 0.326,-4.421 1.695,-5.935 1.215,-1.341 2.942,-2.104 4.748,-2.104 4.061,0 9.623,0 30.377,64.478 10.949,33.996 2.785,65.868 -24.244,94.74 -0.347,0.375 -0.7,0.742 -1.04,1.095 -0.738,0.76 -1.848,1.909 -1.999,2.326 0.006,0 -0.048,1.042 1.755,4.031 11.425,18.864 17.633,42.323 15.832,59.763 -0.429,4.062 -1.206,7.971 -1.879,11.411 -0.4,1.968 -0.879,4.377 -1.126,6.241 0.111,0 0.226,0 0.347,0 3.088,-0.327 7.867,-0.7 13.628,-0.7 13.556,0 32.969,2.077 48.503,11.951 9.382,5.952 21.255,15.137 29.981,33.404 10.281,21.472 14.096,51.453 11.369,89.114 z" />
</g>
</g>
<path
style="fill:#ffffff"
d=""
id="path3618"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width='20px' height='20px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-spin"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><g transform="translate(50 50)"><g transform="rotate(0) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(45) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.12s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.12s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(90) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.25s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.25s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(135) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.37s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.37s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(180) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.5s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.5s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(225) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.62s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.62s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(270) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.75s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.75s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(315) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.87s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.87s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="480"
height="270"
id="svg2"
version="1.1"
inkscape:version="0.48.2 r9819"
sodipodi:docname="videomask.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-16.428571"
inkscape:cy="520"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="998"
inkscape:window-height="711"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-782.36218)">
<rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.92795467;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="rect2985"
width="479.07202"
height="269.07205"
x="0.46397734"
y="782.82617"
ry="20" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--#include virtual="base.html" -->
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<link rel="stylesheet" href="css/all.css">
<script>

View File

@@ -77,7 +77,7 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "exec &gt; /tmp/${PROJECT_NAME}_archive.log 2&gt;&amp;1&#10;&#10;UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal&#10;&#10;if [ &quot;true&quot; == ${ALREADYINVOKED:-false} ]&#10;then&#10;echo &quot;RECURSION: Detected, stopping&quot;&#10;else&#10;export ALREADYINVOKED=&quot;true&quot;&#10;&#10;# make sure the output directory exists&#10;mkdir -p &quot;${UNIVERSAL_OUTPUTFOLDER}&quot;&#10;&#10;echo &quot;Building for iPhoneSimulator&quot;&#10;xcodebuild -workspace &quot;${WORKSPACE_PATH}&quot; -scheme &quot;${TARGET_NAME}&quot; -configuration ${CONFIGURATION} -sdk iphonesimulator -destination &apos;platform=iOS Simulator,name=iPhone 8&apos; ONLY_ACTIVE_ARCH=NO ARCHS=&apos;i386 x86_64&apos; BUILD_DIR=&quot;${BUILD_DIR}&quot; BUILD_ROOT=&quot;${BUILD_ROOT}&quot; ENABLE_BITCODE=YES OTHER_CFLAGS=&quot;-fembed-bitcode&quot; BITCODE_GENERATION_MODE=bitcode build&#10;&#10;# Step 1. Copy the framework structure (from iphoneos build) to the universal folder&#10;echo &quot;Copying to output folder&quot;&#10;cp -R &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/&quot;&#10;&#10;# Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory&#10;SIMULATOR_SWIFT_MODULES_DIR=&quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/.&quot;&#10;if [ -d &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; ]; then&#10;cp -R &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule&quot;&#10;fi&#10;&#10;# Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory&#10;echo &quot;Combining executables&quot;&#10;lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}&quot;&#10;&#10;# Step 4. Create universal binaries for embedded frameworks&#10;#for SUB_FRAMEWORK in $( ls &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks&quot; ); do&#10;#BINARY_NAME=&quot;${SUB_FRAMEWORK%.*}&quot;&#10;#lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot;&#10;#done&#10;&#10;# Step 5. Convenience step to copy the framework to the project&apos;s directory&#10;echo &quot;Copying to project dir&quot;&#10;yes | cp -Rf &quot;${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}&quot; &quot;${PROJECT_DIR}&quot;&#10;&#10;fi&#10;">
scriptText = "exec &gt; /tmp/${PROJECT_NAME}_archive.log 2&gt;&amp;1&#10;&#10;UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal&#10;&#10;if [ &quot;true&quot; == ${ALREADYINVOKED:-false} ]&#10;then&#10;echo &quot;RECURSION: Detected, stopping&quot;&#10;else&#10;export ALREADYINVOKED=&quot;true&quot;&#10;&#10;# make sure the output directory exists&#10;mkdir -p &quot;${UNIVERSAL_OUTPUTFOLDER}&quot;&#10;&#10;echo &quot;Building for iPhoneSimulator&quot;&#10;xcodebuild -workspace &quot;${WORKSPACE_PATH}&quot; -scheme &quot;${TARGET_NAME}&quot; -configuration ${CONFIGURATION} -sdk iphonesimulator -destination &apos;platform=iOS Simulator,name=iPhone 8&apos; ONLY_ACTIVE_ARCH=NO ARCHS=&apos;x86_64&apos; BUILD_DIR=&quot;${BUILD_DIR}&quot; BUILD_ROOT=&quot;${BUILD_ROOT}&quot; ENABLE_BITCODE=YES OTHER_CFLAGS=&quot;-fembed-bitcode&quot; BITCODE_GENERATION_MODE=bitcode build&#10;&#10;# Step 1. Copy the framework structure (from iphoneos build) to the universal folder&#10;echo &quot;Copying to output folder&quot;&#10;cp -R &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/&quot;&#10;&#10;# Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory&#10;SIMULATOR_SWIFT_MODULES_DIR=&quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/.&quot;&#10;if [ -d &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; ]; then&#10;cp -R &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule&quot;&#10;fi&#10;&#10;# Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory&#10;echo &quot;Combining executables&quot;&#10;lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}&quot;&#10;&#10;fi&#10;&#10;# Step 4. Convenience step to copy the framework to the project&amp;apos;s directory&#10;echo &quot;Copying to project dir&amp;quot&quot;&#10;yes | cp -Rf ${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME} ${PROJECT_DIR}&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"

View File

@@ -630,6 +630,10 @@
"lowerYourHand": "Lower your hand",
"moreActions": "More actions",
"mute": "Mute / Unmute",
"noAudioSignalTitle": "There is no input coming from your mic!",
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider changing the device.",
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider using the following device:",
"openChat": "Open chat",
"pip": "Enter Picture-in-Picture mode",
"privateMessage": "Send private message",
@@ -736,7 +740,7 @@
"roomNameAllowedChars": "Meeting name should not contain any of these characters: ?, &, :, ', \", %, #.",
"go": "GO",
"goSmall": "GO",
"join": "JOIN",
"join": "CREATE / JOIN",
"info": "Info",
"privacy": "Privacy",
"recentList": "Recent",

69
package-lock.json generated
View File

@@ -22,38 +22,6 @@
"@atlaskit/type-helpers": "^2.0.0"
}
},
"@atlaskit/avatar": {
"version": "14.1.7",
"resolved": "https://registry.npmjs.org/@atlaskit/avatar/-/avatar-14.1.7.tgz",
"integrity": "sha512-KGtV0lRr3g+JX3XLZQKDGxGhtbVFRvM/Ku5C+CEJw2uDl1KFY0dJxfr2a/E32bEgUuvmqSL7D3ROrTrlHJ2fMA==",
"requires": {
"@atlaskit/analytics-next": "^3.1.2",
"@atlaskit/theme": "^7.0.1",
"@atlaskit/tooltip": "^12.1.13",
"@babel/runtime": "^7.0.0"
},
"dependencies": {
"@atlaskit/analytics-next": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-3.1.2.tgz",
"integrity": "sha512-bkYDvl3Ojsnim+bsc9BALfvOjiL7xdb2rTp/4yqUP9pfidtf5HudbOJ849+dKcRCmk/rFbfB/nhDBRU6rv1Ueg==",
"requires": {
"@babel/runtime": "^7.0.0",
"babel-runtime": "^6.26.0",
"prop-types": "^15.5.10"
}
},
"@atlaskit/theme": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-7.0.1.tgz",
"integrity": "sha512-wxXDnkUablJketNCrQuNUuazufYEA7kv0Y6Yzv6uvqfuyNpWUQt4H1psz/MW8DbZmCdku9dEYbNVK3nFP5TDGg==",
"requires": {
"@babel/runtime": "^7.0.0",
"prop-types": "^15.5.10"
}
}
}
},
"@atlaskit/blanket": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/@atlaskit/blanket/-/blanket-8.0.3.tgz",
@@ -10931,8 +10899,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"from": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"version": "github:jitsi/lib-jitsi-meet#4e7034ee6a0d0e68487c583c981f4e07ab73926c",
"from": "github:jitsi/lib-jitsi-meet#4e7034ee6a0d0e68487c583c981f4e07ab73926c",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.2",
@@ -14830,6 +14798,39 @@
"jssha": "^2.2.0"
}
},
"react-native-collapsible": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/react-native-collapsible/-/react-native-collapsible-1.5.1.tgz",
"integrity": "sha512-uQQ2s6l+7+L/pzJroisWsDsyVYVF5bQ+jGbLATNioRh/03SpEL8pcQEVKqVWswcNNR0B9GENixHaLzmuZIwpQg==",
"requires": {
"prop-types": "^15.6.2"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"react-is": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
}
}
},
"react-native-immersive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-immersive/-/react-native-immersive-2.0.0.tgz",

View File

@@ -15,7 +15,6 @@
"author": "",
"readmeFilename": "README.md",
"dependencies": {
"@atlaskit/avatar": "14.1.7",
"@atlaskit/button": "10.1.1",
"@atlaskit/checkbox": "5.0.10",
"@atlaskit/dropdown-menu": "6.1.25",
@@ -57,7 +56,7 @@
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#4e7034ee6a0d0e68487c583c981f4e07ab73926c",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",
@@ -72,6 +71,7 @@
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#902e6e92d6bae450a6052f76ba4d02f977ffd8f2",
"react-native-callstats": "3.61.0",
"react-native-collapsible": "1.5.1",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",
"react-native-linear-gradient": "2.5.6",

View File

@@ -27,7 +27,7 @@ export default class AmplitudeHandler extends AbstractHandler {
host
};
amplitude.getInstance(this._amplitudeOptions).init(amplitudeAPPKey);
amplitude.getInstance(this._amplitudeOptions).init(amplitudeAPPKey, undefined, { includeReferrer: true });
if (user) {
amplitude.getInstance(this._amplitudeOptions).setUserId(user);

View File

@@ -35,9 +35,12 @@ class Amplitude {
* Sets an identifier for the current user.
*
* @param {string} userId - The new user id.
* @param {string} opt_userId - Currently not used.
* @param {Object} opt_config - Currently not used.
* @param {Function} opt_callback - Currently not used.
* @returns {void}
*/
setUserId(userId) {
setUserId(userId, opt_userId, opt_config, opt_callback) { // eslint-disable-line camelcase, no-unused-vars
AmplitudeNative.setUserId(this._instanceName, userId);
}

View File

@@ -147,9 +147,10 @@ MiddlewareRegistry.register(store => next => action => {
const state = getState();
const { localTracksDuration } = state['features/analytics'];
if (localTracksDuration.conference.startedTime === -1) {
if (localTracksDuration.conference.startedTime === -1 || action.mediaType === 'presenter') {
// We don't want to track the media duration if the conference is not joined yet because otherwise we won't
// be able to compare them with the conference duration (from conference join to conference will leave).
// Also, do not track media duration for presenter tracks.
break;
}
dispatch({

View File

@@ -7,6 +7,7 @@ import { DialogContainer } from '../../base/dialog';
import '../../base/user-interaction';
import '../../chat';
import '../../external-api';
import '../../no-audio-signal';
import '../../power-monitor';
import '../../room-lock';
import '../../talk-while-muted';

View File

@@ -5,6 +5,7 @@ import { Text, TextInput, View } from 'react-native';
import { connect as reduxConnect } from 'react-redux';
import type { Dispatch } from 'redux';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { toJid } from '../../base/connection';
import { connect } from '../../base/connection/actions.native';
import {
@@ -19,7 +20,9 @@ import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet';
import type { StyleType } from '../../base/styles';
import { authenticateAndUpgradeRole, cancelLogin } from '../actions';
import styles from './styles';
// Register styles.
import './styles';
/**
* The type of the React {@link Component} props of {@link LoginDialog}.
@@ -58,6 +61,11 @@ type Props = {
*/
_progress: number,
/**
* The color-schemed stylesheet of this feature.
*/
_styles: StyleType,
/**
* Redux store dispatch method.
*/
@@ -144,46 +152,10 @@ class LoginDialog extends Component<Props, State> {
const {
_connecting: connecting,
_dialogStyles,
_error: error,
_progress: progress,
_styles: styles,
t
} = this.props;
let messageKey;
const messageOptions = {};
if (progress && progress < 1) {
messageKey = 'connection.FETCH_SESSION_ID';
} else if (error) {
const { name } = error;
if (name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
// Show a message that the credentials are incorrect only if the
// credentials which have caused the connection to fail are the
// ones which the user sees.
const { credentials } = error;
if (credentials
&& credentials.jid
=== toJid(
this.state.username,
this.props._configHosts)
&& credentials.password === this.state.password) {
messageKey = 'dialog.incorrectPassword';
}
} else if (name) {
messageKey = 'dialog.connectErrorWithMsg';
messageOptions.msg = `${name} ${error.message}`;
}
}
const showMessage = messageKey || connecting;
const message = messageKey
? t(messageKey, messageOptions)
: connecting
? t('connection.CONNECTING')
: '';
return (
<CustomSubmitDialog
okDisabled = { connecting }
@@ -210,16 +182,77 @@ class LoginDialog extends Component<Props, State> {
] }
underlineColorAndroid = { FIELD_UNDERLINE }
value = { this.state.password } />
{ showMessage && (
<Text style = { styles.dialogText }>
{ message }
</Text>
) }
{ this._renderMessage() }
</View>
</CustomSubmitDialog>
);
}
/**
* Renders an optional message, if applicable.
*
* @returns {ReactElement}
* @private
*/
_renderMessage() {
const {
_connecting: connecting,
_error: error,
_progress: progress,
_styles: styles,
t
} = this.props;
let messageKey;
let messageIsError = false;
const messageOptions = {};
if (progress && progress < 1) {
messageKey = 'connection.FETCH_SESSION_ID';
} else if (error) {
const { name } = error;
if (name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
// Show a message that the credentials are incorrect only if the
// credentials which have caused the connection to fail are the
// ones which the user sees.
const { credentials } = error;
if (credentials
&& credentials.jid
=== toJid(
this.state.username,
this.props._configHosts)
&& credentials.password === this.state.password) {
messageKey = 'dialog.incorrectPassword';
messageIsError = true;
}
} else if (name) {
messageKey = 'dialog.connectErrorWithMsg';
messageOptions.msg = `${name} ${error.message}`;
messageIsError = true;
}
} else if (connecting) {
messageKey = 'connection.CONNECTING';
}
if (messageKey) {
const message = t(messageKey, messageOptions);
const messageStyles = [
styles.dialogText,
messageIsError ? styles.errorMessage : styles.progressMessage
];
return (
<Text style = { messageStyles }>
{ message }
</Text>
);
}
return null;
}
_onUsernameChange: (string) => void;
/**
@@ -295,14 +328,7 @@ class LoginDialog extends Component<Props, State> {
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _conference: JitsiConference,
* _configHosts: Object,
* _connecting: boolean,
* _dialogStyles: StyleType,
* _error: Object,
* _progress: number
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const {
@@ -323,7 +349,8 @@ function _mapStateToProps(state) {
_configHosts: configHosts,
_connecting: Boolean(connecting) || Boolean(thenableWithCancel),
_error: connectionError || authenticateAndUpgradeRoleError,
_progress: progress
_progress: progress,
_styles: ColorSchemeRegistry.get(state, 'LoginDialog')
};
}

View File

@@ -1,50 +1,41 @@
import { BoxModel, ColorPalette, createStyleSheet } from '../../base/styles';
/**
* The style common to {@code LoginDialog} and {@code WaitForOwnerDialog}.
*/
const dialog = {
marginBottom: BoxModel.margin,
marginTop: BoxModel.margin
};
/**
* The style common to {@code Text} rendered by {@code LoginDialog} and
* {@code WaitForOwnerDialog}.
*/
const text = {
color: ColorPalette.white
};
import { ColorSchemeRegistry, schemeColor } from '../../base/color-scheme';
import { BoxModel } from '../../base/styles';
/**
* The styles of the authentication feature.
*/
export default createStyleSheet({
ColorSchemeRegistry.register('LoginDialog', {
/**
* The style of {@code Text} rendered by the {@code Dialog}s of the
* feature authentication.
*/
dialogText: {
...text,
margin: BoxModel.margin,
marginTop: BoxModel.margin * 2
},
/**
* The style used when an error message is rendered.
*/
errorMessage: {
color: schemeColor('errorText')
},
/**
* The style of {@code LoginDialog}.
*/
loginDialog: {
...dialog,
flex: 0,
flexDirection: 'column'
flexDirection: 'column',
marginBottom: BoxModel.margin,
marginTop: BoxModel.margin
},
/**
* The style of {@code WaitForOwnerDialog}.
* The style used then a progress message is rendered.
*/
waitForOwnerDialog: {
...dialog,
...text
progressMessage: {
color: schemeColor('text')
}
});

View File

@@ -54,6 +54,11 @@ export type Props = {
*/
size: number,
/**
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
*/
status?: ?string,
/**
* URL of the avatar, if any.
*/
@@ -117,6 +122,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
colorBase,
id,
size,
status,
url
} = this.props;
const { avatarFailed } = this.state;
@@ -128,6 +134,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
initials: undefined,
onAvatarLoadError: undefined,
size,
status,
url: undefined
};

View File

@@ -12,6 +12,11 @@ import styles from './styles';
type Props = AbstractProps & {
/**
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
*/
status?: ?string,
/**
* External style passed to the componant.
*/
@@ -46,18 +51,40 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
}
return (
<View
style = { [
styles.avatarContainer(size),
style
] }>
{ avatar }
<View>
<View
style = { [
styles.avatarContainer(size),
style
] }>
{ avatar }
</View>
{ this._renderAvatarStatus() }
</View>
);
}
_isIcon: (?string | ?Object) => boolean
/**
* Renders a badge representing the avatar status.
*
* @returns {React$Elementaa}
*/
_renderAvatarStatus() {
const { size, status } = this.props;
if (!status) {
return null;
}
return (
<View style = { styles.badgeContainer }>
<View style = { styles.badge(size, status) } />
</View>
);
}
/**
* Renders the default avatar.
*

View File

@@ -1,5 +1,7 @@
// @flow
import { StyleSheet } from 'react-native';
import { ColorPalette } from '../../../styles';
const DEFAULT_SIZE = 65;
@@ -27,6 +29,38 @@ export default {
};
},
badge: (size: number = DEFAULT_SIZE, status: string) => {
let color;
switch (status) {
case 'available':
color = 'rgb(110, 176, 5)';
break;
case 'away':
color = 'rgb(250, 201, 20)';
break;
case 'busy':
color = 'rgb(233, 0, 27)';
break;
case 'idle':
color = 'rgb(172, 172, 172)';
break;
}
return {
backgroundColor: color,
borderRadius: size / 2,
bottom: 0,
height: size * 0.3,
position: 'absolute',
width: size * 0.3
};
},
badgeContainer: {
...StyleSheet.absoluteFillObject
},
initialsContainer: {
alignItems: 'center',
alignSelf: 'stretch',

View File

@@ -21,7 +21,12 @@ type Props = AbstractProps & {
/**
* ID of the component to be rendered.
*/
id?: string
id?: string,
/**
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
*/
status?: ?string
};
/**
@@ -40,29 +45,33 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
if (this._isIcon(url)) {
return (
<div
className = { this._getAvatarClassName() }
className = { `${this._getAvatarClassName()} ${this._getBadgeClassName()}` }
id = { this.props.id }
style = { this._getAvatarStyle(this.props.color) }>
<Icon src = { url } />
<Icon
size = '50%'
src = { url } />
</div>
);
}
if (url) {
return (
<img
className = { this._getAvatarClassName() }
id = { this.props.id }
onError = { this.props.onAvatarLoadError }
src = { url }
style = { this._getAvatarStyle() } />
<div className = { this._getBadgeClassName() }>
<img
className = { this._getAvatarClassName() }
id = { this.props.id }
onError = { this.props.onAvatarLoadError }
src = { url }
style = { this._getAvatarStyle() } />
</div>
);
}
if (initials) {
return (
<div
className = { this._getAvatarClassName() }
className = { `${this._getAvatarClassName()} ${this._getBadgeClassName()}` }
id = { this.props.id }
style = { this._getAvatarStyle(this.props.color) }>
<svg
@@ -85,11 +94,13 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
// default avatar
return (
<img
className = { this._getAvatarClassName('defaultAvatar') }
id = { this.props.id }
src = { this.props.defaultAvatar || 'images/avatar.png' }
style = { this._getAvatarStyle() } />
<div className = { this._getBadgeClassName() }>
<img
className = { this._getAvatarClassName('defaultAvatar') }
id = { this.props.id }
src = { this.props.defaultAvatar || 'images/avatar.png' }
style = { this._getAvatarStyle() } />
</div>
);
}
@@ -120,5 +131,20 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
return `avatar ${additional || ''} ${this.props.className || ''}`;
}
/**
* Generates a class name to render a badge on the avatar, if necessary.
*
* @returns {string}
*/
_getBadgeClassName() {
const { status } = this.props;
if (status) {
return `avatar-badge avatar-badge-${status}`;
}
return '';
}
_isIcon: (?string | ?Object) => boolean
}

View File

@@ -10,6 +10,7 @@ export default {
// Generic app theme colors that are used accross the entire app.
// All scheme definitions below inherit these values.
background: 'rgb(255, 255, 255)',
errorText: ColorPalette.red,
icon: 'rgb(28, 32, 37)',
text: 'rgb(28, 32, 37)'
},

View File

@@ -46,6 +46,7 @@ import {
getCurrentConference
} from './functions';
import logger from './logger';
import { MEDIA_TYPE } from '../media';
declare var APP: Object;
@@ -589,7 +590,10 @@ function _syncReceiveVideoQuality({ getState }, next, action) {
function _trackAddedOrRemoved(store, next, action) {
const track = action.track;
if (track && track.local) {
// TODO All track swapping should happen here instead of conference.js.
// Since we swap the tracks for the web client in conference.js, ignore
// presenter tracks here and do not add/remove them to/from the conference.
if (track && track.local && track.mediaType !== MEDIA_TYPE.PRESENTER) {
return (
_syncConferenceLocalTracksWithState(store, action)
.then(() => next(action)));

View File

@@ -139,6 +139,39 @@ export function groupDevicesByKind(devices: Object[]): Object {
};
}
/**
* Filters audio devices from a list of MediaDeviceInfo objects.
*
* @param {Array<MediaDeviceInfo>} devices - Unfiltered media devices.
* @private
* @returns {Array<MediaDeviceInfo>} Filtered audio devices.
*/
export function filterAudioDevices(devices: Object[]): Object {
return devices.filter(device => device.kind === 'audioinput');
}
/**
* We want to strip any device details that are not very user friendly, like usb ids put in brackets at the end.
*
* @param {string} label - Device label to format.
*
* @returns {string} - Formatted string.
*/
export function formatDeviceLabel(label: string) {
let formattedLabel = label;
// Remove braked description at the end as it contains non user friendly strings i.e.
// Microsoft® LifeCam HD-3000 (045e:0779:31dg:d1231)
const ix = formattedLabel.lastIndexOf('(');
if (ix !== -1) {
formattedLabel = formattedLabel.substr(0, ix);
}
return formattedLabel;
}
/**
* Set device id of the audio output device which is currently in use.
* Empty string stands for default device.

View File

@@ -20,7 +20,7 @@ import {
} from './actionTypes';
import { showNotification, showWarningNotification } from '../../notifications';
import { updateSettings } from '../settings';
import { setAudioOutputDeviceId } from './functions';
import { formatDeviceLabel, setAudioOutputDeviceId } from './functions';
import logger from './logger';
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
@@ -186,12 +186,7 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
// we want to strip any device details that are not very
// user friendly, like usb ids put in brackets at the end
let description = newDevice.label;
const ix = description.lastIndexOf('(');
if (ix !== -1) {
description = description.substr(0, ix);
}
const description = formatDeviceLabel(newDevice.label);
let titleKey;

View File

@@ -1,7 +1,7 @@
// @flow
import React, { PureComponent, type Node } from 'react';
import { SafeAreaView, ScrollView, View } from 'react-native';
import { PanResponder, SafeAreaView, ScrollView, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { SlidingView } from '../../../react';
@@ -10,6 +10,16 @@ import { StyleType } from '../../../styles';
import { bottomSheetStyles as styles } from './styles';
/**
* Minimal distance that needs to be moved by the finger to consider it a swipe.
*/
const GESTURE_DISTANCE_THRESHOLD = 5;
/**
* The minimal speed needed to be achieved by the finger to consider it as a swipe.
*/
const GESTURE_SPEED_THRESHOLD = 0.2;
/**
* The type of {@code BottomSheet}'s React {@code Component} prop types.
*/
@@ -29,13 +39,40 @@ type Props = {
* Handler for the cancel event, which happens when the user dismisses
* the sheet.
*/
onCancel: ?Function
onCancel: ?Function,
/**
* Callback to be attached to the custom swipe event of the BottomSheet.
*/
onSwipe?: Function,
/**
* Function to render a bottom sheet header element, if necessary.
*/
renderHeader: ?Function
};
/**
* A component emulating Android's BottomSheet.
*/
class BottomSheet extends PureComponent<Props> {
panResponder: Object;
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: this._onShouldSetResponder.bind(this),
onMoveShouldSetPanResponder: this._onShouldSetResponder.bind(this),
onPanResponderRelease: this._onGestureEnd.bind(this)
});
}
/**
* Implements React's {@link Component#render()}.
*
@@ -43,7 +80,7 @@ class BottomSheet extends PureComponent<Props> {
* @returns {ReactElement}
*/
render() {
const { _styles } = this.props;
const { _styles, renderHeader } = this.props;
return (
<SlidingView
@@ -56,23 +93,66 @@ class BottomSheet extends PureComponent<Props> {
<View
pointerEvents = 'box-none'
style = { styles.sheetAreaCover } />
<View
{ renderHeader && renderHeader() }
<SafeAreaView
style = { [
styles.sheetItemContainer,
_styles.sheet
] }>
<SafeAreaView>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false } >
{ this.props.children }
</ScrollView>
</SafeAreaView>
</View>
] }
{ ...this.panResponder.panHandlers }>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }
style = { styles.scrollView } >
{ this.props.children }
</ScrollView>
</SafeAreaView>
</View>
</SlidingView>
);
}
/**
* Callback to handle a gesture end event.
*
* @param {Object} evt - The native gesture event.
* @param {Object} gestureState - The gesture state.
* @returns {void}
*/
_onGestureEnd(evt, gestureState) {
const verticalSwipe = Math.abs(gestureState.vy) > Math.abs(gestureState.vx)
&& Math.abs(gestureState.vy) > GESTURE_SPEED_THRESHOLD;
if (verticalSwipe) {
const direction = gestureState.vy > 0 ? 'down' : 'up';
const { onCancel, onSwipe } = this.props;
let isSwipeHandled = false;
if (onSwipe) {
isSwipeHandled = onSwipe(direction);
}
if (direction === 'down' && !isSwipeHandled) {
// Swipe down is a special gesture that can be used to close the
// BottomSheet, so if the swipe is not handled by the parent
// component, we consider it as a request to close.
onCancel && onCancel();
}
}
}
/**
* Returns true if the pan responder should activate, false otherwise.
*
* @param {Object} evt - The native gesture event.
* @param {Object} gestureState - The gesture state.
* @returns {boolean}
*/
_onShouldSetResponder({ nativeEvent }, gestureState) {
return nativeEvent.touches.length === 1
&& Math.abs(gestureState.dx) > GESTURE_DISTANCE_THRESHOLD
&& Math.abs(gestureState.dy) > GESTURE_DISTANCE_THRESHOLD;
}
}
/**

View File

@@ -33,6 +33,10 @@ export const bottomSheetStyles = {
flex: 1
},
scrollView: {
paddingHorizontal: MD_ITEM_MARGIN_PADDING
},
/**
* Style for the container of the sheet.
*/
@@ -44,9 +48,7 @@ export const bottomSheetStyles = {
},
sheetItemContainer: {
flex: -1,
maxHeight: '60%',
paddingHorizontal: MD_ITEM_MARGIN_PADDING
flex: -1
}
};
@@ -135,23 +137,45 @@ export const inputDialog = {
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
*/
ColorSchemeRegistry.register('BottomSheet', {
/**
* Style for the {@code Icon} element in a generic item of the menu.
*/
iconStyle: {
color: schemeColor('icon'),
fontSize: 24
buttons: {
/**
* Style for the {@code Icon} element in a generic item of the menu.
*/
iconStyle: {
color: schemeColor('icon'),
fontSize: 24
},
/**
* Style for the label in a generic item rendered in the menu.
*/
labelStyle: {
color: schemeColor('text'),
flexShrink: 1,
fontSize: MD_FONT_SIZE,
marginLeft: 32,
opacity: 0.90
},
/**
* Container style for a generic item rendered in the menu.
*/
style: {
alignItems: 'center',
flexDirection: 'row',
height: MD_ITEM_HEIGHT
},
/**
* Additional style that is not directly used as a style object.
*/
underlayColor: ColorPalette.overflowMenuItemUnderlay
},
/**
* Style for the label in a generic item rendered in the menu.
*/
labelStyle: {
color: schemeColor('text'),
flexShrink: 1,
fontSize: MD_FONT_SIZE,
marginLeft: 32,
opacity: 0.90
expandIcon: {
color: schemeColor('icon'),
fontSize: 16,
opacity: 0.7
},
/**
@@ -159,21 +183,7 @@ ColorSchemeRegistry.register('BottomSheet', {
*/
sheet: {
backgroundColor: schemeColor('background')
},
/**
* Container style for a generic item rendered in the menu.
*/
style: {
alignItems: 'center',
flexDirection: 'row',
height: MD_ITEM_HEIGHT
},
/**
* Additional style that is not directly used as a style object.
*/
underlayColor: ColorPalette.overflowMenuItemUnderlay
}
});
/**

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><defs><path id="a" d="M0 0h24v24H0V0z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><path clip-path="url(#b)" d="M20 9H4v2h16V9zM4 15h16v-2H4v2z"/></svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -25,6 +25,7 @@ export { default as IconDeviceHeadphone } from './headset.svg';
export { default as IconDeviceSpeaker } from './volume.svg';
export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
export { default as IconDragHandle } from './drag-handle.svg';
export { default as IconEventNote } from './event_note.svg';
export { default as IconExitFullScreen } from './exit-full-screen.svg';
export { default as IconFeedback } from './feedback.svg';

View File

@@ -14,6 +14,7 @@ export const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
export const JitsiConnectionEvents = JitsiMeetJS.events.connection;
export const JitsiConnectionQualityEvents
= JitsiMeetJS.events.connectionQuality;
export const JitsiDetectionEvents = JitsiMeetJS.events.detection;
export const JitsiE2ePingEvents = JitsiMeetJS.events.e2eping;
export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;
export const JitsiParticipantConnectionStatus

View File

@@ -11,7 +11,11 @@ import {
STORE_VIDEO_TRANSFORM,
TOGGLE_CAMERA_FACING_MODE
} from './actionTypes';
import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
VIDEO_MUTISM_AUTHORITY
} from './constants';
/**
* Action to adjust the availability of the local audio.
@@ -89,6 +93,7 @@ export function setVideoAvailable(available: boolean) {
*
* @param {boolean} muted - True if the local video is to be muted or false if
* the local video is to be unmuted.
* @param {MEDIA_TYPE} mediaType - The type of media.
* @param {number} authority - The {@link VIDEO_MUTISM_AUTHORITY} which is
* muting/unmuting the local video.
* @param {boolean} ensureTrack - True if we want to ensure that a new track is
@@ -97,6 +102,7 @@ export function setVideoAvailable(available: boolean) {
*/
export function setVideoMuted(
muted: boolean,
mediaType: MEDIA_TYPE = MEDIA_TYPE.VIDEO,
authority: number = VIDEO_MUTISM_AUTHORITY.USER,
ensureTrack: boolean = false) {
return (dispatch: Dispatch<any>, getState: Function) => {
@@ -107,6 +113,8 @@ export function setVideoMuted(
return dispatch({
type: SET_VIDEO_MUTED,
authority,
mediaType,
ensureTrack,
muted: newValue
});

View File

@@ -15,6 +15,7 @@ export const CAMERA_FACING_MODE = {
*/
export const MEDIA_TYPE = {
AUDIO: 'audio',
PRESENTER: 'presenter',
VIDEO: 'video'
};

View File

@@ -17,7 +17,11 @@ import { getPropertyValue } from '../settings';
import { setTrackMuted, TRACK_ADDED } from '../tracks';
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
VIDEO_MUTISM_AUTHORITY
} from './constants';
import logger from './logger';
import {
_AUDIO_INITIAL_MEDIA_STATE,
@@ -45,7 +49,10 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { track } = action;
track.local && _syncTrackMutedState(store, track);
// Don't sync track mute state with the redux store for screenshare
// since video mute state represents local camera mute state only.
track.local && track.videoType !== 'desktop'
&& _syncTrackMutedState(store, track);
return result;
}
@@ -72,7 +79,7 @@ function _appStateChanged({ dispatch }, next, action) {
sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
return next(action);
}
@@ -94,7 +101,11 @@ function _setAudioOnly({ dispatch }, next, action) {
sendAnalytics(createTrackMutedEvent('video', 'audio-only mode', audioOnly));
dispatch(setVideoMuted(audioOnly, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
// Make sure we mute both the desktop and video tracks.
dispatch(setVideoMuted(
audioOnly, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
dispatch(setVideoMuted(
audioOnly, MEDIA_TYPE.PRESENTER, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
return next(action);
}
@@ -231,7 +242,9 @@ function _setRoom({ dispatch, getState }, next, action) {
*/
function _syncTrackMutedState({ getState }, track) {
const state = getState()['features/base/media'];
const muted = Boolean(state[track.mediaType].muted);
const mediaType = track.mediaType === MEDIA_TYPE.PRESENTER
? MEDIA_TYPE.VIDEO : track.mediaType;
const muted = Boolean(state[mediaType].muted);
// XXX If muted state of track when it was added is different from our media
// muted state, we need to mute track and explicitly modify 'muted' property

View File

@@ -23,6 +23,11 @@ type Props = {
*/
avatarSize?: number,
/**
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
*/
avatarStatus?: ?string,
/**
* External style to be applied to the avatar (icon).
*/
@@ -83,6 +88,7 @@ export default class AvatarListItem extends Component<Props> {
const {
avatarOnly,
avatarSize = AVATAR_SIZE,
avatarStatus,
avatarStyle
} = this.props;
const { avatar, colorBase, lines, title } = this.props.item;
@@ -96,6 +102,7 @@ export default class AvatarListItem extends Component<Props> {
colorBase = { colorBase }
displayName = { title }
size = { avatarSize }
status = { avatarStatus }
style = { avatarStyle }
url = { avatar } />
{ avatarOnly || <Container style = { styles.listItemDetails }>

View File

@@ -1,3 +1,14 @@
/**
* The type of Redux action which sets the noSrcDataNotificationUid state representing the UID of the previous
* no data from source notification. Used to check if such a notification was previously displayed.
*
* {
* type: SET_NO_SRC_DATA_NOTIFICATION_UID,
* uid: ?number
* }
*/
export const SET_NO_SRC_DATA_NOTIFICATION_UID = 'SET_NO_SRC_DATA_NOTIFICATION_UID';
/**
* The type of redux action dispatched to disable screensharing or to start the
* flow for enabling screenshare.

View File

@@ -14,6 +14,7 @@ import {
import { getLocalParticipant } from '../participants';
import {
SET_NO_SRC_DATA_NOTIFICATION_UID,
TOGGLE_SCREENSHARING,
TRACK_ADDED,
TRACK_CREATE_CANCELED,
@@ -342,6 +343,9 @@ export function trackAdded(track) {
let isReceivingData, noDataFromSourceNotificationInfo, participantId;
if (local) {
// Reset the no data from src notification state when we change the track, as it's context is set
// on a per device basis.
dispatch(setNoSrcDataNotificationUid());
const participant = getLocalParticipant(getState);
if (participant) {
@@ -358,6 +362,12 @@ export function trackAdded(track) {
});
dispatch(notificationAction);
// Set the notification ID so that other parts of the application know that this was
// displayed in the context of the current device.
// I.E. The no-audio-signal notification shouldn't be displayed if this was already shown.
dispatch(setNoSrcDataNotificationUid(notificationAction.uid));
noDataFromSourceNotificationInfo = { uid: notificationAction.uid };
} else {
const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(track)), 5000);
@@ -638,3 +648,20 @@ function _trackCreateCanceled(mediaType) {
trackType: mediaType
};
}
/**
* Sets UID of the displayed no data from source notification. Used to track
* if the notification was previously displayed in this context.
*
* @param {number} uid - Notification UID.
* @returns {{
* type: SET_NO_AUDIO_SIGNAL_UID,
* uid: number
* }}
*/
export function setNoSrcDataNotificationUid(uid) {
return {
type: SET_NO_SRC_DATA_NOTIFICATION_UID,
uid
};
}

View File

@@ -10,6 +10,47 @@ import {
import logger from './logger';
/**
* Creates a local video track for presenter. The constraints are computed based
* on the height of the desktop that is being shared.
*
* @param {Object} options - The options with which the local presenter track
* is to be created.
* @param {string|null} [options.cameraDeviceId] - Camera device id or
* {@code undefined} to use app's settings.
* @param {number} desktopHeight - The height of the desktop that is being
* shared.
* @returns {Promise<JitsiLocalTrack>}
*/
export async function createLocalPresenterTrack(options, desktopHeight) {
const { cameraDeviceId } = options;
// compute the constraints of the camera track based on the resolution
// of the desktop screen that is being shared.
const cameraHeights = [ 180, 270, 360, 540, 720 ];
const proportion = 4;
const result = cameraHeights.find(
height => (desktopHeight / proportion) < height);
const constraints = {
video: {
aspectRatio: 4 / 3,
height: {
ideal: result
}
}
};
const [ videoTrack ] = await JitsiMeetJS.createLocalTracks(
{
cameraDeviceId,
constraints,
devices: [ 'video' ]
});
videoTrack.type = MEDIA_TYPE.PRESENTER;
return videoTrack;
}
/**
* Create local tracks of specific types.
*
@@ -53,11 +94,15 @@ export function createLocalTracksF(
const state = store.getState();
const {
constraints,
desktopSharingFrameRate,
firefox_fake_device, // eslint-disable-line camelcase
resolution
} = state['features/base/config'];
const constraints = options.constraints
?? state['features/base/config'].constraints;
// Do not load blur effect if option for ignoring effects is present.
// This is needed when we are creating a video track for presenter mode.
const loadEffectsPromise = state['features/blur'].blurEnabled
? getBlurEffect()
.then(blurEffect => [ blurEffect ])
@@ -157,6 +202,18 @@ export function getLocalVideoTrack(tracks) {
return getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
}
/**
* Returns the media type of the local video, presenter or video.
*
* @param {Track[]} tracks - List of all tracks.
* @returns {MEDIA_TYPE}
*/
export function getLocalVideoType(tracks) {
const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
return presenterTrack ? MEDIA_TYPE.PRESENTER : MEDIA_TYPE.VIDEO;
}
/**
* Returns track of specified media type for specified participant id.
*
@@ -197,6 +254,29 @@ export function getTracksByMediaType(tracks, mediaType) {
return tracks.filter(t => t.mediaType === mediaType);
}
/**
* Checks if the local video track in the given set of tracks is muted.
*
* @param {Track[]} tracks - List of all tracks.
* @returns {Track[]}
*/
export function isLocalVideoTrackMuted(tracks) {
const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
const videoTrack = getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
// Make sure we check the mute status of only camera tracks, i.e.,
// presenter track when it exists, camera track when the presenter
// track doesn't exist.
if (presenterTrack) {
return isLocalTrackMuted(tracks, MEDIA_TYPE.PRESENTER);
} else if (videoTrack) {
return videoTrack.videoType === 'camera'
? isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO) : true;
}
return true;
}
/**
* Checks if the first local track in the given tracks set is muted.
*

View File

@@ -6,6 +6,7 @@ import {
SET_AUDIO_MUTED,
SET_CAMERA_FACING_MODE,
SET_VIDEO_MUTED,
VIDEO_MUTISM_AUTHORITY,
TOGGLE_CAMERA_FACING_MODE,
toggleCameraFacingMode
} from '../media';
@@ -89,7 +90,7 @@ MiddlewareRegistry.register(store => next => action => {
return;
}
_setMuted(store, action, MEDIA_TYPE.VIDEO);
_setMuted(store, action, action.mediaType);
break;
case TOGGLE_CAMERA_FACING_MODE: {
@@ -131,10 +132,15 @@ MiddlewareRegistry.register(store => next => action => {
const { jitsiTrack } = action.track;
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();
const isVideoTrack = jitsiTrack.isVideoTrack();
const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
if (isVideoTrack) {
if (jitsiTrack.isLocal()) {
if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
APP.conference.mutePresenter(muted);
}
// Make sure we change the video mute state only for camera tracks.
if (jitsiTrack.isLocal() && jitsiTrack.videoType !== 'desktop') {
APP.conference.setVideoMuteStatus(muted);
} else {
APP.UI.setVideoMuted(participantID, muted);
@@ -255,7 +261,7 @@ function _removeNoDataFromSourceNotification({ getState, dispatch }, track) {
* @private
* @returns {void}
*/
function _setMuted(store, { ensureTrack, muted }, mediaType: MEDIA_TYPE) {
function _setMuted(store, { ensureTrack, authority, muted }, mediaType: MEDIA_TYPE) {
const localTrack
= _getLocalTrack(store, mediaType, /* includePending */ true);
@@ -265,8 +271,12 @@ function _setMuted(store, { ensureTrack, muted }, mediaType: MEDIA_TYPE) {
// `jitsiTrack`, then the `muted` state will be applied once the
// `jitsiTrack` is created.
const { jitsiTrack } = localTrack;
const isAudioOnly = authority === VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY;
jitsiTrack && setTrackMuted(jitsiTrack, muted);
// screenshare cannot be muted or unmuted using the video mute button
// anymore, unless it is muted by audioOnly.
jitsiTrack && (jitsiTrack.videoType !== 'desktop' || isAudioOnly)
&& setTrackMuted(jitsiTrack, muted);
} else if (!muted && ensureTrack && typeof APP === 'undefined') {
// FIXME: This only runs on mobile now because web has its own way of
// creating local tracks. Adjust the check once they are unified.

View File

@@ -1,7 +1,8 @@
import { PARTICIPANT_ID_CHANGED } from '../participants';
import { ReducerRegistry } from '../redux';
import { ReducerRegistry, set } from '../redux';
import {
SET_NO_SRC_DATA_NOTIFICATION_UID,
TRACK_ADDED,
TRACK_CREATE_CANCELED,
TRACK_CREATE_ERROR,
@@ -133,3 +134,17 @@ ReducerRegistry.register('features/base/tracks', (state = [], action) => {
return state;
}
});
/**
* Listen for actions that mutate the no-src-data state, like the current notification id
*/
ReducerRegistry.register('features/base/no-src-data', (state = {}, action) => {
switch (action.type) {
case SET_NO_SRC_DATA_NOTIFICATION_UID:
return set(state, 'noSrcDataNotificationUid', action.uid);
default:
return state;
}
});

View File

@@ -32,7 +32,7 @@ const EMOTICON_REGEXP_ARRAY: Array<Array<Object>> = [];
// Adding slack-type emoji format
escapedValues.push(escapeRegexp(`:${key}:`));
const regexp = `(${escapedValues.join('|')})`;
const regexp = `\\B(${escapedValues.join('|')})\\B`;
EMOTICON_REGEXP_ARRAY.push([ new RegExp(regexp, 'g'), value ]);
}

View File

@@ -443,6 +443,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
<AvatarListItem
avatarOnly = { true }
avatarSize = { AVATAR_SIZE }
avatarStatus = { item.status }
avatarStyle = { styles.avatar }
avatarTextStyle = { styles.avatarText }
item = { renderableItem }
@@ -497,6 +498,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
style = { styles.itemWrapper }>
<AvatarListItem
avatarSize = { AVATAR_SIZE }
avatarStatus = { item.status }
avatarStyle = { styles.avatar }
avatarTextStyle = { styles.avatarText }
item = { renderableItem }

View File

@@ -1,11 +1,11 @@
// @flow
import Avatar from '@atlaskit/avatar';
import InlineMessage from '@atlaskit/inline-message';
import React from 'react';
import type { Dispatch } from 'redux';
import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
import { Avatar } from '../../../../base/avatar';
import { Dialog, hideDialog } from '../../../../base/dialog';
import { translate, translateToHTML } from '../../../../base/i18n';
import { Icon, IconPhone } from '../../../../base/icons';
@@ -289,13 +289,15 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
return {
content: user.name,
elemBefore: <Avatar
size = 'small'
src = { user.avatar } />,
className = { 'avatar-small' }
status = { user.status }
url = { user.avatar } />,
item: user,
tag: {
elemBefore: <Avatar
size = 'xsmall'
src = { user.avatar } />
className = { 'avatar-xsmall' }
status = { user.status }
url = { user.avatar } />
},
value: user.id || user.user_id
};

View File

@@ -270,8 +270,8 @@ class AudioRoutePickerDialog extends Component<Props, State> {
<View style = { styles.deviceRow } >
<Icon
src = { icon }
style = { [ styles.deviceIcon, _bottomSheetStyles.iconStyle, selectedStyle ] } />
<Text style = { [ styles.deviceText, _bottomSheetStyles.labelStyle, selectedStyle ] } >
style = { [ styles.deviceIcon, _bottomSheetStyles.buttons.iconStyle, selectedStyle ] } />
<Text style = { [ styles.deviceText, _bottomSheetStyles.buttons.labelStyle, selectedStyle ] } >
{ text }
</Text>
</View>
@@ -292,8 +292,8 @@ class AudioRoutePickerDialog extends Component<Props, State> {
<View style = { styles.deviceRow } >
<Icon
src = { deviceInfoMap.SPEAKER.icon }
style = { [ styles.deviceIcon, _bottomSheetStyles.iconStyle ] } />
<Text style = { [ styles.deviceText, _bottomSheetStyles.labelStyle ] } >
style = { [ styles.deviceIcon, _bottomSheetStyles.buttons.iconStyle ] } />
<Text style = { [ styles.deviceText, _bottomSheetStyles.buttons.labelStyle ] } >
{ t('audioDevices.none') }
</Text>
</View>

View File

@@ -1,6 +1,6 @@
// @flow
import { Alert, Platform } from 'react-native';
import { Alert, NativeModules, Platform } from 'react-native';
import uuid from 'uuid';
import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
@@ -36,6 +36,7 @@ import CallKit from './CallKit';
import ConnectionService from './ConnectionService';
import { isCallIntegrationEnabled } from './functions';
const { AudioMode } = NativeModules;
const CallIntegration = CallKit || ConnectionService;
/**
@@ -185,13 +186,25 @@ function _conferenceJoined({ getState }, next, action) {
const { callUUID } = action.conference;
if (callUUID) {
CallIntegration.reportConnectedOutgoingCall(callUUID).then(() => {
// iOS 13 doesn't like the mute state to be false before the call is started
// so we update it here in case the user selected startWithAudioMuted.
if (Platform.OS === 'ios') {
_updateCallIntegrationMuted(action.conference, getState());
}
});
CallIntegration.reportConnectedOutgoingCall(callUUID)
.then(() => {
// iOS 13 doesn't like the mute state to be false before the call is started
// so we update it here in case the user selected startWithAudioMuted.
if (Platform.OS === 'ios') {
_updateCallIntegrationMuted(action.conference, getState());
}
})
.catch(error => {
// Currently this error code is emitted only by Android.
//
if (error.code === 'CONNECTION_NOT_FOUND_ERROR') {
// Some Samsung devices will fail to fully engage ConnectionService if no SIM card
// was ever installed on the device. We could check for it, but it would require
// the CALL_PHONE permission, which is not something we want to do, so fallback to
// not using ConnectionService.
_handleConnectionServiceFailure(getState());
}
});
}
return result;
@@ -276,7 +289,8 @@ function _conferenceWillJoin({ dispatch, getState }, next, action) {
}
})
.catch(error => {
// Currently this error code is emitted only by Android.
// Currently this error codes are emitted only by Android.
//
if (error.code === 'CREATE_OUTGOING_CALL_FAILED') {
// We're not tracking the call anymore - it doesn't exist on
// the native side.
@@ -290,12 +304,45 @@ function _conferenceWillJoin({ dispatch, getState }, next, action) {
{ text: 'OK' }
],
{ cancelable: false });
} else if (error.code === 'SECURITY_ERROR') {
// Some devices fail because the CALL_PHONE permission is not granted, which is
// nonsense, because it's not needed for self-managed connections.
_handleConnectionServiceFailure(state);
}
});
return result;
}
/**
* Handles a ConnectionService fatal error by falling back to non-ConnectionService device management.
*
* @param {Object} state - Redux store.
* @returns {void}
*/
function _handleConnectionServiceFailure(state: Object) {
const conference = getCurrentConference(state);
if (conference) {
// We're not tracking the call anymore.
delete conference.callUUID;
// ConnectionService has fatally failed. Alas, this also means audio device management would be broken, so
// fallback to not using ConnectionService.
// NOTE: We are not storing this in Settings, in case it's a transient issue, as far fetched as
// that may be.
if (AudioMode.setUseConnectionService) {
AudioMode.setUseConnectionService(false);
const hasVideo = !isVideoMutedByAudioOnly(state);
// Set the desired audio mode, since we just reset the whole thing.
AudioMode.setMode(hasVideo ? AudioMode.VIDEO_CALL : AudioMode.AUDIO_CALL);
}
}
}
/**
* Handles CallKit's event {@code performEndCallAction}.
*

View File

@@ -0,0 +1,11 @@
/**
* The type of Redux action which sets the pending notification UID
* to use it when hiding the notification is necessary, or unset it when
* undefined (or no param) is passed.
*
* {
* type: SET_CURRENT_NOTIFICATION_UID,
* uid: ?number
* }
*/
export const SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID = 'SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID';

View File

@@ -0,0 +1,21 @@
// @flow
import { SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID } from './actionTypes';
/**
* Sets UID of the the pending notification to use it when hiding
* the notification is necessary, or unset it when undefined (or no param) is
* passed.
*
* @param {?number} uid - The UID of the notification.
* @returns {{
* type: SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID,
* uid: number
* }}
*/
export function setNoAudioSignalNotificationUid(uid: ?number) {
return {
type: SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID,
uid
};
}

View File

@@ -0,0 +1,6 @@
/**
* The identifier of the sound to be played when we got an event for no audio signal.
*
* @type {string}
*/
export const NO_AUDIO_SIGNAL_SOUND_ID = 'NO_AUDIO_SIGNAL_SOUND_ID';

View File

@@ -0,0 +1,4 @@
// @flow
import './middleware';
import './reducer';

View File

@@ -0,0 +1,135 @@
// @flow
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { CONFERENCE_JOINED } from '../base/conference';
import {
formatDeviceLabel,
setAudioInputDevice
} from '../base/devices';
import JitsiMeetJS, { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../base/redux';
import { updateSettings } from '../base/settings';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { hideNotification, showNotification } from '../notifications';
import { setNoAudioSignalNotificationUid } from './actions';
import { NO_AUDIO_SIGNAL_SOUND_ID } from './constants';
import { NO_AUDIO_SIGNAL_SOUND_FILE } from './sounds';
MiddlewareRegistry.register(store => next => async action => {
const result = next(action);
const { dispatch } = store;
switch (action.type) {
case APP_WILL_MOUNT:
dispatch(registerSound(NO_AUDIO_SIGNAL_SOUND_ID, NO_AUDIO_SIGNAL_SOUND_FILE));
break;
case APP_WILL_UNMOUNT:
dispatch(unregisterSound(NO_AUDIO_SIGNAL_SOUND_ID));
break;
case CONFERENCE_JOINED:
_handleNoAudioSignalNotification(store, action);
break;
}
return result;
});
/**
* Handles the logic of displaying the no audio input detected notification as well as finding a valid device on the
* system.
*
* @param {Store} store - The redux store in which the specified action is being dispatched.
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is being dispatched in the specified redux
* store.
* @private
* @returns {void}
*/
async function _handleNoAudioSignalNotification({ dispatch, getState }, action) {
const { conference } = action;
let confAudioInputState;
conference.on(JitsiConferenceEvents.AUDIO_INPUT_STATE_CHANGE, hasAudioInput => {
const { noAudioSignalNotificationUid } = getState()['features/no-audio-signal'];
confAudioInputState = hasAudioInput;
// In case the notification is displayed but the conference detected audio input signal we hide it.
if (noAudioSignalNotificationUid && hasAudioInput) {
dispatch(hideNotification(noAudioSignalNotificationUid));
dispatch(setNoAudioSignalNotificationUid());
}
});
conference.on(JitsiConferenceEvents.NO_AUDIO_INPUT, async () => {
const { noSrcDataNotificationUid } = getState()['features/base/no-src-data'];
// In case the 'no data detected from source' notification was already shown, we prevent the
// no audio signal notification as it's redundant i.e. it's clear that the users microphone is
// muted from system settings.
if (noSrcDataNotificationUid) {
return;
}
// Force the flag to false in case AUDIO_INPUT_STATE_CHANGE is received after the notification is displayed,
// possibly preventing the notification from displaying because of an outdated state.
confAudioInputState = false;
const activeDevice = await JitsiMeetJS.getActiveAudioDevice();
if (confAudioInputState) {
return;
}
// In case there is a previous notification displayed just hide it.
const { noAudioSignalNotificationUid } = getState()['features/no-audio-signal'];
if (noAudioSignalNotificationUid) {
dispatch(hideNotification(noAudioSignalNotificationUid));
dispatch(setNoAudioSignalNotificationUid());
}
let descriptionKey = 'toolbar.noAudioSignalDesc';
let customActionNameKey;
let customActionHandler;
// In case the detector picked up a device show a notification with a device suggestion
if (activeDevice.deviceLabel !== '') {
descriptionKey = 'toolbar.noAudioSignalDescSuggestion';
// Preferably the label should be passed as an argument paired with a i18next string, however
// at the point of the implementation the showNotification function only supports doing that for
// the description.
// TODO Add support for arguments to showNotification title and customAction strings.
customActionNameKey = `Use ${formatDeviceLabel(activeDevice.deviceLabel)}`;
customActionHandler = () => {
// Select device callback
dispatch(
updateSettings({
userSelectedMicDeviceId: activeDevice.deviceId,
userSelectedMicDeviceLabel: activeDevice.deviceLabel
})
);
dispatch(setAudioInputDevice(activeDevice.deviceId));
};
}
const notification = showNotification({
titleKey: 'toolbar.noAudioSignalTitle',
descriptionKey,
customActionNameKey,
customActionHandler
});
dispatch(notification);
dispatch(playSound(NO_AUDIO_SIGNAL_SOUND_ID));
// Store the current notification uid so we can check for this state and hide it in case
// a new track was added, thus changing the context of the notification
dispatch(setNoAudioSignalNotificationUid(notification.uid));
});
}

View File

@@ -0,0 +1,17 @@
// @flow
import { ReducerRegistry, set } from '../base/redux';
import { SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID } from './actionTypes';
/**
* Reduces the redux actions of the feature no audio signal
*/
ReducerRegistry.register('features/no-audio-signal', (state = {}, action) => {
switch (action.type) {
case SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID:
return set(state, 'noAudioSignalNotificationUid', action.uid);
}
return state;
});

View File

@@ -0,0 +1,6 @@
/**
* The file used for the no audio signal sound notification.
*
* @type {string}
*/
export const NO_AUDIO_SIGNAL_SOUND_FILE = 'noAudioSignal.mp3';

View File

@@ -1,11 +1,8 @@
// @flow
import React from 'react';
import { translate } from '../../../../base/i18n';
import { IconLiveStreaming } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import { BetaTag } from '../../../../base/toolbox';
import AbstractLiveStreamButton, {
_mapStateToProps as _abstractMapStateToProps,
@@ -37,18 +34,6 @@ type Props = AbstractProps & {
class LiveStreamButton extends AbstractLiveStreamButton<Props> {
icon = IconLiveStreaming;
/**
* 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 />;
}
/**
* Returns the tooltip that should be displayed when the button is disabled.
*

View File

@@ -80,7 +80,7 @@ class RemoteVideoMenu extends Component<Props> {
afterClick: this._onCancel,
showLabel: true,
participantID: participant.id,
styles: this.props._bottomSheetStyles
styles: this.props._bottomSheetStyles.buttons
};
return (

View File

@@ -25,20 +25,3 @@ export function createRnnoiseProcessorPromise() {
throw new Error('Rnnoise module binding createRnnoiseProcessor not found!');
});
}
/**
* Get the accepted sample length for the rnnoise library. We might want to expose it with flow libdefs.
*
* @returns {number}
*/
export function getSampleLength() {
const ns = getJitsiMeetGlobalNS();
const rnnoiseSample = ns?.effects?.rnnoise?.RNNOISE_SAMPLE_LENGTH;
if (!rnnoiseSample) {
throw new Error('Please call createRnnoiseProcessorPromise first or wait for promise to resolve!');
}
return rnnoiseSample;
}

View File

@@ -2,6 +2,7 @@
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
LOCK_STATE_CHANGED,
SET_PASSWORD_FAILED
} from '../base/conference';
@@ -33,6 +34,9 @@ MiddlewareRegistry.register(store => next => action => {
case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case LOCK_STATE_CHANGED: {
// TODO Remove this logic when all components interested in the lock
// state change event are moved into react/redux.
@@ -67,6 +71,25 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Handles cleanup of lock prompt state when a conference is joined.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which
* specifies the details associated with joining the conference.
* @private
* @returns {*}
*/
function _conferenceJoined({ dispatch }, next, action) {
dispatch(hideDialog(PasswordRequiredPrompt));
dispatch(hideDialog(RoomLockPrompt));
return next(action);
}
/**
* Handles errors that occur when a conference fails.
*

View File

@@ -0,0 +1,161 @@
// @flow
import {
CLEAR_INTERVAL,
INTERVAL_TIMEOUT,
SET_INTERVAL,
timerWorkerScript
} from './TimeWorker';
/**
* Represents a modified MediaStream that adds video as pip on a desktop stream.
* <tt>JitsiStreamPresenterEffect</tt> does the processing of the original
* desktop stream.
*/
export default class JitsiStreamPresenterEffect {
_canvas: HTMLCanvasElement;
_ctx: CanvasRenderingContext2D;
_desktopElement: HTMLVideoElement;
_desktopStream: MediaStream;
_frameRate: number;
_onVideoFrameTimer: Function;
_onVideoFrameTimerWorker: Function;
_renderVideo: Function;
_videoFrameTimerWorker: Worker;
_videoElement: HTMLVideoElement;
isEnabled: Function;
startEffect: Function;
stopEffect: Function;
/**
* Represents a modified MediaStream that adds a camera track at the
* bottom right corner of the desktop track using a HTML canvas.
* <tt>JitsiStreamPresenterEffect</tt> does the processing of the original
* video stream.
*
* @param {MediaStream} videoStream - The video stream which is user for
* creating the canvas.
*/
constructor(videoStream: MediaStream) {
const videoDiv = document.createElement('div');
const firstVideoTrack = videoStream.getVideoTracks()[0];
const { height, width, frameRate } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
this._canvas = document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
if (document.body !== null) {
document.body.appendChild(this._canvas);
}
this._desktopElement = document.createElement('video');
this._videoElement = document.createElement('video');
videoDiv.appendChild(this._videoElement);
videoDiv.appendChild(this._desktopElement);
if (document.body !== null) {
document.body.appendChild(videoDiv);
}
// Set the video element properties
this._frameRate = parseInt(frameRate, 10);
this._videoElement.width = parseInt(width, 10);
this._videoElement.height = parseInt(height, 10);
this._videoElement.autoplay = true;
this._videoElement.srcObject = videoStream;
// set the style attribute of the div to make it invisible
videoDiv.style.display = 'none';
// Bind event handler so it is only bound once for every instance.
this._onVideoFrameTimer = this._onVideoFrameTimer.bind(this);
this._videoFrameTimerWorker = new Worker(timerWorkerScript);
this._videoFrameTimerWorker.onmessage = this._onVideoFrameTimer;
}
/**
* EventHandler onmessage for the videoFrameTimerWorker WebWorker.
*
* @private
* @param {EventHandler} response - The onmessage EventHandler parameter.
* @returns {void}
*/
_onVideoFrameTimer(response) {
if (response.data.id === INTERVAL_TIMEOUT) {
this._renderVideo();
}
}
/**
* Loop function to render the video frame input and draw presenter effect.
*
* @private
* @returns {void}
*/
_renderVideo() {
// adjust the canvas width/height on every frame incase the window has been resized.
const [ track ] = this._desktopStream.getVideoTracks();
const { height, width } = track.getSettings() ?? track.getConstraints();
this._canvas.width = parseInt(width, 10);
this._canvas.height = parseInt(height, 10);
this._ctx.drawImage(this._desktopElement, 0, 0, this._canvas.width, this._canvas.height);
this._ctx.drawImage(this._videoElement, this._canvas.width - this._videoElement.width, this._canvas.height
- this._videoElement.height, this._videoElement.width, this._videoElement.height);
// draw a border around the video element.
this._ctx.beginPath();
this._ctx.lineWidth = 2;
this._ctx.strokeStyle = '#A9A9A9'; // dark grey
this._ctx.rect(this._canvas.width - this._videoElement.width, this._canvas.height - this._videoElement.height,
this._videoElement.width, this._videoElement.height);
this._ctx.stroke();
}
/**
* Checks if the local track supports this effect.
*
* @param {JitsiLocalTrack} jitsiLocalTrack - Track to apply effect.
* @returns {boolean} - Returns true if this effect can run on the
* specified track, false otherwise.
*/
isEnabled(jitsiLocalTrack: Object) {
return jitsiLocalTrack.isVideoTrack() && jitsiLocalTrack.videoType === 'desktop';
}
/**
* Starts loop to capture video frame and render presenter effect.
*
* @param {MediaStream} desktopStream - Stream to be used for processing.
* @returns {MediaStream} - The stream with the applied effect.
*/
startEffect(desktopStream: MediaStream) {
const firstVideoTrack = desktopStream.getVideoTracks()[0];
const { height, width } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
// set the desktop element properties.
this._desktopStream = desktopStream;
this._desktopElement.width = parseInt(width, 10);
this._desktopElement.height = parseInt(height, 10);
this._desktopElement.autoplay = true;
this._desktopElement.srcObject = desktopStream;
this._canvas.width = parseInt(width, 10);
this._canvas.height = parseInt(height, 10);
this._videoFrameTimerWorker.postMessage({
id: SET_INTERVAL,
timeMs: 1000 / this._frameRate
});
return this._canvas.captureStream(this._frameRate);
}
/**
* Stops the capture and render loop.
*
* @returns {void}
*/
stopEffect() {
this._videoFrameTimerWorker.postMessage({
id: CLEAR_INTERVAL
});
}
}

View File

@@ -0,0 +1,62 @@
// @flow
/**
* SET_INTERVAL constant is used to set interval and it is set in
* the id property of the request.data property. timeMs property must
* also be set. request.data example:
*
* {
* id: SET_INTERVAL,
* timeMs: 33
* }
*/
export const SET_INTERVAL = 1;
/**
* CLEAR_INTERVAL constant is used to clear the interval and it is set in
* the id property of the request.data property.
*
* {
* id: CLEAR_INTERVAL
* }
*/
export const CLEAR_INTERVAL = 2;
/**
* INTERVAL_TIMEOUT constant is used as response and it is set in the id
* property.
*
* {
* id: INTERVAL_TIMEOUT
* }
*/
export const INTERVAL_TIMEOUT = 3;
/**
* The following code is needed as string to create a URL from a Blob.
* The URL is then passed to a WebWorker. Reason for this is to enable
* use of setInterval that is not throttled when tab is inactive.
*/
const code = `
var timer;
onmessage = function(request) {
switch (request.data.id) {
case ${SET_INTERVAL}: {
timer = setInterval(() => {
postMessage({ id: ${INTERVAL_TIMEOUT} });
}, request.data.timeMs);
break;
}
case ${CLEAR_INTERVAL}: {
if (timer) {
clearInterval(timer);
}
break;
}
}
};
`;
export const timerWorkerScript
= URL.createObjectURL(new Blob([ code ], { type: 'application/javascript' }));

View File

@@ -0,0 +1,19 @@
// @flow
import JitsiStreamPresenterEffect from './JitsiStreamPresenterEffect';
/**
* Creates a new instance of JitsiStreamPresenterEffect.
*
* @param {MediaStream} stream - The video stream which will be used for
* creating the presenter effect.
* @returns {Promise<JitsiStreamPresenterEffect>}
*/
export function createPresenterEffect(stream: MediaStream) {
if (!MediaStreamTrack.prototype.getSettings
&& !MediaStreamTrack.prototype.getConstraints) {
return Promise.reject(new Error('JitsiStreamPresenterEffect not supported!'));
}
return Promise.resolve(new JitsiStreamPresenterEffect(stream));
}

View File

@@ -10,6 +10,11 @@ export const RNNOISE_SAMPLE_LENGTH: number = 480;
*/
const RNNOISE_BUFFER_SIZE: number = RNNOISE_SAMPLE_LENGTH * 4;
/**
* Constant. Rnnoise only takes operates on 44.1Khz float 32 little endian PCM.
*/
const PCM_FREQUENCY: number = 44100;
/**
* Represents an adaptor for the rnnoise library compiled to webassembly. The class takes care of webassembly
* memory management and exposes rnnoise functionality such as PCM audio denoising and VAD (voice activity
@@ -131,6 +136,24 @@ export default class RnnoiseProcessor {
}
}
/**
* Rnnoise can only operate on a certain PCM array size.
*
* @returns {number} - The PCM sample array size as required by rnnoise.
*/
getSampleLength() {
return RNNOISE_SAMPLE_LENGTH;
}
/**
* Rnnoise can only operate on a certain format of PCM sample namely float 32 44.1Kz.
*
* @returns {number} - PCM sample frequency as required by rnnoise.
*/
getRequiredPCMFrequency() {
return PCM_FREQUENCY;
}
/**
* Release any resources required by the rnnoise context this needs to be called
* before destroying any context that uses the processor.

View File

@@ -10,14 +10,13 @@ import {
import { setAudioOnly } from '../../base/audio-only';
import { translate } from '../../base/i18n';
import {
MEDIA_TYPE,
VIDEO_MUTISM_AUTHORITY,
setVideoMuted
} from '../../base/media';
import { connect } from '../../base/redux';
import { AbstractVideoMuteButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox';
import { isLocalTrackMuted } from '../../base/tracks';
import { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks';
import UIEvents from '../../../../service/UI/UIEvents';
declare var APP: Object;
@@ -32,6 +31,11 @@ type Props = AbstractButtonProps & {
*/
_audioOnly: boolean,
/**
* MEDIA_TYPE of the local video.
*/
_videoMediaType: string,
/**
* Whether video is currently muted or not.
*/
@@ -136,10 +140,12 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
this.props.dispatch(
setAudioOnly(false, /* ensureTrack */ true));
}
const mediaType = this.props._videoMediaType;
this.props.dispatch(
setVideoMuted(
videoMuted,
mediaType,
VIDEO_MUTISM_AUTHORITY.USER,
/* ensureTrack */ true));
@@ -167,7 +173,8 @@ function _mapStateToProps(state): Object {
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
_videoMediaType: getLocalVideoType(tracks),
_videoMuted: isLocalVideoTrackMuted(tracks)
};
}

View File

@@ -1,10 +1,12 @@
// @flow
import React, { Component } from 'react';
import { Platform } from 'react-native';
import React, { PureComponent } from 'react';
import { Platform, TouchableOpacity, View } from 'react-native';
import Collapsible from 'react-native-collapsible';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
import { IconDragHandle } from '../../../base/icons';
import { CHAT_ENABLED, IOS_RECORDING_ENABLED, getFeatureFlag } from '../../../base/flags';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
@@ -16,10 +18,12 @@ import { RoomLockButton } from '../../../room-lock';
import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout';
import AudioOnlyButton from './AudioOnlyButton';
import HelpButton from '../HelpButton';
import AudioOnlyButton from './AudioOnlyButton';
import RaiseHandButton from './RaiseHandButton';
import ToggleCameraButton from './ToggleCameraButton';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link OverflowMenu}.
@@ -52,6 +56,19 @@ type Props = {
dispatch: Function
};
type State = {
/**
* True if the bottom scheet is scrolled to the top.
*/
scrolledToTop: boolean,
/**
* True if the 'more' button set needas to be rendered.
*/
showMore: boolean
}
/**
* The exported React {@code Component}. We need it to execute
* {@link hideDialog}.
@@ -65,7 +82,7 @@ let OverflowMenu_; // eslint-disable-line prefer-const
* Implements a React {@code Component} with some extra actions in addition to
* those in the toolbar.
*/
class OverflowMenu extends Component<Props> {
class OverflowMenu extends PureComponent<Props, State> {
/**
* Initializes a new {@code OverflowMenu} instance.
*
@@ -74,8 +91,16 @@ class OverflowMenu extends Component<Props> {
constructor(props: Props) {
super(props);
this.state = {
scrolledToTop: true,
showMore: false
};
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._onSwipe = this._onSwipe.bind(this);
this._onToggleMenu = this._onToggleMenu.bind(this);
this._renderMenuExpandToggle = this._renderMenuExpandToggle.bind(this);
}
/**
@@ -85,37 +110,67 @@ class OverflowMenu extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _bottomSheetStyles } = this.props;
const { showMore } = this.state;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
styles: this.props._bottomSheetStyles
styles: _bottomSheetStyles.buttons
};
return (
<BottomSheet onCancel = { this._onCancel }>
<BottomSheet
onCancel = { this._onCancel }
onSwipe = { this._onSwipe }
renderHeader = { this._renderMenuExpandToggle }>
<AudioRouteButton { ...buttonProps } />
<ToggleCameraButton { ...buttonProps } />
<AudioOnlyButton { ...buttonProps } />
<RoomLockButton { ...buttonProps } />
<ClosedCaptionButton { ...buttonProps } />
{
this.props._recordingEnabled
&& <RecordButton { ...buttonProps } />
}
<LiveStreamButton { ...buttonProps } />
<TileViewButton { ...buttonProps } />
<InviteButton { ...buttonProps } />
{
this.props._chatEnabled
&& <InfoDialogButton { ...buttonProps } />
}
<RaiseHandButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<HelpButton { ...buttonProps } />
<Collapsible collapsed = { !showMore }>
<RoomLockButton { ...buttonProps } />
<ClosedCaptionButton { ...buttonProps } />
{
this.props._recordingEnabled
&& <RecordButton { ...buttonProps } />
}
<LiveStreamButton { ...buttonProps } />
<TileViewButton { ...buttonProps } />
<InviteButton { ...buttonProps } />
{
this.props._chatEnabled
&& <InfoDialogButton { ...buttonProps } />
}
<RaiseHandButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<HelpButton { ...buttonProps } />
</Collapsible>
</BottomSheet>
);
}
_renderMenuExpandToggle: () => React$Element<any>;
/**
* Function to render the menu toggle in the bottom sheet header area.
*
* @returns {React$Element}
*/
_renderMenuExpandToggle() {
return (
<View
style = { [
this.props._bottomSheetStyles.sheet,
styles.expandMenuContainer
] }>
<TouchableOpacity onPress = { this._onToggleMenu }>
{ /* $FlowFixMeProps */ }
<IconDragHandle style = { this.props._bottomSheetStyles.expandIcon } />
</TouchableOpacity>
</View>
);
}
_onCancel: () => boolean;
/**
@@ -133,6 +188,47 @@ class OverflowMenu extends Component<Props> {
return false;
}
_onSwipe: string => void;
/**
* Callback to be invoked when swipe gesture is detected on the menu. Returns true
* if the swipe gesture is handled by the menu, false otherwise.
*
* @param {string} direction - Direction of 'up' or 'down'.
* @returns {boolean}
*/
_onSwipe(direction) {
const { showMore } = this.state;
switch (direction) {
case 'up':
!showMore && this.setState({
showMore: true
});
return !showMore;
case 'down':
showMore && this.setState({
showMore: false
});
return showMore;
}
}
_onToggleMenu: () => void;
/**
* Callback to be invoked when the expand menu button is pressed.
*
* @returns {void}
*/
_onToggleMenu() {
this.setState({
showMore: !this.state.showMore
});
}
}
/**

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