Compare commits

..

2 Commits

Author SHA1 Message Date
Hristo Terezov
ffeb88677a fix(thumbnails): es6 support & cleanup. 2019-12-13 14:20:25 +00:00
Hristo Terezov
b2ddf55d44 feat(load-test): Initial implementation. 2019-11-28 13:20:49 +00:00
76 changed files with 9650 additions and 2734 deletions

View File

@@ -29,18 +29,11 @@ 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,9 +69,7 @@ repositories {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-5'
implementation 'androidx.appcompat:appcompat:1.0.2'
if (!rootProject.ext.libreBuild) {
implementation 'com.google.android.gms:play-services-auth:16.0.1'
@@ -85,6 +83,9 @@ 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,6 +5,7 @@
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

@@ -0,0 +1,36 @@
/*
* 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.3.3'
classpath 'io.fabric.tools:gradle:1.28.1'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.27.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files.
@@ -165,6 +165,50 @@ 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,12 +37,10 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.fragment:fragment:1.1.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.fragment:fragment:1.0.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_SPEAKER;
return CallAudioState.ROUTE_EARPIECE;
}
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_SPEAKER;
return CallAudioState.ROUTE_EARPIECE;
}
}

View File

@@ -123,17 +123,14 @@ 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 boolean setConnectionActive(String callUUID) {
static void setConnectionActive(String callUUID) {
ConnectionImpl connection = connections.get(callUUID);
if (connection != null) {
connection.setActive();
return true;
} else {
JitsiMeetLogger.w("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
return false;
JitsiMeetLogger.e("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
}
}

View File

@@ -105,22 +105,15 @@ class RNConnectionService extends ReactContextBaseJavaModule {
ConnectionService.registerStartCallPromise(callUUID, promise);
TelecomManager tm = null;
try {
tm = (TelecomManager) ctx.getSystemService(Context.TELECOM_SERVICE);
TelecomManager 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);
if (e instanceof SecurityException) {
promise.reject("SECURITY_ERROR", "Required permissions not granted.");
} else {
promise.reject(e);
}
promise.reject(e);
}
}
@@ -158,11 +151,8 @@ class RNConnectionService extends ReactContextBaseJavaModule {
@ReactMethod
public void reportConnectedOutgoingCall(String callUUID, Promise promise) {
JitsiMeetLogger.d(TAG + " reportConnectedOutgoingCall " + callUUID);
if (ConnectionService.setConnectionActive(callUUID)) {
promise.resolve(null);
} else {
promise.reject("CONNECTION_NOT_FOUND_ERROR", "Connection wasn't found.");
}
ConnectionService.setConnectionActive(callUUID);
promise.resolve(null);
}
@Override

View File

@@ -93,15 +93,10 @@ 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,
@@ -118,7 +113,6 @@ 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';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -443,11 +437,6 @@ 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
@@ -733,8 +722,9 @@ export default {
isLocalVideoMuted() {
// If the tracks are not ready, read from base/media state
return this._localTracksInitialized
? isLocalVideoTrackMuted(
APP.store.getState()['features/base/tracks'])
? isLocalTrackMuted(
APP.store.getState()['features/base/tracks'],
MEDIA_TYPE.VIDEO)
: isVideoMutedByUser(APP.store);
},
@@ -808,51 +798,6 @@ 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 mutePresenterVideo(mute, showUI = true) {
const maybeShowErrorDialog = error => {
showUI && APP.store.dispatch(notifyCameraError(error));
};
if (mute) {
try {
await this.localVideo.setEffect(undefined);
APP.store.dispatch(
setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
} catch (err) {
logger.error('Failed to mute the Presenter video');
}
return;
}
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);
}
},
/**
* Simulates toolbar button click for video mute. Used by shortcuts and API.
* @param mute true for mute and false for unmute.
@@ -867,10 +812,6 @@ 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
@@ -1410,7 +1351,7 @@ export default {
* in case it fails.
* @private
*/
_turnScreenSharingOff(didHaveVideo) {
_turnScreenSharingOff(didHaveVideo, wasVideoMuted) {
this._untoggleScreenSharing = null;
this.videoSwitchInProgress = true;
const { receiver } = APP.remoteControl;
@@ -1428,7 +1369,13 @@ export default {
.then(([ stream ]) => this.useVideoStream(stream))
.then(() => {
sendAnalytics(createScreenSharingEvent('stopped'));
logger.log('Screen sharing 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();
}
})
.catch(error => {
logger.error('failed to switch back to local video', error);
@@ -1443,16 +1390,6 @@ 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;
@@ -1478,7 +1415,7 @@ export default {
* 'window', etc.).
* @return {Promise.<T>}
*/
async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
if (this.videoSwitchInProgress) {
return Promise.reject('Switch in progress.');
}
@@ -1492,15 +1429,7 @@ export default {
}
if (toggle) {
try {
await this._switchToScreenSharing(options);
return;
} catch (err) {
logger.error('Failed to switch to screensharing', err);
return;
}
return this._switchToScreenSharing(options);
}
return this._untoggleScreenSharing
@@ -1525,7 +1454,8 @@ export default {
_createDesktopTrack(options = {}) {
let externalInstallation = false;
let DSExternalInstallationInProgress = false;
const didHaveVideo = !this.isLocalVideoMuted();
const didHaveVideo = Boolean(this.localVideo);
const wasVideoMuted = this.isLocalVideoMuted();
const getDesktopStreamPromise = options.desktopStream
? Promise.resolve([ options.desktopStream ])
@@ -1576,7 +1506,8 @@ 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);
= this._turnScreenSharingOff
.bind(this, didHaveVideo, wasVideoMuted);
desktopStream.on(
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => {
@@ -1601,45 +1532,6 @@ 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, cameraDeviceId = null) {
let presenterTrack;
try {
presenterTrack = await createLocalPresenterTrack({
cameraDeviceId
},
height);
} catch (err) {
logger.error('Failed to create a camera track for presenter', err);
return;
}
this.localPresenterVideo = presenterTrack;
try {
const effect = await createPresenterEffect(presenterTrack.stream);
APP.store.dispatch(trackAdded(this.localPresenterVideo));
return effect;
} catch (err) {
logger.error('Failed to create the presenter effect', err);
APP.store.dispatch(
setVideoMuted(true, MEDIA_TYPE.PRESENTER));
APP.store.dispatch(notifyCameraError(err));
}
},
/**
* Tries to switch to the screensharing mode by disposing camera stream and
* replacing it with a desktop one.
@@ -2100,56 +1992,36 @@ 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);
}
// 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 stream;
})
.then(stream => {
// if we are screen sharing we do not want to stop it
if (this.isSharingScreen) {
return Promise.resolve();
}
// dispose the existing presenter track and create a new
// camera track.
APP.store.dispatch(setVideoMuted(true, MEDIA_TYPE.PRESENTER));
return this._createPresenterStreamEffect(height, cameraDeviceId)
.then(effect => this.localVideo.setEffect(effect))
.then(() => {
muteLocalVideo(false);
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.
} else if (this.isSharingScreen && videoWasMuted) {
return this.useVideoStream(stream);
})
.then(() => {
logger.log('switched local video device');
this._updateVideoDeviceId();
// 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)));
}
})
.catch(err => {
APP.store.dispatch(notifyCameraError(err));
});
}
);

1
debian/control vendored
View File

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

@@ -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/ -- "token_verification"/ "token_verification"/g' $PROSODY_HOST_CONFIG
sed -i 's/ modules_enabled = { "token_verification" }/ --modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
if [ -x "/etc/init.d/prosody" ]; then
invoke-rc.d prosody restart

View File

@@ -1,4 +1,5 @@
plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
-- 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/" }
VirtualHost "jitmeet.example.com"
-- enabled = false -- Remove this line to enable this host
@@ -26,10 +27,7 @@ VirtualHost "jitmeet.example.com"
Component "conference.jitmeet.example.com" "muc"
storage = "null"
modules_enabled = {
"muc_meeting_id";
-- "token_verification";
}
--modules_enabled = { "token_verification" }
admins = { "focusUser@auth.jitmeet.example.com" }
Component "jitsi-videobridge.jitmeet.example.com"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

BIN
images/avatarprezi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
images/chrome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
images/chromium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

42
images/dropboxLogo.svg Normal file
View File

@@ -0,0 +1,42 @@
<?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>

After

Width:  |  Height:  |  Size: 3.6 KiB

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

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/estoslogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
images/firefox-nightly.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
images/firefox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/ie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

182
images/logo-blue.svg Normal file
View File

@@ -0,0 +1,182 @@
<?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>

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/opera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
images/popupPointer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/safari.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

2
images/spin.svg Normal file
View File

@@ -0,0 +1,2 @@
<?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>

After

Width:  |  Height:  |  Size: 3.0 KiB

64
images/videomask.svg Normal file
View File

@@ -0,0 +1,64 @@
<?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>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

@@ -699,10 +699,6 @@ UI.showExtensionInlineInstallationDialog = function(callback) {
});
};
UI.updateDevicesAvailability = function(id, devices) {
VideoLayout.setDeviceAvailabilityIcons(id, devices);
};
/**
* Show shared video.
* @param {string} id the id of the sender of the command

View File

@@ -7,77 +7,83 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
*
*/
export default function SharedVideoThumb(participant, videoType, VideoLayout) {
this.id = participant.id;
export default class SharedVideoThumb extends SmallVideo {
/**
*
* @param {*} participant
* @param {*} videoType
* @param {*} VideoLayout
*/
constructor(participant, videoType, VideoLayout) {
super(VideoLayout);
this.id = participant.id;
this.url = participant.id;
this.setVideoType(videoType);
this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container);
this.url = participant.id;
this.setVideoType(videoType);
this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container);
this.bindHoverHandler();
SmallVideo.call(this, VideoLayout);
this.isVideoMuted = true;
this.updateDisplayName();
this.bindHoverHandler();
this.isVideoMuted = true;
this.updateDisplayName();
this.container.onclick = this._onContainerClick;
}
SharedVideoThumb.prototype = Object.create(SmallVideo.prototype);
SharedVideoThumb.prototype.constructor = SharedVideoThumb;
/**
* hide display name
*/
// eslint-disable-next-line no-empty-function
SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function() {};
// eslint-disable-next-line no-empty-function
SharedVideoThumb.prototype.initializeAvatar = function() {};
SharedVideoThumb.prototype.createContainer = function(spanId) {
const container = document.createElement('span');
container.id = spanId;
container.className = 'videocontainer';
// add the avatar
const avatar = document.createElement('img');
avatar.className = 'sharedVideoAvatar';
avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
container.appendChild(avatar);
const displayNameContainer = document.createElement('div');
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return container;
};
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
SharedVideoThumb.prototype.updateDisplayName = function() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
this.container.onclick = this._onContainerClick;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
};
/**
*
*/
initializeAvatar() {} // eslint-disable-line no-empty-function
/**
*
* @param {*} spanId
*/
createContainer(spanId) {
const container = document.createElement('span');
container.id = spanId;
container.className = 'videocontainer';
// add the avatar
const avatar = document.createElement('img');
avatar.className = 'sharedVideoAvatar';
avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
container.appendChild(avatar);
const displayNameContainer = document.createElement('div');
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return container;
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
}
}

View File

@@ -64,11 +64,9 @@ const Filmstrip = {
return this._calculateThumbnailSizeForTileView();
}
const availableSizes = this.calculateAvailableSize();
const width = availableSizes.availableWidth;
const height = availableSizes.availableHeight;
const { availableWidth, availableHeight } = this.calculateAvailableSize();
return this.calculateThumbnailSizeFromAvailable(width, height);
return this.calculateThumbnailSizeFromAvailable(availableWidth, availableHeight);
},
/**
@@ -80,8 +78,7 @@ const Filmstrip = {
calculateAvailableSize() {
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const isHorizontalFilmstripView
= currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
const isHorizontalFilmstripView = currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
/**
* If the videoAreaAvailableWidth is set we use this one to calculate
@@ -100,7 +97,6 @@ const Filmstrip = {
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
let availableWidth = videoAreaAvailableWidth;
const thumbs = this.getThumbs(true);
// If local thumb is not hidden
@@ -149,15 +145,11 @@ const Filmstrip = {
);
}
const maxHeight
// If the MAX_HEIGHT property hasn't been specified
// we have the static value.
= Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
availableHeight);
const maxHeight = Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120, availableHeight);
availableHeight
= Math.min(maxHeight, window.innerHeight - 18);
availableHeight = Math.min(maxHeight, window.innerHeight - 18);
return {
availableHeight,
@@ -239,13 +231,10 @@ const Filmstrip = {
* availableHeight/h > availableWidth/totalWidth otherwise 2) is true
*/
const remoteThumbsInRow = interfaceConfig.VERTICAL_FILMSTRIP
? 0 : this.getThumbs(true).remoteThumbs.length;
const remoteLocalWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO
/ interfaceConfig.LOCAL_THUMBNAIL_RATIO;
const lW = Math.min(availableWidth
/ ((remoteLocalWidthRatio * remoteThumbsInRow) + 1), availableHeight
* interfaceConfig.LOCAL_THUMBNAIL_RATIO);
const remoteThumbsInRow = interfaceConfig.VERTICAL_FILMSTRIP ? 0 : this.getThumbs(true).remoteThumbs.length;
const remoteLocalWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
const lW = Math.min(availableWidth / ((remoteLocalWidthRatio * remoteThumbsInRow) + 1),
availableHeight * interfaceConfig.LOCAL_THUMBNAIL_RATIO);
const h = lW / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
const remoteVideoWidth = lW * remoteLocalWidthRatio;
@@ -333,18 +322,12 @@ const Filmstrip = {
if (shouldDisplayTileView(state)) {
// The size of the side margins for each tile as set in CSS.
const sideMargins = 10 * 2;
const {
columns,
rows
} = getTileViewGridDimensions(state, getMaxColumnCount());
const { columns, rows } = getTileViewGridDimensions(state, getMaxColumnCount());
const hasOverflow = rows > columns;
// Width is set so that the flex layout can automatically wrap
// tiles onto new rows.
this.filmstripRemoteVideos.css({
width: (local.thumbWidth * columns) + (columns * sideMargins)
});
this.filmstripRemoteVideos.css({ width: (local.thumbWidth * columns) + (columns * sideMargins) });
this.filmstripRemoteVideos.toggleClass('has-overflow', hasOverflow);
} else {
this.filmstripRemoteVideos.css('width', '');

View File

@@ -20,260 +20,264 @@ import SmallVideo from './SmallVideo';
/**
*
*/
function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
this.videoSpanId = 'localVideoContainer';
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
this.$container = $(this.container);
this.updateDOMLocation();
export default class LocalVideo extends SmallVideo {
/**
*
* @param {*} VideoLayout
* @param {*} emitter
* @param {*} streamEndedCallback
*/
constructor(VideoLayout, emitter, streamEndedCallback) {
super(VideoLayout);
this.videoSpanId = 'localVideoContainer';
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
this.$container = $(this.container);
this.updateDOMLocation();
this.localVideoId = null;
this.bindHoverHandler();
if (!config.disableLocalVideoFlip) {
this._buildContextMenu();
}
this.isLocal = true;
this.emitter = emitter;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
? 'left top' : 'top center';
Object.defineProperty(this, 'id', {
get() {
return APP.conference.getMyUserId();
this.localVideoId = null;
this.bindHoverHandler();
if (!config.disableLocalVideoFlip) {
this._buildContextMenu();
}
});
this.initBrowserSpecificProperties();
this.isLocal = true;
this.emitter = emitter;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
? 'left top' : 'top center';
SmallVideo.call(this, VideoLayout);
Object.defineProperty(this, 'id', {
get() {
return APP.conference.getMyUserId();
}
});
this.initBrowserSpecificProperties();
// Set default display name.
this.updateDisplayName();
// Set default display name.
this.updateDisplayName();
// Initialize the avatar display with an avatar url selected from the redux
// state. Redux stores the local user with a hardcoded participant id of
// 'local' if no id has been assigned yet.
this.initializeAvatar();
// Initialize the avatar display with an avatar url selected from the redux
// state. Redux stores the local user with a hardcoded participant id of
// 'local' if no id has been assigned yet.
this.initializeAvatar();
this.addAudioLevelIndicator();
this.updateIndicators();
this.addAudioLevelIndicator();
this.updateIndicators();
this.container.onclick = this._onContainerClick;
}
LocalVideo.prototype = Object.create(SmallVideo.prototype);
LocalVideo.prototype.constructor = LocalVideo;
LocalVideo.prototype.createContainer = function() {
const containerSpan = document.createElement('span');
containerSpan.classList.add('videocontainer');
containerSpan.id = this.videoSpanId;
containerSpan.innerHTML = `
<div class = 'videocontainer__background'></div>
<span id = 'localVideoWrapper'></span>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>`;
return containerSpan;
};
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
LocalVideo.prototype.updateDisplayName = function() {
if (!this.container) {
logger.warn(
`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
this.container.onclick = this._onContainerClick;
}
this._renderDisplayName({
allowEditing: APP.store.getState()['features/base/jwt'].isGuest,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id
});
};
/**
*
*/
createContainer() {
const containerSpan = document.createElement('span');
LocalVideo.prototype.changeVideo = function(stream) {
this.videoStream = stream;
containerSpan.classList.add('videocontainer');
containerSpan.id = this.videoSpanId;
this.localVideoId = `localVideo_${stream.getId()}`;
containerSpan.innerHTML = `
<div class = 'videocontainer__background'></div>
<span id = 'localVideoWrapper'></span>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>`;
this._updateVideoElement();
return containerSpan;
}
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
const settings = APP.store.getState()['features/base/settings'];
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
updateDisplayName() {
if (!this.container) {
logger.warn(
`Unable to set displayName - ${this.videoSpanId
} does not exist`);
this._enableDisableContextMenu(isVideo);
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
const localVideoContainer
= document.getElementById('localVideoWrapper');
// Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (localVideoContainer && this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
return;
}
this._notifyOfStreamEnded();
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
this._renderDisplayName({
allowEditing: APP.store.getState()['features/base/jwt'].isGuest,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id
});
}
/**
*
* @param {*} stream
*/
changeVideo(stream) {
this.videoStream = stream;
this.localVideoId = `localVideo_${stream.getId()}`;
this._updateVideoElement();
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
const settings = APP.store.getState()['features/base/settings'];
this._enableDisableContextMenu(isVideo);
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
const localVideoContainer
= document.getElementById('localVideoWrapper');
// Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (localVideoContainer && this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
}
this._notifyOfStreamEnded();
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
}
/**
* Notify any subscribers of the local video stream ending.
*
* @private
* @returns {void}
*/
_notifyOfStreamEnded() {
if (this.streamEndedCallback) {
this.streamEndedCallback(this.id);
}
}
/**
* Shows or hides the local video container.
* @param {boolean} true to make the local video container visible, false
* otherwise
*/
setVisible(visible) {
// We toggle the hidden class as an indication to other interested parties
// that this container has been hidden on purpose.
this.$container.toggleClass('hidden');
// We still show/hide it as we need to overwrite the style property if we
// want our action to take effect. Toggling the display property through
// the above css class didn't succeed in overwriting the style.
if (visible) {
this.$container.show();
} else {
this.$container.hide();
}
};
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
/**
* Notify any subscribers of the local video stream ending.
*
* @private
* @returns {void}
*/
LocalVideo.prototype._notifyOfStreamEnded = function() {
if (this.streamEndedCallback) {
this.streamEndedCallback(this.id);
/**
* Sets the flipX state of the video.
* @param val {boolean} true for flipped otherwise false;
*/
setFlipX(val) {
this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
if (!this.localVideoId) {
return;
}
if (val) {
this.selectVideoElement().addClass('flipVideoX');
} else {
this.selectVideoElement().removeClass('flipVideoX');
}
}
};
/**
* Shows or hides the local video container.
* @param {boolean} true to make the local video container visible, false
* otherwise
*/
LocalVideo.prototype.setVisible = function(visible) {
/**
* Builds the context menu for the local video.
*/
_buildContextMenu() {
$.contextMenu({
selector: `#${this.videoSpanId}`,
zIndex: 10000,
items: {
flip: {
name: 'Flip',
callback: () => {
const { store } = APP;
const val = !store.getState()['features/base/settings']
.localFlipX;
// We toggle the hidden class as an indication to other interested parties
// that this container has been hidden on purpose.
this.$container.toggleClass('hidden');
// We still show/hide it as we need to overwrite the style property if we
// want our action to take effect. Toggling the display property through
// the above css class didn't succeed in overwriting the style.
if (visible) {
this.$container.show();
} else {
this.$container.hide();
}
};
/**
* Sets the flipX state of the video.
* @param val {boolean} true for flipped otherwise false;
*/
LocalVideo.prototype.setFlipX = function(val) {
this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
if (!this.localVideoId) {
return;
}
if (val) {
this.selectVideoElement().addClass('flipVideoX');
} else {
this.selectVideoElement().removeClass('flipVideoX');
}
};
/**
* Builds the context menu for the local video.
*/
LocalVideo.prototype._buildContextMenu = function() {
$.contextMenu({
selector: `#${this.videoSpanId}`,
zIndex: 10000,
items: {
flip: {
name: 'Flip',
callback: () => {
const { store } = APP;
const val = !store.getState()['features/base/settings']
.localFlipX;
this.setFlipX(val);
store.dispatch(updateSettings({
localFlipX: val
}));
this.setFlipX(val);
store.dispatch(updateSettings({
localFlipX: val
}));
}
}
},
events: {
show(options) {
options.items.flip.name
= APP.translation.generateTranslationHTML(
'videothumbnail.flip');
}
}
},
events: {
show(options) {
options.items.flip.name
= APP.translation.generateTranslationHTML(
'videothumbnail.flip');
}
});
}
/**
* Enables or disables the context menu for the local video.
* @param enable {boolean} true for enable, false for disable
*/
_enableDisableContextMenu(enable) {
if (this.$container.contextMenu) {
this.$container.contextMenu(enable);
}
});
};
/**
* Enables or disables the context menu for the local video.
* @param enable {boolean} true for enable, false for disable
*/
LocalVideo.prototype._enableDisableContextMenu = function(enable) {
if (this.$container.contextMenu) {
this.$container.contextMenu(enable);
}
};
/**
* Places the {@code LocalVideo} in the DOM based on the current video layout.
*
* @returns {void}
*/
LocalVideo.prototype.updateDOMLocation = function() {
if (!this.container) {
return;
}
if (this.container.parentElement) {
this.container.parentElement.removeChild(this.container);
/**
* Places the {@code LocalVideo} in the DOM based on the current video layout.
*
* @returns {void}
*/
updateDOMLocation() {
if (!this.container) {
return;
}
if (this.container.parentElement) {
this.container.parentElement.removeChild(this.container);
}
const appendTarget = shouldDisplayTileView(APP.store.getState())
? document.getElementById('localVideoTileViewContainer')
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
}
const appendTarget = shouldDisplayTileView(APP.store.getState())
? document.getElementById('localVideoTileViewContainer')
: document.getElementById('filmstripLocalVideoThumbnail');
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
_updateVideoElement() {
const localVideoContainer = document.getElementById('localVideoWrapper');
const videoTrack
= getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
appendTarget && appendTarget.appendChild(this.container);
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = { videoTrack } />
</Provider>,
localVideoContainer
);
this._updateVideoElement();
};
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay.
const video = this.container.querySelector('video');
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
LocalVideo.prototype._updateVideoElement = function() {
const localVideoContainer = document.getElementById('localVideoWrapper');
const videoTrack
= getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = { videoTrack } />
</Provider>,
localVideoContainer
);
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay.
const video = this.container.querySelector('video');
video && !config.testing?.noAutoPlayVideo && video.play();
};
export default LocalVideo;
video && !config.testing?.noAutoPlayVideo && video.play();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -166,27 +166,6 @@ const VideoLayout = {
localVideoThumbnail.updateIndicators();
},
/**
* Adds or removes icons for not available camera and microphone.
* @param resourceJid the jid of user
* @param devices available devices
*/
setDeviceAvailabilityIcons(id, devices) {
if (APP.conference.isLocalId(id)) {
localVideoThumbnail.setDeviceAvailabilityIcons(devices);
return;
}
const video = remoteVideos[id];
if (!video) {
return;
}
video.setDeviceAvailabilityIcons(devices);
},
/**
* Shows/hides local video.
* @param {boolean} true to make the local video visible, false - otherwise
@@ -411,27 +390,20 @@ const VideoLayout = {
/**
* Resizes thumbnails.
*/
resizeThumbnails(
forceUpdate = false,
onComplete = null) {
const { localVideo, remoteVideo }
= Filmstrip.calculateThumbnailSize();
resizeThumbnails(forceUpdate = false, onComplete = null) {
const { localVideo, remoteVideo } = Filmstrip.calculateThumbnailSize();
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
if (shouldDisplayTileView(APP.store.getState())) {
const height
= (localVideo && localVideo.thumbHeight)
|| (remoteVideo && remoteVideo.thumbnHeight)
|| 0;
const height = (localVideo && localVideo.thumbHeight) || (remoteVideo && remoteVideo.thumbnHeight) || 0;
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
}
localVideoThumbnail && localVideoThumbnail.rerender();
Object.values(remoteVideos).forEach(
remoteVideoThumbnail => remoteVideoThumbnail.rerender());
Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender());
if (onComplete && typeof onComplete === 'function') {
onComplete();

42
package-lock.json generated
View File

@@ -10931,8 +10931,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#4a87f342858963c36bb64a8a0e89d8d7e6e06060",
"from": "github:jitsi/lib-jitsi-meet#4a87f342858963c36bb64a8a0e89d8d7e6e06060",
"version": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"from": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.2",
@@ -14830,39 +14830,6 @@
"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",
@@ -14904,11 +14871,6 @@
}
}
},
"react-native-swipe-gestures": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.4.tgz",
"integrity": "sha512-C/vz0KPHNyqHk3uF4Cz/jzd/0N8z34ZgsjAZUh/RsXPH2FtJJf3Fw73pQDWJSoCMtvVadlztb8xQ+/aEQrll7w=="
},
"react-native-swipeout": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/react-native-swipeout/-/react-native-swipeout-2.3.6.tgz",

View File

@@ -57,7 +57,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#4a87f342858963c36bb64a8a0e89d8d7e6e06060",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",
@@ -72,14 +72,12 @@
"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",
"react-native-sound": "0.11.0",
"react-native-svg": "9.7.1",
"react-native-svg-transformer": "0.13.0",
"react-native-swipe-gestures": "1.0.4",
"react-native-swipeout": "2.3.6",
"react-native-watch-connectivity": "0.2.0",
"react-native-webrtc": "1.75.2",

View File

@@ -692,21 +692,6 @@ export function createSyncTrackStateEvent(mediaType, muted) {
};
}
/**
* Creates an event that indicates the thumbnail offset parent is null.
*
* @param {string} id - The id of the user related to the thumbnail.
* @returns {Object} The event in a format suitable for sending via sendAnalytics.
*/
export function createThumbnailOffsetParentIsNullEvent(id) {
return {
action: 'OffsetParentIsNull',
attributes: {
id
}
};
}
/**
* Creates an event associated with a toolbar button being clicked/pressed. By
* convention, where appropriate an attribute named 'enable' should be used to

View File

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

View File

@@ -35,12 +35,9 @@ 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, opt_userId, opt_config, opt_callback) { // eslint-disable-line camelcase, no-unused-vars
setUserId(userId) {
AmplitudeNative.setUserId(this._instanceName, userId);
}

View File

@@ -147,10 +147,9 @@ MiddlewareRegistry.register(store => next => action => {
const state = getState();
const { localTracksDuration } = state['features/analytics'];
if (localTracksDuration.conference.startedTime === -1 || action.mediaType === 'presenter') {
if (localTracksDuration.conference.startedTime === -1) {
// 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

@@ -43,9 +43,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
className = { this._getAvatarClassName() }
id = { this.props.id }
style = { this._getAvatarStyle(this.props.color) }>
<Icon
size = '50%'
src = { url } />
<Icon src = { url } />
</div>
);
}

View File

@@ -46,7 +46,6 @@ import {
getCurrentConference
} from './functions';
import logger from './logger';
import { MEDIA_TYPE } from '../media';
declare var APP: Object;
@@ -590,10 +589,7 @@ function _syncReceiveVideoQuality({ getState }, next, action) {
function _trackAddedOrRemoved(store, next, action) {
const track = action.track;
// 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) {
if (track && track.local) {
return (
_syncConferenceLocalTracksWithState(store, action)
.then(() => next(action)));

View File

@@ -29,12 +29,7 @@ type Props = {
* Handler for the cancel event, which happens when the user dismisses
* the sheet.
*/
onCancel: ?Function,
/**
* Function to render a bottom sheet header element, if necessary.
*/
renderHeader: ?Function
onCancel: ?Function
};
/**
@@ -48,7 +43,7 @@ class BottomSheet extends PureComponent<Props> {
* @returns {ReactElement}
*/
render() {
const { _styles, renderHeader } = this.props;
const { _styles } = this.props;
return (
<SlidingView
@@ -61,19 +56,19 @@ class BottomSheet extends PureComponent<Props> {
<View
pointerEvents = 'box-none'
style = { styles.sheetAreaCover } />
{ renderHeader && renderHeader() }
<SafeAreaView
<View
style = { [
styles.sheetItemContainer,
_styles.sheet
] }>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }
style = { styles.scrollView } >
{ this.props.children }
</ScrollView>
</SafeAreaView>
<SafeAreaView>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false } >
{ this.props.children }
</ScrollView>
</SafeAreaView>
</View>
</View>
</SlidingView>
);

View File

@@ -33,10 +33,6 @@ export const bottomSheetStyles = {
flex: 1
},
scrollView: {
paddingHorizontal: MD_ITEM_MARGIN_PADDING
},
/**
* Style for the container of the sheet.
*/
@@ -48,7 +44,9 @@ export const bottomSheetStyles = {
},
sheetItemContainer: {
flex: -1
flex: -1,
maxHeight: '60%',
paddingHorizontal: MD_ITEM_MARGIN_PADDING
}
};
@@ -137,45 +135,23 @@ export const inputDialog = {
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
*/
ColorSchemeRegistry.register('BottomSheet', {
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 {@code Icon} element in a generic item of the menu.
*/
iconStyle: {
color: schemeColor('icon'),
fontSize: 24
},
expandIcon: {
color: schemeColor('icon'),
fontSize: 16,
opacity: 0.7
/**
* 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
},
/**
@@ -183,7 +159,21 @@ 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

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 311 B

View File

@@ -25,7 +25,6 @@ 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

@@ -11,11 +11,7 @@ import {
STORE_VIDEO_TRANSFORM,
TOGGLE_CAMERA_FACING_MODE
} from './actionTypes';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
VIDEO_MUTISM_AUTHORITY
} from './constants';
import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
/**
* Action to adjust the availability of the local audio.
@@ -93,7 +89,6 @@ 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
@@ -102,7 +97,6 @@ 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) => {
@@ -113,8 +107,6 @@ export function setVideoMuted(
return dispatch({
type: SET_VIDEO_MUTED,
authority,
mediaType,
ensureTrack,
muted: newValue
});

View File

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

View File

@@ -17,11 +17,7 @@ import { getPropertyValue } from '../settings';
import { setTrackMuted, TRACK_ADDED } from '../tracks';
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
VIDEO_MUTISM_AUTHORITY
} from './constants';
import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
import logger from './logger';
import {
_AUDIO_INITIAL_MEDIA_STATE,
@@ -49,10 +45,7 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { track } = action;
// 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);
track.local && _syncTrackMutedState(store, track);
return result;
}
@@ -79,7 +72,7 @@ function _appStateChanged({ dispatch }, next, action) {
sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
return next(action);
}
@@ -101,11 +94,7 @@ function _setAudioOnly({ dispatch }, next, action) {
sendAnalytics(createTrackMutedEvent('video', 'audio-only mode', audioOnly));
// 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));
dispatch(setVideoMuted(audioOnly, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
return next(action);
}
@@ -242,9 +231,7 @@ function _setRoom({ dispatch, getState }, next, action) {
*/
function _syncTrackMutedState({ getState }, track) {
const state = getState()['features/base/media'];
const mediaType = track.mediaType === MEDIA_TYPE.PRESENTER
? MEDIA_TYPE.VIDEO : track.mediaType;
const muted = Boolean(state[mediaType].muted);
const muted = Boolean(state[track.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

@@ -10,47 +10,6 @@ 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: {
exact: result
}
}
};
const [ videoTrack ] = await JitsiMeetJS.createLocalTracks(
{
cameraDeviceId,
constraints,
devices: [ 'video' ]
});
videoTrack.type = MEDIA_TYPE.PRESENTER;
return videoTrack;
}
/**
* Create local tracks of specific types.
*
@@ -94,15 +53,11 @@ 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 ])
@@ -202,18 +157,6 @@ 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.
*
@@ -254,29 +197,6 @@ 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,7 +6,6 @@ import {
SET_AUDIO_MUTED,
SET_CAMERA_FACING_MODE,
SET_VIDEO_MUTED,
VIDEO_MUTISM_AUTHORITY,
TOGGLE_CAMERA_FACING_MODE,
toggleCameraFacingMode
} from '../media';
@@ -90,7 +89,7 @@ MiddlewareRegistry.register(store => next => action => {
return;
}
_setMuted(store, action, action.mediaType);
_setMuted(store, action, MEDIA_TYPE.VIDEO);
break;
case TOGGLE_CAMERA_FACING_MODE: {
@@ -132,7 +131,7 @@ MiddlewareRegistry.register(store => next => action => {
const { jitsiTrack } = action.track;
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();
const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
const isVideoTrack = jitsiTrack.isVideoTrack();
if (isVideoTrack) {
if (jitsiTrack.isLocal()) {
@@ -256,7 +255,7 @@ function _removeNoDataFromSourceNotification({ getState, dispatch }, track) {
* @private
* @returns {void}
*/
function _setMuted(store, { ensureTrack, authority, muted }, mediaType: MEDIA_TYPE) {
function _setMuted(store, { ensureTrack, muted }, mediaType: MEDIA_TYPE) {
const localTrack
= _getLocalTrack(store, mediaType, /* includePending */ true);
@@ -266,12 +265,8 @@ function _setMuted(store, { ensureTrack, authority, muted }, mediaType: MEDIA_TY
// `jitsiTrack`, then the `muted` state will be applied once the
// `jitsiTrack` is created.
const { jitsiTrack } = localTrack;
const isAudioOnly = authority === VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY;
// 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);
jitsiTrack && 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

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

View File

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

View File

@@ -1,6 +1,6 @@
// @flow
import { Alert, NativeModules, Platform } from 'react-native';
import { Alert, Platform } from 'react-native';
import uuid from 'uuid';
import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
@@ -36,7 +36,6 @@ import CallKit from './CallKit';
import ConnectionService from './ConnectionService';
import { isCallIntegrationEnabled } from './functions';
const { AudioMode } = NativeModules;
const CallIntegration = CallKit || ConnectionService;
/**
@@ -186,25 +185,13 @@ 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());
}
})
.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());
}
});
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());
}
});
}
return result;
@@ -289,8 +276,7 @@ function _conferenceWillJoin({ dispatch, getState }, next, action) {
}
})
.catch(error => {
// Currently this error codes are emitted only by Android.
//
// Currently this error code is 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.
@@ -304,45 +290,12 @@ 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

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

View File

@@ -1,161 +0,0 @@
// @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

@@ -1,62 +0,0 @@
// @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

@@ -1,19 +0,0 @@
// @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,13 +10,14 @@ 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 { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks';
import { isLocalTrackMuted } from '../../base/tracks';
import UIEvents from '../../../../service/UI/UIEvents';
declare var APP: Object;
@@ -31,11 +32,6 @@ type Props = AbstractButtonProps & {
*/
_audioOnly: boolean,
/**
* MEDIA_TYPE of the local video.
*/
_videoMediaType: string,
/**
* Whether video is currently muted or not.
*/
@@ -140,12 +136,10 @@ 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));
@@ -173,8 +167,7 @@ function _mapStateToProps(state): Object {
return {
_audioOnly: Boolean(audioOnly),
_videoMediaType: getLocalVideoType(tracks),
_videoMuted: isLocalVideoTrackMuted(tracks)
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
};
}

View File

@@ -1,13 +1,10 @@
// @flow
import React, { PureComponent } from 'react';
import { Platform, TouchableOpacity } from 'react-native';
import Collapsible from 'react-native-collapsible';
import GestureRecognizer, { swipeDirections } from 'react-native-swipe-gestures';
import React, { Component } from 'react';
import { Platform } from 'react-native';
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';
@@ -19,12 +16,10 @@ import { RoomLockButton } from '../../../room-lock';
import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout';
import HelpButton from '../HelpButton';
import AudioOnlyButton from './AudioOnlyButton';
import HelpButton from '../HelpButton';
import RaiseHandButton from './RaiseHandButton';
import ToggleCameraButton from './ToggleCameraButton';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link OverflowMenu}.
@@ -57,14 +52,6 @@ type Props = {
dispatch: Function
};
type State = {
/**
* True if the 'more' button set needas to be rendered.
*/
showMore: boolean
}
/**
* The exported React {@code Component}. We need it to execute
* {@link hideDialog}.
@@ -78,7 +65,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 PureComponent<Props, State> {
class OverflowMenu extends Component<Props> {
/**
* Initializes a new {@code OverflowMenu} instance.
*
@@ -87,15 +74,8 @@ class OverflowMenu extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
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);
}
/**
@@ -105,71 +85,37 @@ class OverflowMenu extends PureComponent<Props, State> {
* @returns {ReactElement}
*/
render() {
const { _bottomSheetStyles } = this.props;
const { showMore } = this.state;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
styles: _bottomSheetStyles.buttons
styles: this.props._bottomSheetStyles
};
return (
<BottomSheet
onCancel = { this._onCancel }
renderHeader = { this._renderMenuExpandToggle }>
<BottomSheet onCancel = { this._onCancel }>
<AudioRouteButton { ...buttonProps } />
<ToggleCameraButton { ...buttonProps } />
<AudioOnlyButton { ...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>
<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 } />
</BottomSheet>
);
}
_renderMenuExpandToggle: () => React$Element<any>;
/**
* Function to render the menu toggle in the bottom sheet header area.
*
* @returns {React$Element}
*/
_renderMenuExpandToggle() {
return (
<GestureRecognizer
config = {{
velocityThreshold: 0.1,
directionalOffsetThreshold: 30
}}
onSwipe = { this._onSwipe }
style = { [
this.props._bottomSheetStyles.sheet,
styles.expandMenuContainer
] }>
<TouchableOpacity onPress = { this._onToggleMenu }>
{ /* $FlowFixMeProps */ }
<IconDragHandle style = { this.props._bottomSheetStyles.expandIcon } />
</TouchableOpacity>
</GestureRecognizer>
);
}
_onCancel: () => boolean;
/**
@@ -187,50 +133,6 @@ class OverflowMenu extends PureComponent<Props, State> {
return false;
}
_onSwipe: (string) => void;
/**
* Callback to be invoked when a swipe gesture is detected on the menu.
*
* @param {string} gestureName - The name of the swipe gesture.
* @returns {void}
*/
_onSwipe(gestureName) {
const { showMore } = this.state;
switch (gestureName) {
case swipeDirections.SWIPE_UP:
!showMore && this.setState({
showMore: true
});
break;
case swipeDirections.SWIPE_DOWN:
if (showMore) {
// If the menu is expanded, we collapse it.
this.setState({
showMore: false
});
} else {
// If the menu is not expanded, we close the menu
this._onCancel();
}
break;
}
}
_onToggleMenu: () => void;
/**
* Callback to be invoked when the expand menu button is pressed.
*
* @returns {void}
*/
_onToggleMenu() {
this.setState({
showMore: !this.state.showMore
});
}
}
/**

View File

@@ -55,11 +55,6 @@ const whiteToolbarButtonIcon = {
*/
const styles = {
expandMenuContainer: {
alignItems: 'center',
flexDirection: 'column'
},
/**
* The style of the toolbar.
*/

View File

@@ -101,7 +101,6 @@ export class AbstractWelcomePage extends Component<Props, *> {
*/
componentDidMount() {
this._mounted = true;
sendAnalytics(createWelcomePageEvent('viewed', undefined, { value: 1 }));
}
/**

View File

@@ -116,8 +116,6 @@ class WelcomePage extends AbstractWelcomePage {
* @returns {void}
*/
componentDidMount() {
super.componentDidMount();
document.body.classList.add('welcome-page');
document.title = interfaceConfig.APP_NAME;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,34 +0,0 @@
local uuid_gen = require "util.uuid".generate;
-- Module that generates a unique meetingId, attaches it to the room
-- and adds it to all disco info form data (when room is queried or in the
-- initial room owner config)
-- Hook to assign meetingId for new rooms
module:hook("muc-room-created", function(event)
local room = event.room;
room._data.meetingId = uuid_gen();
module:log("debug", "Created meetingId:%s for %s",
room._data.meetingId, room.jid);
end);
-- Returns the meeting config Id form data.
function getMeetingIdConfig(room)
return {
name = "muc#roominfo_meetingId";
type = "text-single";
label = "The meeting unique id.";
value = room._data.meetingId or "";
};
end
-- add meeting Id to the disco info requests to the room
module:hook("muc-disco#info", function(event)
table.insert(event.form, getMeetingIdConfig(event.room));
end);
-- add the meeting Id in the default config we return to jicofo
module:hook("muc-config-form", function(event)
table.insert(event.form, getMeetingIdConfig(event.room));
end, 90-3);

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="../../libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="libs/load-test-participant.min.js" ></script>
</head>
<body>
<div>Number of participants: <span id="participants">1</span></div>
</body>
</html>

View File

@@ -0,0 +1,261 @@
/* global $, JitsiMeetJS */
import 'jquery';
import parseURLParams from '../../react/features/base/config/parseURLParams';
const params = parseURLParams(window.location, false, 'hash');
const { isHuman = false } = params;
const {
roomName = 'loadtest0',
localAudio = isHuman,
localVideo = isHuman,
remoteVideo = isHuman,
remoteAudio = isHuman
} = params;
const options = {
hosts: {
domain: 'george-perf.jitsi.net',
muc: 'conference.george-perf.jitsi.net'
},
bosh: '//george-perf.jitsi.net/http-bind',
// The name of client node advertised in XEP-0115 'c' stanza
clientNode: 'http://jitsi.org/jitsimeet'
};
const confOptions = {
openBridgeChannel: 'websocket',
testing: {
testMode: true,
noAutoPlayVideo: true
},
disableNS: true,
disableAEC: true,
gatherStats: true,
callStatsID: false
};
let connection = null;
let isJoined = false;
let room = null;
let numParticipants = 1;
let localTracks = [];
const remoteTracks = {};
window.APP = {
get room() {
return room;
},
get connection() {
return connection;
},
get numParticipants() {
return numParticipants;
},
get localTracks() {
return localTracks;
},
get remoteTracks() {
return remoteTracks;
},
get params() {
return {
roomName,
localAudio,
localVideo,
remoteVideo,
remoteAudio
};
}
};
/**
*
*/
function setNumberOfParticipants() {
$('#participants').text(numParticipants);
}
/**
* Handles local tracks.
* @param tracks Array with JitsiTrack objects
*/
function onLocalTracks(tracks = []) {
localTracks = tracks;
for (let i = 0; i < localTracks.length; i++) {
if (localTracks[i].getType() === 'video') {
$('body').append(`<video autoplay='1' id='localVideo${i}' />`);
localTracks[i].attach($(`#localVideo${i}`)[0]);
} else {
$('body').append(
`<audio autoplay='1' muted='true' id='localAudio${i}' />`);
localTracks[i].attach($(`#localAudio${i}`)[0]);
}
if (isJoined) {
room.addTrack(localTracks[i]);
}
}
}
/**
* Handles remote tracks
* @param track JitsiTrack object
*/
function onRemoteTrack(track) {
if (track.isLocal()
|| (track.getType() === 'video' && !remoteVideo) || (track.getType() === 'audio' && !remoteAudio)) {
return;
}
const participant = track.getParticipantId();
if (!remoteTracks[participant]) {
remoteTracks[participant] = [];
}
const idx = remoteTracks[participant].push(track);
const id = participant + track.getType() + idx;
if (track.getType() === 'video') {
$('body').append(`<video autoplay='1' id='${id}' />`);
} else {
$('body').append(`<audio autoplay='1' id='${id}' />`);
}
track.attach($(`#${id}`)[0]);
}
/**
* That function is executed when the conference is joined
*/
function onConferenceJoined() {
isJoined = true;
for (let i = 0; i < localTracks.length; i++) {
room.addTrack(localTracks[i]);
}
}
/**
*
* @param id
*/
function onUserLeft(id) {
numParticipants--;
setNumberOfParticipants();
if (!remoteTracks[id]) {
return;
}
const tracks = remoteTracks[id];
for (let i = 0; i < tracks.length; i++) {
const container = $(`#${id}${tracks[i].getType()}${i + 1}`)[0];
if (container) {
tracks[i].detach(container);
container.parentElement.removeChild(container);
}
}
}
/**
* That function is called when connection is established successfully
*/
function onConnectionSuccess() {
room = connection.initJitsiConference(roomName, confOptions);
room.on(JitsiMeetJS.events.conference.TRACK_ADDED, onRemoteTrack);
room.on(JitsiMeetJS.events.conference.CONFERENCE_JOINED, onConferenceJoined);
room.on(JitsiMeetJS.events.conference.USER_JOINED, id => {
numParticipants++;
setNumberOfParticipants();
remoteTracks[id] = [];
});
room.on(JitsiMeetJS.events.conference.USER_LEFT, onUserLeft);
room.join();
}
/**
* This function is called when the connection fail.
*/
function onConnectionFailed() {
console.error('Connection Failed!');
}
/**
* This function is called when we disconnect.
*/
function disconnect() {
console.log('disconnect!');
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED,
onConnectionSuccess);
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_FAILED,
onConnectionFailed);
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED,
disconnect);
}
/**
*
*/
function unload() {
for (let i = 0; i < localTracks.length; i++) {
localTracks[i].dispose();
}
room.leave();
connection.disconnect();
}
$(window).bind('beforeunload', unload);
$(window).bind('unload', unload);
JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
const initOptions = {
disableAudioLevels: true,
// The ID of the jidesha extension for Chrome.
desktopSharingChromeExtId: 'mbocklcggfhnbahlnepmldehdhpjfcjp',
// Whether desktop sharing should be disabled on Chrome.
desktopSharingChromeDisabled: true,
// The media sources to use when using screen sharing with the Chrome
// extension.
desktopSharingChromeSources: [ 'screen', 'window' ],
// Required version of Chrome extension
desktopSharingChromeMinExtVersion: '0.1',
// Whether desktop sharing should be disabled on Firefox.
desktopSharingFirefoxDisabled: true
};
JitsiMeetJS.init(initOptions);
connection = new JitsiMeetJS.JitsiConnection(null, null, options);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED, onConnectionSuccess);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_FAILED, onConnectionFailed);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED, disconnect);
connection.connect();
const devices = [];
if (localVideo) {
devices.push('video');
}
if (localAudio) {
devices.push('audio');
}
if (devices.length > 0) {
JitsiMeetJS.createLocalTracks({ devices })
.then(onLocalTracks)
.catch(error => {
throw error;
});
}

6983
static/load-test/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
{
"name": "jitsi-meet-load-test",
"version": "0.0.0",
"description": "A load test participant",
"repository": {
"type": "git",
"url": "git://github.com/jitsi/jitsi-meet"
},
"keywords": [
"jingle",
"webrtc",
"xmpp",
"browser"
],
"author": "",
"readmeFilename": "../README.md",
"dependencies": {
"jquery": "3.4.0"
},
"devDependencies": {
"@babel/core": "7.5.5",
"@babel/plugin-proposal-class-properties": "7.1.0",
"@babel/plugin-proposal-export-default-from": "7.0.0",
"@babel/plugin-proposal-export-namespace-from": "7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
"@babel/plugin-proposal-optional-chaining": "7.2.0",
"@babel/plugin-transform-flow-strip-types": "7.0.0",
"@babel/preset-env": "7.1.0",
"@babel/preset-flow": "7.0.0",
"@babel/runtime": "7.5.5",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.4",
"eslint": "5.6.1",
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#1.0.1",
"eslint-plugin-flowtype": "2.50.3",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jsdoc": "3.8.0",
"expose-loader": "0.7.5",
"flow-bin": "0.104.0",
"imports-loader": "0.7.1",
"string-replace-loader": "2.1.1",
"style-loader": "0.19.0",
"webpack": "4.27.1",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.1.2"
},
"engines": {
"node": ">=8.0.0",
"npm": ">=6.0.0"
},
"license": "Apache-2.0",
"scripts": {
"build": "webpack -p"
}
}

View File

@@ -0,0 +1,131 @@
/* global __dirname */
const process = require('process');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const analyzeBundle = process.argv.indexOf('--analyze-bundle') !== -1;
const minimize
= process.argv.indexOf('-p') !== -1
|| process.argv.indexOf('--optimize-minimize') !== -1;
/**
* Build a Performance configuration object for the given size.
* See: https://webpack.js.org/configuration/performance/
*/
function getPerformanceHints(size) {
return {
hints: minimize ? 'error' : false,
maxAssetSize: size,
maxEntrypointSize: size
};
}
// The base Webpack configuration to bundle the JavaScript artifacts of
// jitsi-meet such as app.bundle.js and external_api.js.
const config = {
devtool: 'source-map',
mode: minimize ? 'production' : 'development',
module: {
rules: [ {
// Transpile ES2015 (aka ES6) to ES5. Accept the JSX syntax by React
// as well.
exclude: [
new RegExp(`${__dirname}/node_modules/(?!js-utils)`)
],
loader: 'babel-loader',
options: {
// XXX The require.resolve bellow solves failures to locate the
// presets when lib-jitsi-meet, for example, is npm linked in
// jitsi-meet.
plugins: [
require.resolve('@babel/plugin-transform-flow-strip-types'),
require.resolve('@babel/plugin-proposal-class-properties'),
require.resolve('@babel/plugin-proposal-export-default-from'),
require.resolve('@babel/plugin-proposal-export-namespace-from'),
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'),
require.resolve('@babel/plugin-proposal-optional-chaining')
],
presets: [
[
require.resolve('@babel/preset-env'),
// Tell babel to avoid compiling imports into CommonJS
// so that webpack may do tree shaking.
{
modules: false,
// Specify our target browsers so no transpiling is
// done unnecessarily. For browsers not specified
// here, the ES2015+ profile will be used.
targets: {
chrome: 58,
electron: 2,
firefox: 54,
safari: 11
}
}
],
require.resolve('@babel/preset-flow'),
require.resolve('@babel/preset-react')
]
},
test: /\.jsx?$/
}, {
// Expose jquery as the globals $ and jQuery because it is expected
// to be available in such a form by multiple jitsi-meet
// dependencies including lib-jitsi-meet.
loader: 'expose-loader?$!expose-loader?jQuery',
test: /\/node_modules\/jquery\/.*\.js$/
}]
},
node: {
// Allow the use of the real filename of the module being executed. By
// default Webpack does not leak path-related information and provides a
// value that is a mock (/index.js).
__filename: true
},
optimization: {
concatenateModules: minimize,
minimize
},
output: {
filename: `[name]${minimize ? '.min' : ''}.js`,
path: `${__dirname}/libs`,
publicPath: 'load-test/libs/',
sourceMapFilename: `[name].${minimize ? 'min' : 'js'}.map`
},
plugins: [
analyzeBundle
&& new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true
})
].filter(Boolean),
resolve: {
alias: {
jquery: `jquery/dist/jquery${minimize ? '.min' : ''}.js`
},
aliasFields: [
'browser'
],
extensions: [
'.web.js',
// Webpack defaults:
'.js',
'.json'
]
}
};
module.exports = [
Object.assign({}, config, {
entry: {
'load-test-participant': './load-test-participant.js'
},
performance: getPerformanceHints(3 * 1024 * 1024)
})
];