>> dependencies() {
+ return Collections.emptyList();
+ }
+}
\ No newline at end of file
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeet.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeet.java
new file mode 100644
index 0000000..b8873e9
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeet.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright @ 2017-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+import com.facebook.react.ReactInstanceManager;
+
+import com.splashview.SplashView;
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+public class JitsiMeet {
+
+ /**
+ * Default {@link JitsiMeetConferenceOptions} which will be used for all conferences. When
+ * joining a conference these options will be merged with the ones passed to
+ * {@link JitsiMeetView} join().
+ */
+ private static JitsiMeetConferenceOptions defaultConferenceOptions;
+
+ public static JitsiMeetConferenceOptions getDefaultConferenceOptions() {
+ return defaultConferenceOptions;
+ }
+
+ public static void setDefaultConferenceOptions(JitsiMeetConferenceOptions options) {
+ if (options != null && options.getRoom() != null) {
+ throw new RuntimeException("'room' must be null in the default conference options");
+ }
+ defaultConferenceOptions = options;
+ }
+
+ /**
+ * Returns the current conference URL as a string.
+ *
+ * @return the current conference URL.
+ */
+ public static String getCurrentConference() {
+ return OngoingConferenceTracker.getInstance().getCurrentConference();
+ }
+
+ /**
+ * Helper to get the default conference options as a {@link Bundle}.
+ *
+ * @return a {@link Bundle} with the default conference options.
+ */
+ static Bundle getDefaultProps() {
+ if (defaultConferenceOptions != null) {
+ return defaultConferenceOptions.asProps();
+ }
+
+ return new Bundle();
+ }
+
+ /**
+ * Used in development mode. It displays the React Native development menu.
+ */
+ public static void showDevOptions() {
+ ReactInstanceManager reactInstanceManager
+ = ReactInstanceManagerHolder.getReactInstanceManager();
+
+ if (reactInstanceManager != null) {
+ reactInstanceManager.showDevOptionsDialog();
+ }
+ }
+
+ public static boolean isCrashReportingDisabled(Context context) {
+ SharedPreferences preferences = context.getSharedPreferences("jitsi-default-preferences", Context.MODE_PRIVATE);
+ String value = preferences.getString("isCrashReportingDisabled", "");
+ return Boolean.parseBoolean(value);
+ }
+
+ /**
+ * Helper method to show the SplashScreen.
+ *
+ * @param activity - The activity on which to show the SplashScreen {@link Activity}.
+ */
+ public static void showSplashScreen(Activity activity) {
+ try {
+ SplashView.INSTANCE.showSplashView(activity);
+ } catch (Exception e) {
+ JitsiMeetLogger.e(e, "Failed to show splash screen");
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java
new file mode 100644
index 0000000..d8eadc0
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import com.facebook.react.modules.core.PermissionListener;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+import java.util.HashMap;
+
+/**
+ * A base activity for SDK users to embed. It contains all the required wiring
+ * between the {@code JitsiMeetView} and the Activity lifecycle methods.
+ *
+ * In this activity we use a single {@code JitsiMeetView} instance. This
+ * instance gives us access to a view which displays the welcome page and the
+ * conference itself. All lifecycle methods associated with this Activity are
+ * hooked to the React Native subsystem via proxy calls through the
+ * {@code JitsiMeetActivityDelegate} static methods.
+ */
+public class JitsiMeetActivity extends AppCompatActivity
+ implements JitsiMeetActivityInterface {
+
+ protected static final String TAG = JitsiMeetActivity.class.getSimpleName();
+
+ private static final String ACTION_JITSI_MEET_CONFERENCE = "org.jitsi.meet.CONFERENCE";
+ private static final String JITSI_MEET_CONFERENCE_OPTIONS = "JitsiMeetConferenceOptions";
+
+ private boolean isReadyToClose;
+
+ private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onBroadcastReceived(intent);
+ }
+ };
+
+ /**
+ * Instance of the {@link JitsiMeetView} which this activity will display.
+ */
+ private JitsiMeetView jitsiView;
+
+ // Helpers for starting the activity
+ //
+
+ public static void launch(Context context, JitsiMeetConferenceOptions options) {
+ Intent intent = new Intent(context, JitsiMeetActivity.class);
+ intent.setAction(ACTION_JITSI_MEET_CONFERENCE);
+ intent.putExtra(JITSI_MEET_CONFERENCE_OPTIONS, options);
+ if (!(context instanceof Activity)) {
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ context.startActivity(intent);
+ }
+
+ public static void launch(Context context, String url) {
+ JitsiMeetConferenceOptions options
+ = new JitsiMeetConferenceOptions.Builder().setRoom(url).build();
+ launch(context, options);
+ }
+
+ public static void addTopBottomInsets(@NonNull Window w, @NonNull View v) {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) return;
+
+ View decorView = w.getDecorView();
+
+ decorView.post(() -> {
+ WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
+ if (insets != null) {
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
+ params.topMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
+ params.bottomMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
+ v.setLayoutParams(params);
+
+ decorView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
+ view.setBackgroundColor(JitsiMeetView.BACKGROUND_COLOR);
+
+ return windowInsets;
+ });
+ }
+ });
+ }
+
+ // Overrides
+ //
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ Intent intent = new Intent("onConfigurationChanged");
+ intent.putExtra("newConfig", newConfig);
+ this.sendBroadcast(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // ReactInstanceManager is now initialized by JitsiInitializer during application startup
+ // Just call onHostResume since the manager is already ready
+ JitsiMeetActivityDelegate.onHostResume(this);
+
+ setContentView(R.layout.activity_jitsi_meet);
+ addTopBottomInsets(getWindow(),findViewById(android.R.id.content));
+ this.jitsiView = findViewById(R.id.jitsiView);
+
+ registerForBroadcastMessages();
+
+ if (!extraInitialize()) {
+ initialize();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ JitsiMeetActivityDelegate.onHostResume(this);
+ }
+
+ @Override
+ public void onStop() {
+ JitsiMeetActivityDelegate.onHostPause(this);
+ super.onStop();
+ }
+
+ @Override
+ public void onDestroy() {
+ JitsiMeetLogger.i("onDestroy()");
+
+ // Here we are trying to handle the following corner case: an application using the SDK
+ // is using this Activity for displaying meetings, but there is another "main" Activity
+ // with other content. If this Activity is "swiped out" from the recent list we will get
+ // Activity#onDestroy() called without warning. At this point we can try to leave the
+ // current meeting, but when our view is detached from React the JS <-> Native bridge won't
+ // be operational so the external API won't be able to notify the native side that the
+ // conference terminated. Thus, try our best to clean up.
+ if (!isReadyToClose) {
+ JitsiMeetLogger.i("onDestroy(): leaving...");
+ leave();
+ }
+
+ this.jitsiView = null;
+
+ if (AudioModeModule.useConnectionService()) {
+ ConnectionService.abortConnections();
+ }
+ JitsiMeetOngoingConferenceService.abort(this);
+
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
+
+ JitsiMeetActivityDelegate.onHostDestroy(this);
+
+ super.onDestroy();
+ }
+
+ @Override
+ public void finish() {
+ if (!isReadyToClose) {
+ JitsiMeetLogger.i("finish(): leaving...");
+ leave();
+ }
+
+ JitsiMeetLogger.i("finish(): finishing...");
+ super.finish();
+ }
+
+ // Helper methods
+ //
+
+ protected JitsiMeetView getJitsiView() {
+ return jitsiView;
+ }
+
+ public void join(@Nullable String url) {
+ JitsiMeetConferenceOptions options
+ = new JitsiMeetConferenceOptions.Builder()
+ .setRoom(url)
+ .build();
+ join(options);
+ }
+
+ public void join(JitsiMeetConferenceOptions options) {
+ if (this.jitsiView != null) {
+ this.jitsiView.join(options);
+ } else {
+ JitsiMeetLogger.w("Cannot join, view is null");
+ }
+ }
+
+ protected void leave() {
+ if (this.jitsiView != null) {
+ this.jitsiView.abort();
+ } else {
+ JitsiMeetLogger.w("Cannot leave, view is null");
+ }
+ }
+
+ private @Nullable
+ JitsiMeetConferenceOptions getConferenceOptions(Intent intent) {
+ String action = intent.getAction();
+
+ if (Intent.ACTION_VIEW.equals(action)) {
+ Uri uri = intent.getData();
+ if (uri != null) {
+ return new JitsiMeetConferenceOptions.Builder().setRoom(uri.toString()).build();
+ }
+ } else if (ACTION_JITSI_MEET_CONFERENCE.equals(action)) {
+ return intent.getParcelableExtra(JITSI_MEET_CONFERENCE_OPTIONS);
+ }
+
+ return null;
+ }
+
+ /**
+ * Helper function called during activity initialization. If {@code true} is returned, the
+ * initialization is delayed and the {@link JitsiMeetActivity#initialize()} method is not
+ * called. In this case, it's up to the subclass to call the initialize method when ready.
+ *
+ * This is mainly required so we do some extra initialization in the Jitsi Meet app.
+ *
+ * @return {@code true} if the initialization will be delayed, {@code false} otherwise.
+ */
+ protected boolean extraInitialize() {
+ return false;
+ }
+
+ protected void initialize() {
+ // Join the room specified by the URL the app was launched with.
+ // Joining without the room option displays the welcome page.
+ join(getConferenceOptions(getIntent()));
+ }
+
+ protected void onConferenceJoined(HashMap extraData) {
+ JitsiMeetLogger.i("Conference joined: " + extraData);
+ // Launch the service for the ongoing notification.
+ JitsiMeetOngoingConferenceService.launch(this, extraData);
+ }
+
+ protected void onConferenceTerminated(HashMap extraData) {
+ JitsiMeetLogger.i("Conference terminated: " + extraData);
+ }
+
+ protected void onConferenceWillJoin(HashMap extraData) {
+ JitsiMeetLogger.i("Conference will join: " + extraData);
+ }
+
+ protected void onParticipantJoined(HashMap extraData) {
+ try {
+ JitsiMeetLogger.i("Participant joined: ", extraData);
+ } catch (Exception e) {
+ JitsiMeetLogger.w("Invalid participant joined extraData", e);
+ }
+ }
+
+ protected void onParticipantLeft(HashMap extraData) {
+ try {
+ JitsiMeetLogger.i("Participant left: ", extraData);
+ } catch (Exception e) {
+ JitsiMeetLogger.w("Invalid participant left extraData", e);
+ }
+ }
+
+ protected void onReadyToClose() {
+ JitsiMeetLogger.i("SDK is ready to close");
+ isReadyToClose = true;
+ finish();
+ }
+
+// protected void onTranscriptionChunkReceived(HashMap extraData) {
+// JitsiMeetLogger.i("Transcription chunk received: " + extraData);
+// }
+
+// protected void onCustomButtonPressed(HashMap extraData) {
+// JitsiMeetLogger.i("Custom button pressed: " + extraData);
+// }
+
+// protected void onConferenceUniqueIdSet(HashMap extraData) {
+// JitsiMeetLogger.i("Conference unique id set: " + extraData);
+// }
+
+// protected void onRecordingStatusChanged(HashMap extraData) {
+// JitsiMeetLogger.i("Recording status changed: " + extraData);
+// }
+
+ // Activity lifecycle methods
+ //
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ JitsiMeetActivityDelegate.onActivityResult(this, requestCode, resultCode, data);
+ }
+
+ @Override
+ public void onBackPressed() {
+ JitsiMeetActivityDelegate.onBackPressed();
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ JitsiMeetConferenceOptions options;
+
+ if ((options = getConferenceOptions(intent)) != null) {
+ join(options);
+ return;
+ }
+
+ JitsiMeetActivityDelegate.onNewIntent(intent);
+ }
+
+ @Override
+ protected void onUserLeaveHint() {
+ if (this.jitsiView != null) {
+ this.jitsiView.enterPictureInPicture();
+ }
+ }
+
+ // JitsiMeetActivityInterface
+ //
+
+ @Override
+ public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
+ JitsiMeetActivityDelegate.requestPermissions(this, permissions, requestCode, listener);
+ }
+
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ private void registerForBroadcastMessages() {
+ IntentFilter intentFilter = new IntentFilter();
+
+ for (BroadcastEvent.Type type : BroadcastEvent.Type.values()) {
+ intentFilter.addAction(type.getAction());
+ }
+
+ LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter);
+ }
+
+ private void onBroadcastReceived(Intent intent) {
+ if (intent != null) {
+ BroadcastEvent event = new BroadcastEvent(intent);
+
+ switch (event.getType()) {
+ case CONFERENCE_JOINED:
+ onConferenceJoined(event.getData());
+ break;
+ case CONFERENCE_WILL_JOIN:
+ onConferenceWillJoin(event.getData());
+ break;
+ case CONFERENCE_TERMINATED:
+ onConferenceTerminated(event.getData());
+ break;
+ case PARTICIPANT_JOINED:
+ onParticipantJoined(event.getData());
+ break;
+ case PARTICIPANT_LEFT:
+ onParticipantLeft(event.getData());
+ break;
+ case READY_TO_CLOSE:
+ onReadyToClose();
+ break;
+ // case TRANSCRIPTION_CHUNK_RECEIVED:
+ // onTranscriptionChunkReceived(event.getData());
+ // break;
+ // case CUSTOM_BUTTON_PRESSED:
+ // onCustomButtonPressed(event.getData());
+ // break;
+ // case CONFERENCE_UNIQUE_ID_SET:
+ // onConferenceUniqueIdSet(event.getData());
+ // break;
+ // case RECORDING_STATUS_CHANGED:
+ // onRecordingStatusChanged(event.getData());
+ // break;
+ }
+ }
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivityDelegate.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivityDelegate.java
new file mode 100644
index 0000000..4145eeb
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivityDelegate.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright @ 2018-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.app.Activity;
+import android.content.Intent;
+
+import com.facebook.react.ReactInstanceManager;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.modules.core.PermissionListener;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+/**
+ * Helper class to encapsulate the work which needs to be done on
+ * {@link Activity} lifecycle methods in order for the React side to be aware of
+ * it.
+ */
+public class JitsiMeetActivityDelegate {
+ /**
+ * Needed for making sure this class working with the "PermissionsAndroid"
+ * React Native module.
+ */
+ private static PermissionListener permissionListener;
+ private static Callback permissionsCallback;
+
+ /**
+ * Tells whether or not the permissions request is currently in progress.
+ *
+ * @return {@code true} if the permissions are being requested or {@code false} otherwise.
+ */
+ static boolean arePermissionsBeingRequested() {
+ return permissionListener != null;
+ }
+
+ /**
+ * {@link Activity} lifecycle method which should be called from
+ * {@code Activity#onActivityResult} so we are notified about results of external intents
+ * started/finished.
+ *
+ * @param activity {@code Activity} activity from where the result comes from.
+ * @param requestCode {@code int} code of the request.
+ * @param resultCode {@code int} code of the result.
+ * @param data {@code Intent} the intent of the activity.
+ */
+ public static void onActivityResult(
+ Activity activity,
+ int requestCode,
+ int resultCode,
+ Intent data) {
+ ReactInstanceManager reactInstanceManager
+ = ReactInstanceManagerHolder.getReactInstanceManager();
+
+ if (reactInstanceManager != null) {
+ reactInstanceManager.onActivityResult(activity, requestCode, resultCode, data);
+ }
+ }
+
+ /**
+ * {@link Activity} lifecycle method which should be called from
+ * {@link Activity#onBackPressed} so we can do the required internal
+ * processing.
+ *
+ * @return {@code true} if the back-press was processed; {@code false},
+ * otherwise. If {@code false}, the application should call the
+ * {@code super}'s implementation.
+ */
+ public static void onBackPressed() {
+ ReactInstanceManager reactInstanceManager
+ = ReactInstanceManagerHolder.getReactInstanceManager();
+
+ if (reactInstanceManager != null) {
+ reactInstanceManager.onBackPressed();
+ }
+ }
+
+ /**
+ * {@link Activity} lifecycle method which should be called from
+ * {@code Activity#onDestroy} so we can do the required internal
+ * processing.
+ *
+ * @param activity {@code Activity} being destroyed.
+ */
+ public static void onHostDestroy(Activity activity) {
+ ReactInstanceManager reactInstanceManager
+ = ReactInstanceManagerHolder.getReactInstanceManager();
+
+ if (reactInstanceManager != null) {
+ reactInstanceManager.onHostDestroy(activity);
+ }
+ }
+
+ /**
+ * {@link Activity} lifecycle method which should be called from
+ * {@code Activity#onPause} so we can do the required internal processing.
+ *
+ * @param activity {@code Activity} being paused.
+ */
+ public static void onHostPause(Activity activity) {
+ ReactInstanceManager reactInstanceManager
+ = ReactInstanceManagerHolder.getReactInstanceManager();
+
+ if (reactInstanceManager != null) {
+ try {
+ reactInstanceManager.onHostPause(activity);
+ } catch (AssertionError e) {
+ // There seems to be a problem in RN when resuming an Activity when
+ // rotation is involved and the planets align. There doesn't seem to
+ // be a proper solution, but since the activity is going away anyway,
+ // we'll YOLO-ignore the exception and hope fo the best.
+ // Ref: https://github.com/facebook/react-native/search?q=Pausing+an+activity+that+is+not+the+current+activity%2C+this+is+incorrect%21&type=issues
+ JitsiMeetLogger.e(e, "Error running onHostPause, ignoring");
+ }
+ }
+ }
+
+ /**
+ * {@link Activity} lifecycle method which should be called from
+ * {@code Activity#onResume} so we can do the required internal processing.
+ *
+ * @param activity {@code Activity} being resumed.
+ */
+ public static void onHostResume(Activity activity) {
+ ReactInstanceManager reactInstanceManager
+ = ReactInstanceManagerHolder.getReactInstanceManager();
+
+ if (reactInstanceManager != null) {
+ reactInstanceManager.onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity));
+ }
+
+ if (permissionsCallback != null) {
+ permissionsCallback.invoke();
+ permissionsCallback = null;
+ }
+ }
+
+ /**
+ * {@link Activity} lifecycle method which should be called from
+ * {@code Activity#onNewIntent} so we can do the required internal
+ * processing. Note that this is only needed if the activity's "launchMode"
+ * was set to "singleTask". This is required for deep linking to work once
+ * the application is already running.
+ *
+ * @param intent {@code Intent} instance which was received.
+ */
+ public static void onNewIntent(Intent intent) {
+ ReactInstanceManager reactInstanceManager
+ = ReactInstanceManagerHolder.getReactInstanceManager();
+
+ if (reactInstanceManager != null) {
+ reactInstanceManager.onNewIntent(intent);
+ }
+ }
+
+ public static void onRequestPermissionsResult(
+ final int requestCode, final String[] permissions, final int[] grantResults) {
+ permissionsCallback = new Callback() {
+ @Override
+ public void invoke(Object... args) {
+ if (permissionListener != null
+ && permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
+ permissionListener = null;
+ }
+ }
+ };
+ }
+
+ public static void requestPermissions(Activity activity, String[] permissions, int requestCode, PermissionListener listener) {
+ permissionListener = listener;
+
+ // The RN Permissions module calls this in a non-UI thread. What we observe is a crash in ViewGroup.dispatchCancelPendingInputEvents,
+ // which is called on the calling (ie, non-UI) thread. This doesn't look very safe, so try to avoid a crash by pretending the permission
+ // was denied.
+
+ try {
+ activity.requestPermissions(permissions, requestCode);
+ } catch (Exception e) {
+ JitsiMeetLogger.e(e, "Error requesting permissions");
+ onRequestPermissionsResult(requestCode, permissions, new int[0]);
+ }
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivityInterface.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivityInterface.java
new file mode 100644
index 0000000..2616594
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivityInterface.java
@@ -0,0 +1,15 @@
+package org.jitsi.meet.sdk;
+
+import androidx.core.app.ActivityCompat;
+
+import com.facebook.react.modules.core.PermissionAwareActivity;
+
+/**
+ * This interface serves as the umbrella interface that applications not using
+ * {@code JitsiMeetFragment} must implement in order to ensure full
+ * functionality.
+ */
+public interface JitsiMeetActivityInterface
+ extends ActivityCompat.OnRequestPermissionsResultCallback,
+ PermissionAwareActivity {
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetConferenceOptions.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetConferenceOptions.java
new file mode 100644
index 0000000..22406d3
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetConferenceOptions.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.URL;
+import java.util.ArrayList;
+
+
+/**
+ * This class represents the options when joining a Jitsi Meet conference. The user can create an
+ * instance by using {@link JitsiMeetConferenceOptions.Builder} and setting the desired options
+ * there.
+ *
+ * The resulting {@link JitsiMeetConferenceOptions} object is immutable and represents how the
+ * conference will be joined.
+ */
+public class JitsiMeetConferenceOptions implements Parcelable {
+ /**
+ * Server where the conference should take place.
+ */
+ private URL serverURL;
+ /**
+ * Room name.
+ */
+ private String room;
+ /**
+ * JWT token used for authentication.
+ */
+ private String token;
+
+ /**
+ * Config. See: https://github.com/jitsi/jitsi-meet/blob/master/config.js
+ */
+ private Bundle config;
+
+ /**
+ * Feature flags. See: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
+ */
+ private Bundle featureFlags;
+
+ /**
+ * USer information, to be used when no token is specified.
+ */
+ private JitsiMeetUserInfo userInfo;
+
+ public URL getServerURL() {
+ return serverURL;
+ }
+
+ public String getRoom() {
+ return room;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public Bundle getFeatureFlags() {
+ return featureFlags;
+ }
+
+ public JitsiMeetUserInfo getUserInfo() {
+ return userInfo;
+ }
+
+ /**
+ * Class used to build the immutable {@link JitsiMeetConferenceOptions} object.
+ */
+ public static class Builder {
+ private URL serverURL;
+ private String room;
+ private String token;
+
+ private Bundle config;
+ private Bundle featureFlags;
+
+ private JitsiMeetUserInfo userInfo;
+
+ public Builder() {
+ config = new Bundle();
+ featureFlags = new Bundle();
+ }
+
+ /**\
+ * Sets the server URL.
+ * @param url - {@link URL} of the server where the conference should take place.
+ * @return - The {@link Builder} object itself so the method calls can be chained.
+ */
+ public Builder setServerURL(URL url) {
+ this.serverURL = url;
+
+ return this;
+ }
+
+ /**
+ * Sets the room where the conference will take place.
+ * @param room - Name of the room.
+ * @return - The {@link Builder} object itself so the method calls can be chained.
+ */
+ public Builder setRoom(String room) {
+ this.room = room;
+
+ return this;
+ }
+
+ /**
+ * Sets the conference subject.
+ * @param subject - Subject for the conference.
+ * @return - The {@link Builder} object itself so the method calls can be chained.
+ */
+ public Builder setSubject(String subject) {
+ setConfigOverride("subject", subject);
+
+ return this;
+ }
+
+ /**
+ * Sets the JWT token to be used for authentication when joining a conference.
+ * @param token - The JWT token to be used for authentication.
+ * @return - The {@link Builder} object itself so the method calls can be chained.
+ */
+ public Builder setToken(String token) {
+ this.token = token;
+
+ return this;
+ }
+
+ /**
+ * Indicates the conference will be joined with the microphone muted.
+ * @param audioMuted - Muted indication.
+ * @return - The {@link Builder} object itself so the method calls can be chained.
+ */
+ public Builder setAudioMuted(boolean audioMuted) {
+ setConfigOverride("startWithAudioMuted", audioMuted);
+
+ return this;
+ }
+
+ /**
+ * Indicates the conference will be joined in audio-only mode. In this mode no video is
+ * sent or received.
+ * @param audioOnly - Audio-mode indicator.
+ * @return - The {@link Builder} object itself so the method calls can be chained.
+ */
+ public Builder setAudioOnly(boolean audioOnly) {
+ setConfigOverride("startAudioOnly", audioOnly);
+
+ return this;
+ }
+ /**
+ * Indicates the conference will be joined with the camera muted.
+ * @param videoMuted - Muted indication.
+ * @return - The {@link Builder} object itself so the method calls can be chained.
+ */
+ public Builder setVideoMuted(boolean videoMuted) {
+ setConfigOverride("startWithVideoMuted", videoMuted);
+
+ return this;
+ }
+
+ public Builder setFeatureFlag(String flag, boolean value) {
+ this.featureFlags.putBoolean(flag, value);
+
+ return this;
+ }
+
+ public Builder setFeatureFlag(String flag, String value) {
+ this.featureFlags.putString(flag, value);
+
+ return this;
+ }
+
+ public Builder setFeatureFlag(String flag, int value) {
+ this.featureFlags.putInt(flag, value);
+
+ return this;
+ }
+
+ public Builder setUserInfo(JitsiMeetUserInfo userInfo) {
+ this.userInfo = userInfo;
+
+ return this;
+ }
+
+ public Builder setConfigOverride(String config, String value) {
+ this.config.putString(config, value);
+
+ return this;
+ }
+
+ public Builder setConfigOverride(String config, int value) {
+ this.config.putInt(config, value);
+
+ return this;
+ }
+
+ public Builder setConfigOverride(String config, boolean value) {
+ this.config.putBoolean(config, value);
+
+ return this;
+ }
+
+ public Builder setConfigOverride(String config, Bundle bundle) {
+ this.config.putBundle(config, bundle);
+
+ return this;
+ }
+
+ public Builder setConfigOverride(String config, String[] list) {
+ this.config.putStringArray(config, list);
+
+ return this;
+ }
+
+ public Builder setConfigOverride(String config, ArrayList arrayList) {
+ this.config.putParcelableArrayList(config, arrayList);
+
+ return this;
+ }
+
+ /**
+ * Builds the immutable {@link JitsiMeetConferenceOptions} object with the configuration
+ * that this {@link Builder} instance specified.
+ * @return - The built {@link JitsiMeetConferenceOptions} object.
+ */
+ public JitsiMeetConferenceOptions build() {
+ JitsiMeetConferenceOptions options = new JitsiMeetConferenceOptions();
+
+ options.serverURL = this.serverURL;
+ options.room = this.room;
+ options.token = this.token;
+ options.config = this.config;
+ options.featureFlags = this.featureFlags;
+ options.userInfo = this.userInfo;
+
+ return options;
+ }
+ }
+
+ private JitsiMeetConferenceOptions() {
+ }
+
+ private JitsiMeetConferenceOptions(Parcel in) {
+ serverURL = (URL) in.readSerializable();
+ room = in.readString();
+ token = in.readString();
+ config = in.readBundle();
+ featureFlags = in.readBundle();
+ userInfo = new JitsiMeetUserInfo(in.readBundle());
+ }
+
+ Bundle asProps() {
+ Bundle props = new Bundle();
+
+ props.putBundle("flags", featureFlags);
+
+ Bundle urlProps = new Bundle();
+
+ // The room is fully qualified
+ if (room != null && room.contains("://")) {
+ urlProps.putString("url", room);
+ } else {
+ if (serverURL != null) {
+ urlProps.putString("serverURL", serverURL.toString());
+ }
+ if (room != null) {
+ urlProps.putString("room", room);
+ }
+ }
+
+ if (token != null) {
+ urlProps.putString("jwt", token);
+ }
+
+ if (userInfo != null) {
+ props.putBundle("userInfo", userInfo.asBundle());
+ }
+
+ urlProps.putBundle("config", config);
+ props.putBundle("url", urlProps);
+
+ return props;
+ }
+
+ // Parcelable interface
+ //
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public JitsiMeetConferenceOptions createFromParcel(Parcel in) {
+ return new JitsiMeetConferenceOptions(in);
+ }
+
+ @Override
+ public JitsiMeetConferenceOptions[] newArray(int size) {
+ return new JitsiMeetConferenceOptions[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeSerializable(serverURL);
+ dest.writeString(room);
+ dest.writeString(token);
+ dest.writeBundle(config);
+ dest.writeBundle(featureFlags);
+ dest.writeBundle(userInfo != null ? userInfo.asBundle() : new Bundle());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
new file mode 100644
index 0000000..f2ed6c6
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import static android.Manifest.permission.POST_NOTIFICATIONS;
+import static android.Manifest.permission.RECORD_AUDIO;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import com.facebook.react.modules.core.PermissionListener;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * This class implements an Android {@link Service}, a foreground one specifically, and it's
+ * responsible for presenting an ongoing notification when a conference is in progress.
+ * The service will help keep the app running while in the background.
+ *
+ * See: https://developer.android.com/guide/components/services
+ */
+public class JitsiMeetOngoingConferenceService extends Service implements OngoingConferenceTracker.OngoingConferenceListener {
+ private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
+ private static final String ACTIVITY_DATA_KEY = "activityDataKey";
+ private static final String EXTRA_DATA_KEY = "extraDataKey";
+ private static final String EXTRA_DATA_BUNDLE_KEY = "extraDataBundleKey";
+ private static final String IS_AUDIO_MUTED_KEY = "isAudioMuted";
+
+ private static final int PERMISSIONS_REQUEST_CODE = (int) (Math.random() * Short.MAX_VALUE);
+
+ private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver();
+
+ private boolean isAudioMuted;
+ private Class tapBackActivity;
+
+ static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
+
+ private static void doLaunch(Context context, HashMap extraData) {
+ Activity activity = (Activity) context;
+
+ OngoingNotification.createNotificationChannel(activity);
+
+ Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
+
+ Bundle extraDataBundle = new Bundle();
+ extraDataBundle.putSerializable(EXTRA_DATA_KEY, extraData);
+
+ intent.putExtra(EXTRA_DATA_BUNDLE_KEY, extraDataBundle);
+ intent.putExtra(ACTIVITY_DATA_KEY, activity.getClass().getCanonicalName());
+
+ ComponentName componentName;
+
+ try {
+ componentName = context.startForegroundService(intent);
+ } catch (RuntimeException e) {
+ // Avoid crashing due to ForegroundServiceStartNotAllowedException (API level 31).
+ // See: https://developer.android.com/guide/components/foreground-services#background-start-restrictions
+ JitsiMeetLogger.w(TAG + " Ongoing conference service not started", e);
+ return;
+ }
+
+ if (componentName == null) {
+ JitsiMeetLogger.w(TAG + " Ongoing conference service not started");
+ }
+ }
+
+
+ public static void launch(Context context, HashMap extraData) {
+ List permissionsList = new ArrayList<>();
+
+ PermissionListener listener = new PermissionListener() {
+ @Override
+ public boolean onRequestPermissionsResult(int i, String[] strings, int[] results) {
+ int counter = 0;
+
+ if (results.length > 0) {
+ for (int result : results) {
+ if (result == PackageManager.PERMISSION_GRANTED) {
+ counter++;
+ }
+ }
+
+ if (counter == results.length){
+ doLaunch(context, extraData);
+ JitsiMeetLogger.w(TAG + " Service launched, permissions were granted");
+ } else {
+ JitsiMeetLogger.w(TAG + " Couldn't launch service, permissions were not granted");
+ }
+ }
+
+ return true;
+ }
+ };
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ permissionsList.add(POST_NOTIFICATIONS);
+ permissionsList.add(RECORD_AUDIO);
+ }
+
+ String[] permissionsArray = new String[ permissionsList.size() ];
+ permissionsArray = permissionsList.toArray( permissionsArray );
+
+ if (permissionsArray.length > 0) {
+ JitsiMeetActivityDelegate.requestPermissions(
+ (Activity) context,
+ permissionsArray,
+ PERMISSIONS_REQUEST_CODE,
+ listener
+ );
+ } else {
+ doLaunch(context, extraData);
+ JitsiMeetLogger.w(TAG + " Service launched");
+ }
+ }
+
+ public static void abort(Context context) {
+ Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
+ context.stopService(intent);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted, this, tapBackActivity);
+ if (notification == null) {
+ stopSelf();
+ JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
+ } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
+ startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
+ } else {
+ startForeground(NOTIFICATION_ID, notification);
+ }
+ }
+
+ OngoingConferenceTracker.getInstance().addListener(this);
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(BroadcastEvent.Type.AUDIO_MUTED_CHANGED.getAction());
+ LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(broadcastReceiver, intentFilter);
+ }
+
+ @Override
+ public void onDestroy() {
+ OngoingConferenceTracker.getInstance().removeListener(this);
+ LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(broadcastReceiver);
+
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ final String actionName = intent.getAction();
+ final Action action = Action.fromName(actionName);
+
+ if (action != Action.HANGUP) {
+ Boolean isAudioMuted = tryParseIsAudioMuted(intent);
+
+ if (isAudioMuted != null) {
+ this.isAudioMuted = Boolean.parseBoolean(intent.getStringExtra("muted"));
+ }
+
+ if (tapBackActivity == null) {
+ String targetActivityName = intent.getExtras().getString(ACTIVITY_DATA_KEY);
+ Class extends Activity> targetActivity = null;
+ try {
+ targetActivity = Class.forName(targetActivityName).asSubclass(Activity.class);
+ tapBackActivity = targetActivity;
+ } catch (ClassNotFoundException e) {
+ JitsiMeetLogger.w(TAG + " Could not find target Activity: " + targetActivityName);
+ }
+ }
+
+ Notification notification = OngoingNotification.buildOngoingConferenceNotification(this.isAudioMuted, this, tapBackActivity);
+ if (notification == null) {
+ stopSelf();
+ JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
+ } else {
+ NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ }
+
+ // When starting the service, there is no action passed in the intent
+ if (action != null) {
+ switch (action) {
+ case UNMUTE:
+ case MUTE:
+ Intent muteBroadcastIntent = BroadcastIntentHelper.buildSetAudioMutedIntent(action == Action.MUTE);
+ LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(muteBroadcastIntent);
+ break;
+ case HANGUP:
+ JitsiMeetLogger.i(TAG + " Hangup requested");
+
+ Intent hangupBroadcastIntent = BroadcastIntentHelper.buildHangUpIntent();
+ LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(hangupBroadcastIntent);
+
+ stopSelf();
+ break;
+ default:
+ JitsiMeetLogger.w(TAG + " Unknown action received: " + action);
+ break;
+ }
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onCurrentConferenceChanged(String conferenceUrl) {
+ if (conferenceUrl == null) {
+ stopSelf();
+ OngoingNotification.resetStartingtime();
+ JitsiMeetLogger.i(TAG + "Service stopped");
+ }
+ }
+
+ public enum Action {
+ HANGUP(TAG + ":HANGUP"),
+ MUTE(TAG + ":MUTE"),
+ UNMUTE(TAG + ":UNMUTE");
+
+ private final String name;
+
+ Action(String name) {
+ this.name = name;
+ }
+
+ public static Action fromName(String name) {
+ for (Action action : Action.values()) {
+ if (action.name.equalsIgnoreCase(name)) {
+ return action;
+ }
+ }
+ return null;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ private Boolean tryParseIsAudioMuted(Intent intent) {
+ try {
+ HashMap extraData = (HashMap) intent.getBundleExtra(EXTRA_DATA_BUNDLE_KEY).getSerializable(EXTRA_DATA_KEY);
+ return Boolean.parseBoolean((String) extraData.get(IS_AUDIO_MUTED_KEY));
+ } catch (Exception ignored) {
+ }
+ return null;
+ }
+
+ private class BroadcastReceiver extends android.content.BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Class tapBackActivity = JitsiMeetOngoingConferenceService.this.tapBackActivity;
+ isAudioMuted = Boolean.parseBoolean(intent.getStringExtra("muted"));
+ Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted, context, tapBackActivity);
+ if (notification == null) {
+ stopSelf();
+ JitsiMeetLogger.w(TAG + " Couldn't update service, notification is null");
+ } else {
+ NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(NOTIFICATION_ID, notification);
+
+ JitsiMeetLogger.i(TAG + " audio muted changed");
+ }
+ }
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetUncaughtExceptionHandler.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetUncaughtExceptionHandler.java
new file mode 100644
index 0000000..bafc02e
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetUncaughtExceptionHandler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright @ 2018-present 8x8, Inc.
+ * Copyright @ 2017-2018 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.sdk;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+class JitsiMeetUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
+ private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
+
+ public static void register() {
+ Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
+
+ JitsiMeetUncaughtExceptionHandler uncaughtExceptionHandler
+ = new JitsiMeetUncaughtExceptionHandler(defaultUncaughtExceptionHandler);
+
+ Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
+ }
+
+ private JitsiMeetUncaughtExceptionHandler(Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler) {
+ this.defaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
+ }
+
+ @Override
+ public void uncaughtException(Thread t, Throwable e) {
+ JitsiMeetLogger.e(e, this.getClass().getSimpleName() + " FATAL ERROR");
+
+ // Abort all ConnectionService ongoing calls
+ if (AudioModeModule.useConnectionService()) {
+ ConnectionService.abortConnections();
+ }
+
+ if (defaultUncaughtExceptionHandler != null) {
+ defaultUncaughtExceptionHandler.uncaughtException(t, e);
+ }
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetUserInfo.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetUserInfo.java
new file mode 100644
index 0000000..7d59909
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetUserInfo.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.os.Bundle;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * This class represents user information to be passed to {@link JitsiMeetConferenceOptions} for
+ * identifying a user.
+ */
+public class JitsiMeetUserInfo {
+ /**
+ * User's display name.
+ */
+ private String displayName;
+
+ /**
+ * User's email address.
+ */
+ private String email;
+
+ /**
+ * User's avatar URL.
+ */
+ private URL avatar;
+
+ public JitsiMeetUserInfo() {}
+
+ public JitsiMeetUserInfo(Bundle b) {
+ super();
+
+ if (b.containsKey("displayName")) {
+ displayName = b.getString("displayName");
+ }
+
+ if (b.containsKey("email")) {
+ email = b.getString("email");
+ }
+
+ if (b.containsKey("avatarURL")) {
+ String avatarURL = b.getString("avatarURL");
+ try {
+ avatar = new URL(avatarURL);
+ } catch (MalformedURLException e) {
+ }
+ }
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public URL getAvatar() {
+ return avatar;
+ }
+
+ public void setAvatar(URL avatar) {
+ this.avatar = avatar;
+ }
+
+ Bundle asBundle() {
+ Bundle b = new Bundle();
+
+ if (displayName != null) {
+ b.putString("displayName", displayName);
+ }
+
+ if (email != null) {
+ b.putString("email", email);
+ }
+
+ if (avatar != null) {
+ b.putString("avatarURL", avatar.toString());
+ }
+
+ return b;
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java
new file mode 100644
index 0000000..d107d17
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright @ 2017-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.facebook.react.ReactRootView;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+
+public class JitsiMeetView extends FrameLayout {
+
+ /**
+ * Background color. Should match the background color set in JS.
+ */
+ public static final int BACKGROUND_COLOR = 0xFF040404;
+
+ /**
+ * React Native root view.
+ */
+ private ReactRootView reactRootView;
+
+ /**
+ * Helper method to recursively merge 2 {@link Bundle} objects representing React Native props.
+ *
+ * @param a - The first {@link Bundle}.
+ * @param b - The second {@link Bundle}.
+ * @return The merged {@link Bundle} object.
+ */
+ private static Bundle mergeProps(@Nullable Bundle a, @Nullable Bundle b) {
+ Bundle result = new Bundle();
+
+ if (a == null) {
+ if (b != null) {
+ result.putAll(b);
+ }
+
+ return result;
+ }
+
+ if (b == null) {
+ result.putAll(a);
+
+ return result;
+ }
+
+ // Start by putting all of a in the result.
+ result.putAll(a);
+
+ // Iterate over each key in b and override if appropriate.
+ for (String key : b.keySet()) {
+ Object bValue = b.get(key);
+ Object aValue = a.get(key);
+ String valueType = bValue.getClass().getSimpleName();
+
+ if (valueType.contentEquals("Boolean")) {
+ result.putBoolean(key, (Boolean)bValue);
+ } else if (valueType.contentEquals("String")) {
+ result.putString(key, (String)bValue);
+ } else if (valueType.contentEquals("Integer")) {
+ result.putInt(key, (int)bValue);
+ } else if (valueType.contentEquals("Bundle")) {
+ result.putBundle(key, mergeProps((Bundle)aValue, (Bundle)bValue));
+ } else {
+ throw new RuntimeException("Unsupported type: " + valueType);
+ }
+ }
+
+ return result;
+ }
+
+ public JitsiMeetView(@NonNull Context context) {
+ super(context);
+ initialize(context);
+ }
+
+ public JitsiMeetView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize(context);
+ }
+
+ public JitsiMeetView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initialize(context);
+ }
+
+ /**
+ * Releases the React resources (specifically the {@link ReactRootView})
+ * associated with this view.
+ *
+ * MUST be called when the {@link Activity} holding this view is destroyed,
+ * typically in the {@code onDestroy} method.
+ */
+ public void dispose() {
+ if (reactRootView != null) {
+ removeView(reactRootView);
+ reactRootView.unmountReactApplication();
+ reactRootView = null;
+ }
+ }
+
+ /**
+ * Enters Picture-In-Picture mode, if possible. This method is designed to
+ * be called from the {@code Activity.onUserLeaveHint} method.
+ *
+ * This is currently not mandatory, but if used will provide automatic
+ * handling of the picture in picture mode when user minimizes the app. It
+ * will be probably the most useful in case the app is using the welcome
+ * page.
+ */
+ public void enterPictureInPicture() {
+ PictureInPictureModule pipModule
+ = ReactInstanceManagerHolder.getNativeModule(
+ PictureInPictureModule.class);
+ if (pipModule != null
+ && pipModule.isPictureInPictureSupported()
+ && !JitsiMeetActivityDelegate.arePermissionsBeingRequested()) {
+ try {
+ pipModule.enterPictureInPicture();
+ } catch (RuntimeException re) {
+ JitsiMeetLogger.e(re, "Failed to enter PiP mode");
+ }
+ }
+ }
+
+ /**
+ * Joins the conference specified by the given {@link JitsiMeetConferenceOptions}. If there is
+ * already an active conference, it will be left and the new one will be joined.
+ * @param options - Description of what conference must be joined and what options will be used
+ * when doing so.
+ */
+ public void join(@Nullable JitsiMeetConferenceOptions options) {
+ setProps(options != null ? options.asProps() : new Bundle());
+ }
+
+ /**
+ * Internal method which aborts running RN by passing empty props.
+ * This is only meant to be used from the enclosing Activity's onDestroy.
+ */
+ public void abort() {
+ setProps(new Bundle());
+ }
+
+ /**
+ * Creates the {@code ReactRootView} for the given app name with the given
+ * props. Once created it's set as the view of this {@code FrameLayout}.
+ *
+ * @param appName - The name of the "app" (in React Native terms) to load.
+ * @param props - The React Component props to pass to the app.
+ */
+ private void createReactRootView(String appName, @Nullable Bundle props) {
+ if (props == null) {
+ props = new Bundle();
+ }
+
+ if (reactRootView == null) {
+ reactRootView = new ReactRootView(getContext());
+ reactRootView.startReactApplication(
+ ReactInstanceManagerHolder.getReactInstanceManager(),
+ appName,
+ props);
+ reactRootView.setBackgroundColor(BACKGROUND_COLOR);
+ addView(reactRootView);
+ } else {
+ reactRootView.setAppProperties(props);
+ }
+ }
+
+ private void initialize(@NonNull Context context) {
+ // Check if the parent Activity implements JitsiMeetActivityInterface,
+ // otherwise things may go wrong.
+ if (!(context instanceof JitsiMeetActivityInterface)) {
+ throw new RuntimeException("Enclosing Activity must implement JitsiMeetActivityInterface");
+ }
+
+ setBackgroundColor(BACKGROUND_COLOR);
+ }
+
+ /**
+ * Helper method to set the React Native props.
+ * @param newProps - New props to be set on the React Native view.
+ */
+ private void setProps(@NonNull Bundle newProps) {
+ // Merge the default options with the newly provided ones.
+ Bundle props = mergeProps(JitsiMeet.getDefaultProps(), newProps);
+
+ // XXX The setProps() method is supposed to be imperative i.e.
+ // a second invocation with one and the same URL is expected to join
+ // the respective conference again if the first invocation was followed
+ // by leaving the conference. However, React and, respectively,
+ // appProperties/initialProperties are declarative expressions i.e. one
+ // and the same URL will not trigger an automatic re-render in the
+ // JavaScript source code. The workaround implemented below introduces
+ // "imperativeness" in React Component props by defining a unique value
+ // per setProps() invocation.
+ props.putLong("timestamp", System.currentTimeMillis());
+
+ createReactRootView("App", props);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ dispose();
+ super.onDetachedFromWindow();
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiVideoDecoderFactory.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiVideoDecoderFactory.java
new file mode 100644
index 0000000..1beaf4c
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiVideoDecoderFactory.java
@@ -0,0 +1,84 @@
+package org.jitsi.meet.sdk;
+
+/*
+ * Copyright 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+import android.media.MediaCodecInfo;
+import androidx.annotation.Nullable;
+
+import com.oney.WebRTCModule.webrtcutils.SoftwareVideoDecoderFactoryProxy;
+
+import org.webrtc.EglBase;
+import org.webrtc.HardwareVideoDecoderFactory;
+import org.webrtc.JitsiPlatformVideoDecoderFactory;
+import org.webrtc.Predicate;
+import org.webrtc.VideoCodecInfo;
+import org.webrtc.VideoDecoder;
+import org.webrtc.VideoDecoderFactory;
+import org.webrtc.VideoDecoderFallback;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+
+/**
+ * Custom decoder factory which uses HW decoders and falls back to SW.
+ */
+public class JitsiVideoDecoderFactory implements VideoDecoderFactory {
+ private final VideoDecoderFactory hardwareVideoDecoderFactory;
+ private final VideoDecoderFactory softwareVideoDecoderFactory = new SoftwareVideoDecoderFactoryProxy();
+ private final VideoDecoderFactory platformSoftwareVideoDecoderFactory;
+
+ /**
+ * Predicate to filter out the AV1 hardware decoder, as we've seen decoding issues with it.
+ */
+ private static final String GOOGLE_AV1_DECODER = "c2.google.av1";
+ private static final Predicate hwCodecPredicate = arg -> {
+ // Filter out the Google AV1 codec.
+ return !arg.getName().startsWith(GOOGLE_AV1_DECODER);
+ };
+ private static final Predicate swCodecPredicate = arg -> {
+ // Noop, just making sure we can customize it easily if needed.
+ return true;
+ };
+
+ /**
+ * Create decoder factory using default hardware decoder factory.
+ */
+ public JitsiVideoDecoderFactory(@Nullable EglBase.Context eglContext) {
+ this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext, hwCodecPredicate);
+ this.platformSoftwareVideoDecoderFactory = new JitsiPlatformVideoDecoderFactory(eglContext, swCodecPredicate);
+ }
+
+ @Override
+ public @Nullable VideoDecoder createDecoder(VideoCodecInfo codecType) {
+ VideoDecoder softwareDecoder = softwareVideoDecoderFactory.createDecoder(codecType);
+ final VideoDecoder hardwareDecoder = hardwareVideoDecoderFactory.createDecoder(codecType);
+ if (softwareDecoder == null) {
+ softwareDecoder = platformSoftwareVideoDecoderFactory.createDecoder(codecType);
+ }
+ if (hardwareDecoder != null && softwareDecoder != null) {
+ // Both hardware and software supported, wrap it in a software fallback
+ return new VideoDecoderFallback(
+ /* fallback= */ softwareDecoder, /* primary= */ hardwareDecoder);
+ }
+ return hardwareDecoder != null ? hardwareDecoder : softwareDecoder;
+ }
+
+ @Override
+ public VideoCodecInfo[] getSupportedCodecs() {
+ LinkedHashSet supportedCodecInfos = new LinkedHashSet<>();
+
+ supportedCodecInfos.addAll(Arrays.asList(softwareVideoDecoderFactory.getSupportedCodecs()));
+ supportedCodecInfos.addAll(Arrays.asList(hardwareVideoDecoderFactory.getSupportedCodecs()));
+ supportedCodecInfos.addAll(Arrays.asList(platformSoftwareVideoDecoderFactory.getSupportedCodecs()));
+
+ return supportedCodecInfos.toArray(new VideoCodecInfo[supportedCodecInfos.size()]);
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiVideoEncoderFactory.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiVideoEncoderFactory.java
new file mode 100644
index 0000000..5d075ab
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiVideoEncoderFactory.java
@@ -0,0 +1,16 @@
+package org.jitsi.meet.sdk;
+
+import androidx.annotation.Nullable;
+
+import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
+
+import org.webrtc.EglBase;
+
+/**
+ * Custom encoder factory which uses HW for H.264 and SW for everything else.
+ */
+public class JitsiVideoEncoderFactory extends H264AndSoftwareVideoEncoderFactory {
+ public JitsiVideoEncoderFactory(@Nullable EglBase.Context eglContext) {
+ super(eglContext);
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/LocaleDetector.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/LocaleDetector.java
new file mode 100644
index 0000000..73b5a86
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/LocaleDetector.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright @ 2018-present 8x8, Inc.
+ *
+ * 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.
+ */
+
+/*
+ * Based on https://github.com/DylanVann/react-native-locale-detector
+ */
+
+package org.jitsi.meet.sdk;
+
+import android.content.Context;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Module which provides information about the system locale.
+ */
+class LocaleDetector extends ReactContextBaseJavaModule {
+
+ public LocaleDetector(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ /**
+ * Gets a {@code Map} of constants this module exports to JS. Supports JSON
+ * types.
+ *
+ * @return a {@link Map} of constants this module exports to JS
+ */
+ @Override
+ public Map getConstants() {
+ Context context = getReactApplicationContext();
+ HashMap constants = new HashMap<>();
+ constants.put("locale", context.getResources().getConfiguration().locale.toLanguageTag());
+ return constants;
+ }
+
+ @Override
+ public String getName() {
+ return "LocaleDetector";
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/LogBridgeModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/LogBridgeModule.java
new file mode 100644
index 0000000..2b7d349
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/LogBridgeModule.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.module.annotations.ReactModule;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Module implementing a "bridge" between the JS loggers and the native one.
+ */
+@ReactModule(name = LogBridgeModule.NAME)
+class LogBridgeModule extends ReactContextBaseJavaModule {
+ public static final String NAME = "LogBridge";
+
+ public LogBridgeModule(@Nonnull ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @ReactMethod
+ public void trace(final String message) {
+ JitsiMeetLogger.v(message);
+ }
+
+ @ReactMethod
+ public void debug(final String message) {
+ JitsiMeetLogger.d(message);
+ }
+
+ @ReactMethod
+ public void info(final String message) {
+ JitsiMeetLogger.i(message);
+ }
+
+ @ReactMethod
+ public void log(final String message) {
+ JitsiMeetLogger.i(message);
+ }
+
+ @ReactMethod
+ public void warn(final String message) {
+ JitsiMeetLogger.w(message);
+ }
+
+ @ReactMethod
+ public void error(final String message) {
+ JitsiMeetLogger.e(message);
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingConferenceTracker.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingConferenceTracker.java
new file mode 100644
index 0000000..f603d67
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingConferenceTracker.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import com.facebook.react.bridge.ReadableMap;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+
+/**
+ * Helper class to keep track of what the current conference is.
+ */
+class OngoingConferenceTracker {
+ private static final OngoingConferenceTracker instance = new OngoingConferenceTracker();
+
+ private static final String CONFERENCE_WILL_JOIN = "CONFERENCE_WILL_JOIN";
+ private static final String CONFERENCE_TERMINATED = "CONFERENCE_TERMINATED";
+
+ private final Collection listeners =
+ Collections.synchronizedSet(new HashSet());
+ private String currentConference;
+
+ public OngoingConferenceTracker() {
+ }
+
+ public static OngoingConferenceTracker getInstance() {
+ return instance;
+ }
+
+ /**
+ * Gets the current active conference URL.
+ *
+ * @return - The current conference URL as a String.
+ */
+ synchronized String getCurrentConference() {
+ return currentConference;
+ }
+
+ synchronized void onExternalAPIEvent(String name, ReadableMap data) {
+ if (!data.hasKey("url")) {
+ return;
+ }
+
+ String url = data.getString("url");
+ if (url == null) {
+ return;
+ }
+
+ switch(name) {
+ case CONFERENCE_WILL_JOIN:
+ currentConference = url;
+ updateListeners();
+ break;
+
+ case CONFERENCE_TERMINATED:
+ if (url.equals(currentConference)) {
+ currentConference = null;
+ updateListeners();
+ }
+ break;
+ }
+ }
+
+ void addListener(OngoingConferenceListener listener) {
+ listeners.add(listener);
+ }
+
+ void removeListener(OngoingConferenceListener listener) {
+ listeners.remove(listener);
+ }
+
+ private void updateListeners() {
+ synchronized (listeners) {
+ for (OngoingConferenceListener listener : listeners) {
+ listener.onCurrentConferenceChanged(currentConference);
+ }
+ }
+ }
+
+ public interface OngoingConferenceListener {
+ void onCurrentConferenceChanged(String conferenceUrl);
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingNotification.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingNotification.java
new file mode 100644
index 0000000..63484f9
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingNotification.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+import android.app.Activity;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.StringRes;
+import androidx.core.app.NotificationCompat;
+
+
+/**
+ * Helper class for creating the ongoing notification which is used with
+ * {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
+ * and to hangup from within the notification itself.
+ */
+class OngoingNotification {
+ private static final String TAG = OngoingNotification.class.getSimpleName();
+
+ private static long startingTime = 0;
+
+ static final String ONGOING_CONFERENCE_CHANNEL_ID = "JitsiOngoingConferenceChannel";
+
+ static void createNotificationChannel(Activity context) {
+ if (context == null) {
+ JitsiMeetLogger.w(TAG + " Cannot create notification channel: no current context");
+ return;
+ }
+
+ NotificationManager notificationManager
+ = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ NotificationChannel channel
+ = notificationManager.getNotificationChannel(ONGOING_CONFERENCE_CHANNEL_ID);
+
+ if (channel != null) {
+ // The channel was already created, no need to do it again.
+ return;
+ }
+
+ channel = new NotificationChannel(ONGOING_CONFERENCE_CHANNEL_ID, context.getString(R.string.ongoing_notification_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
+ channel.enableLights(false);
+ channel.enableVibration(false);
+ channel.setShowBadge(false);
+
+ notificationManager.createNotificationChannel(channel);
+ }
+
+ static Notification buildOngoingConferenceNotification(Boolean isMuted, Context context, Class tapBackActivity) {
+ if (context == null) {
+ JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
+ return null;
+ }
+
+ Intent notificationIntent = new Intent(context, tapBackActivity == null ? context.getClass() : tapBackActivity);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, ONGOING_CONFERENCE_CHANNEL_ID);
+
+ if (startingTime == 0) {
+ startingTime = System.currentTimeMillis();
+ }
+
+ builder
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setContentTitle(context.getString(R.string.ongoing_notification_title))
+ .setContentText(context.getString(R.string.ongoing_notification_text))
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .setWhen(startingTime)
+ .setUsesChronometer(true)
+ .setAutoCancel(false)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setOnlyAlertOnce(true)
+ .setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
+
+ NotificationCompat.Action hangupAction = createAction(context, JitsiMeetOngoingConferenceService.Action.HANGUP, R.string.ongoing_notification_action_hang_up);
+
+ JitsiMeetOngoingConferenceService.Action toggleAudioAction = isMuted
+ ? JitsiMeetOngoingConferenceService.Action.UNMUTE : JitsiMeetOngoingConferenceService.Action.MUTE;
+ int toggleAudioTitle = isMuted ? R.string.ongoing_notification_action_unmute : R.string.ongoing_notification_action_mute;
+ NotificationCompat.Action audioAction = createAction(context, toggleAudioAction, toggleAudioTitle);
+
+ builder.addAction(hangupAction);
+ builder.addAction(audioAction);
+
+ return builder.build();
+ }
+
+ static void resetStartingtime() {
+ startingTime = 0;
+ }
+
+ private static NotificationCompat.Action createAction(Context context, JitsiMeetOngoingConferenceService.Action action, @StringRes int titleId) {
+ Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
+ intent.setAction(action.getName());
+ PendingIntent pendingIntent
+ = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
+ String title = context.getString(titleId);
+ return new NotificationCompat.Action(0, title, pendingIntent);
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ParticipantInfo.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ParticipantInfo.java
new file mode 100644
index 0000000..ad95159
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ParticipantInfo.java
@@ -0,0 +1,27 @@
+package org.jitsi.meet.sdk;
+
+import com.google.gson.annotations.SerializedName;
+
+public class ParticipantInfo {
+
+ @SerializedName("participantId")
+ public String id;
+
+ @SerializedName("displayName")
+ public String displayName;
+
+ @SerializedName("avatarUrl")
+ public String avatarUrl;
+
+ @SerializedName("email")
+ public String email;
+
+ @SerializedName("name")
+ public String name;
+
+ @SerializedName("isLocal")
+ public boolean isLocal;
+
+ @SerializedName("role")
+ public String role;
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ParticipantsService.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ParticipantsService.java
new file mode 100644
index 0000000..667f538
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ParticipantsService.java
@@ -0,0 +1,90 @@
+package org.jitsi.meet.sdk;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+public class ParticipantsService extends android.content.BroadcastReceiver {
+
+ private static final String TAG = ParticipantsService.class.getSimpleName();
+ private static final String REQUEST_ID = "requestId";
+
+ private final Map> participantsInfoCallbackMap = new HashMap<>();
+
+ private static ParticipantsService instance;
+
+ @Nullable
+ public static ParticipantsService getInstance() {
+ return instance;
+ }
+
+ private ParticipantsService(Context context) {
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(BroadcastEvent.Type.PARTICIPANTS_INFO_RETRIEVED.getAction());
+ localBroadcastManager.registerReceiver(this, intentFilter);
+ }
+
+ static void init(Context context) {
+ instance = new ParticipantsService(context);
+ }
+
+ public void retrieveParticipantsInfo(ParticipantsInfoCallback participantsInfoCallback) {
+ String callbackKey = UUID.randomUUID().toString();
+ this.participantsInfoCallbackMap.put(callbackKey, new WeakReference<>(participantsInfoCallback));
+
+ String actionName = BroadcastAction.Type.RETRIEVE_PARTICIPANTS_INFO.getAction();
+ WritableMap data = Arguments.createMap();
+ data.putString(REQUEST_ID, callbackKey);
+ ReactInstanceManagerHolder.emitEvent(actionName, data);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ BroadcastEvent event = new BroadcastEvent(intent);
+
+ switch (event.getType()) {
+ case PARTICIPANTS_INFO_RETRIEVED:
+ try {
+ List participantInfoList = new Gson().fromJson(
+ event.getData().get("participantsInfo").toString(),
+ new TypeToken>() {
+ }.getType());
+
+ ParticipantsInfoCallback participantsInfoCallback = this.participantsInfoCallbackMap.get(event.getData().get(REQUEST_ID).toString()).get();
+
+ if (participantsInfoCallback != null) {
+ participantsInfoCallback.onReceived(participantInfoList);
+ this.participantsInfoCallbackMap.remove(participantsInfoCallback);
+ }
+ } catch (Exception e) {
+ JitsiMeetLogger.w(TAG + "error parsing participantsList", e);
+ }
+
+ break;
+ }
+ }
+
+ public interface ParticipantsInfoCallback {
+ void onReceived(List participantInfoList);
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java
new file mode 100644
index 0000000..4a95846
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright @ 2017-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.PictureInPictureParams;
+import android.util.Rational;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.module.annotations.ReactModule;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static android.content.Context.ACTIVITY_SERVICE;
+
+@ReactModule(name = PictureInPictureModule.NAME)
+class PictureInPictureModule extends ReactContextBaseJavaModule {
+
+ public static final String NAME = "PictureInPicture";
+ private static final String TAG = NAME;
+
+ private static boolean isSupported;
+ private boolean isEnabled;
+
+ public PictureInPictureModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+
+ ActivityManager am = (ActivityManager) reactContext.getSystemService(ACTIVITY_SERVICE);
+
+ // Android Go devices don't support PiP. There doesn't seem to be a better way to detect it than
+ // to use ActivityManager.isLowRamDevice().
+ // https://stackoverflow.com/questions/58340558/how-to-detect-android-go
+ isSupported = !am.isLowRamDevice();
+ }
+
+ /**
+ * Gets a {@code Map} of constants this module exports to JS. Supports JSON
+ * types.
+ *
+ * @return a {@link Map} of constants this module exports to JS
+ */
+ @Override
+ public Map getConstants() {
+ Map constants = new HashMap<>();
+ constants.put("SUPPORTED", isSupported);
+ return constants;
+ }
+
+ /**
+ * Enters Picture-in-Picture (mode) for the current {@link Activity}.
+ * Supported on Android API >= 26 (Oreo) only.
+ *
+ * @throws IllegalStateException if {@link #isPictureInPictureSupported()}
+ * returns {@code false} or if {@link #getCurrentActivity()} returns
+ * {@code null}.
+ * @throws RuntimeException if
+ * {@link Activity#enterPictureInPictureMode(PictureInPictureParams)} fails.
+ * That method can also throw a {@link RuntimeException} in various cases,
+ * including when the activity is not visible (paused or stopped), if the
+ * screen is locked or if the user has an activity pinned.
+ */
+ public void enterPictureInPicture() {
+ if (!isEnabled) {
+ return;
+ }
+
+ if (!isSupported) {
+ throw new IllegalStateException("Picture-in-Picture not supported");
+ }
+
+ Activity currentActivity = getCurrentActivity();
+
+ if (currentActivity == null) {
+ throw new IllegalStateException("No current Activity!");
+ }
+
+ JitsiMeetLogger.i(TAG + " Entering Picture-in-Picture");
+
+ PictureInPictureParams.Builder builder
+ = new PictureInPictureParams.Builder()
+ .setAspectRatio(new Rational(1, 1));
+
+ // https://developer.android.com/reference/android/app/Activity.html#enterPictureInPictureMode(android.app.PictureInPictureParams)
+ //
+ // The system may disallow entering picture-in-picture in various cases,
+ // including when the activity is not visible, if the screen is locked
+ // or if the user has an activity pinned.
+ if (!currentActivity.enterPictureInPictureMode(builder.build())) {
+ throw new RuntimeException("Failed to enter Picture-in-Picture");
+ }
+ }
+
+ /**
+ * Enters Picture-in-Picture (mode) for the current {@link Activity}.
+ * Supported on Android API >= 26 (Oreo) only.
+ *
+ * @param promise a {@code Promise} which will resolve with a {@code null}
+ * value upon success, and an {@link Exception} otherwise.
+ */
+ @ReactMethod
+ public void enterPictureInPicture(Promise promise) {
+ try {
+ enterPictureInPicture();
+ promise.resolve(null);
+ } catch (RuntimeException re) {
+ promise.reject(re);
+ }
+ }
+
+ @ReactMethod
+ public void setPictureInPictureEnabled(Boolean enabled) {
+ this.isEnabled = enabled;
+ }
+
+ public boolean isPictureInPictureSupported() {
+ return isSupported;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ProximityModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ProximityModule.java
new file mode 100644
index 0000000..68e4354
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ProximityModule.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright @ 2017-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.content.Context;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.UiThreadUtil;
+import com.facebook.react.module.annotations.ReactModule;
+
+/**
+ * Module implementing a simple API to enable a proximity sensor-controlled
+ * wake lock. When the lock is held, if the proximity sensor detects a nearby
+ * object it will dim the screen and disable touch controls. The functionality
+ * is used with the conference audio-only mode.
+ */
+@ReactModule(name = ProximityModule.NAME)
+class ProximityModule extends ReactContextBaseJavaModule {
+
+ public static final String NAME = "Proximity";
+
+ /**
+ * {@link WakeLock} instance.
+ */
+ private final WakeLock wakeLock;
+
+ /**
+ * Initializes a new module instance. There shall be a single instance of
+ * this module throughout the lifetime of the application.
+ *
+ * @param reactContext The {@link ReactApplicationContext} where this module
+ * is created.
+ */
+ public ProximityModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+
+ WakeLock wakeLock;
+ PowerManager powerManager
+ = (PowerManager)
+ reactContext.getSystemService(Context.POWER_SERVICE);
+
+ try {
+ wakeLock
+ = powerManager.newWakeLock(
+ PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
+ "jitsi:"+NAME);
+ } catch (Throwable ignored) {
+ wakeLock = null;
+ }
+
+ this.wakeLock = wakeLock;
+ }
+
+ /**
+ * Gets the name of this module to be used in the React Native bridge.
+ *
+ * @return The name of this module to be used in the React Native bridge.
+ */
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ /**
+ * Acquires / releases the proximity sensor wake lock.
+ *
+ * @param enabled {@code true} to enable the proximity sensor; otherwise,
+ * {@code false}.
+ */
+ @ReactMethod
+ public void setEnabled(final boolean enabled) {
+ if (wakeLock == null) {
+ return;
+ }
+
+ UiThreadUtil.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (enabled) {
+ if (!wakeLock.isHeld()) {
+ wakeLock.acquire();
+ }
+ } else if (wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ }
+ });
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/RNConnectionService.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/RNConnectionService.java
new file mode 100644
index 0000000..24451e7
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/RNConnectionService.java
@@ -0,0 +1,255 @@
+package org.jitsi.meet.sdk;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.module.annotations.ReactModule;
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+/**
+ * The react-native side of Jitsi Meet's {@link ConnectionService}. Exposes
+ * the Java Script API.
+ *
+ * @author Pawel Domas
+ */
+@ReactModule(name = RNConnectionService.NAME)
+class RNConnectionService extends ReactContextBaseJavaModule {
+
+ public static final String NAME = "ConnectionService";
+
+ private static final String TAG = ConnectionService.TAG;
+
+ private static RNConnectionService sRNConnectionServiceInstance;
+ /**
+ * Handler for dealing with call state changes. We are acting as a proxy between ConnectionService
+ * and other modules such as {@link AudioModeModule}.
+ */
+ private CallAudioStateListener callAudioStateListener;
+
+ /**
+ * Sets the audio route on all existing {@link android.telecom.Connection}s
+ *
+ * @param audioRoute the new audio route to be set. See
+ * {@link android.telecom.CallAudioState} constants prefixed with "ROUTE_".
+ */
+ static void setAudioRoute(int audioRoute) {
+ for (ConnectionService.ConnectionImpl c
+ : ConnectionService.getConnections()) {
+ c.setAudioRoute(audioRoute);
+ }
+ }
+
+ RNConnectionService(ReactApplicationContext reactContext) {
+ super(reactContext);
+ sRNConnectionServiceInstance = this;
+ }
+
+ static RNConnectionService getInstance() {
+ return sRNConnectionServiceInstance;
+ }
+
+ @ReactMethod
+ public void addListener(String eventName) {
+ // Keep: Required for RN built in Event Emitter Calls.
+ }
+
+ @ReactMethod
+ public void removeListeners(Integer count) {
+ // Keep: Required for RN built in Event Emitter Calls.
+ }
+
+ /**
+ * Starts a new outgoing call.
+ *
+ * @param callUUID - unique call identifier assigned by Jitsi Meet to
+ * a conference call.
+ * @param handle - a call handle which by default is Jitsi Meet room's URL.
+ * @param hasVideo - whether or not user starts with the video turned on.
+ * @param promise - the Promise instance passed by the React-native bridge,
+ * so that this method returns a Promise on the JS side.
+ *
+ * NOTE regarding the "missingPermission" suppress - SecurityException will
+ * be handled as part of the Exception try catch block and the Promise will
+ * be rejected.
+ */
+ @SuppressLint("MissingPermission")
+ @ReactMethod
+ public void startCall(
+ String callUUID,
+ String handle,
+ boolean hasVideo,
+ Promise promise) {
+ JitsiMeetLogger.d("%s startCall UUID=%s, h=%s, v=%s",
+ TAG,
+ callUUID,
+ handle,
+ hasVideo);
+
+ ReactApplicationContext ctx = getReactApplicationContext();
+
+ Uri address = Uri.fromParts(PhoneAccount.SCHEME_SIP, handle, null);
+ PhoneAccountHandle accountHandle;
+
+ try {
+ accountHandle
+ = ConnectionService.registerPhoneAccount(getReactApplicationContext(), address, callUUID);
+ } catch (Throwable tr) {
+ JitsiMeetLogger.e(tr, TAG + " error in startCall");
+
+ promise.reject(tr);
+ return;
+ }
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(
+ TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ accountHandle);
+ extras.putInt(
+ TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ hasVideo
+ ? VideoProfile.STATE_BIDIRECTIONAL
+ : VideoProfile.STATE_AUDIO_ONLY);
+
+ ConnectionService.registerStartCallPromise(callUUID, promise);
+
+ TelecomManager tm = null;
+
+ try {
+ tm = (TelecomManager) ctx.getSystemService(Context.TELECOM_SERVICE);
+ tm.placeCall(address, extras);
+ } catch (Throwable tr) {
+ JitsiMeetLogger.e(tr, TAG + " error in startCall");
+ if (tm != null) {
+ try {
+ tm.unregisterPhoneAccount(accountHandle);
+ } catch (Throwable tr1) {
+ // UnsupportedOperationException: System does not support feature android.software.connectionservice
+ // was observed here. Ignore.
+ }
+ }
+ ConnectionService.unregisterStartCallPromise(callUUID);
+ promise.reject(tr);
+ }
+ }
+
+ /**
+ * Called by the JS side of things to mark the call as failed.
+ *
+ * @param callUUID - the call's UUID.
+ */
+ @ReactMethod
+ public void reportCallFailed(String callUUID) {
+ JitsiMeetLogger.d(TAG + " reportCallFailed " + callUUID);
+ ConnectionService.setConnectionDisconnected(
+ callUUID,
+ new DisconnectCause(DisconnectCause.ERROR));
+ }
+
+ /**
+ * Called by the JS side of things to mark the call as disconnected.
+ *
+ * @param callUUID - the call's UUID.
+ */
+ @ReactMethod
+ public void endCall(String callUUID) {
+ JitsiMeetLogger.d(TAG + " endCall " + callUUID);
+ ConnectionService.setConnectionDisconnected(
+ callUUID,
+ new DisconnectCause(DisconnectCause.LOCAL));
+ }
+
+ /**
+ * Called by the JS side of things to mark the call as active.
+ *
+ * @param callUUID - the call's UUID.
+ */
+ @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.");
+ }
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ /**
+ * Called by the JS side to update the call's state.
+ *
+ * @param callUUID - the call's UUID.
+ * @param callState - the map which carries info about the current call's
+ * state. See static fields in {@link ConnectionService.ConnectionImpl}
+ * prefixed with "KEY_" for the values supported by the Android
+ * implementation.
+ */
+ @ReactMethod
+ public void updateCall(String callUUID, ReadableMap callState) {
+ ConnectionService.updateCall(callUUID, callState);
+ }
+
+ public CallAudioStateListener getCallAudioStateListener() {
+ return callAudioStateListener;
+ }
+
+ public void setCallAudioStateListener(CallAudioStateListener callAudioStateListener) {
+ this.callAudioStateListener = callAudioStateListener;
+ }
+
+ /**
+ * Handler for call state changes. {@code ConnectionServiceImpl} will call this handler when the
+ * call audio state changes.
+ *
+ * @param callAudioState The current call's audio state.
+ */
+ void onCallAudioStateChange(android.telecom.CallAudioState callAudioState) {
+ if (callAudioStateListener != null) {
+ callAudioStateListener.onCallAudioStateChange(callAudioState);
+ }
+ }
+
+ interface CallAudioStateListener {
+ void onCallAudioStateChange(android.telecom.CallAudioState callAudioState);
+ }
+
+ /**
+ * Helper function to send an event to JavaScript.
+ *
+ * @param eventName {@code String} containing the event name.
+ * @param data {@code Object} optional ancillary data for the event.
+ */
+ void emitEvent(
+ String eventName,
+ @Nullable Object data) {
+ ReactContext reactContext = getReactApplicationContext();
+
+ if (reactContext != null) {
+ reactContext
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit(eventName, data);
+ }
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
new file mode 100644
index 0000000..ee730ed
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright @ 2017-present 8x8, Inc.
+ *
+ * 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.sdk;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Application;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
+import com.facebook.react.ReactInstanceManager;
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.common.LifecycleState;
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+import com.facebook.react.uimanager.ViewManager;
+import com.oney.WebRTCModule.EglUtils;
+import com.oney.WebRTCModule.WebRTCModuleOptions;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+import org.webrtc.EglBase;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+class ReactInstanceManagerHolder {
+ private static final String TAG = ReactInstanceManagerHolder.class.getSimpleName();
+
+ /**
+ * FIXME (from linter): Do not place Android context classes in static
+ * fields (static reference to ReactInstanceManager which has field
+ * mApplicationContext pointing to Context); this is a memory leak (and
+ * also breaks Instant Run).
+ *
+ * React Native bridge. The instance manager allows embedding applications
+ * to create multiple root views off the same JavaScript bundle.
+ */
+ private static ReactInstanceManager reactInstanceManager;
+
+ private static List createNativeModules(ReactApplicationContext reactContext) {
+ List nativeModules
+ = new ArrayList<>(Arrays.asList(
+ new AndroidSettingsModule(reactContext),
+ new AppInfoModule(reactContext),
+ new AudioModeModule(reactContext),
+ new DropboxModule(reactContext),
+ new ExternalAPIModule(reactContext),
+ new JavaScriptSandboxModule(reactContext),
+ new LocaleDetector(reactContext),
+ new LogBridgeModule(reactContext),
+ new PictureInPictureModule(reactContext),
+ new ProximityModule(reactContext),
+ new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)));
+
+ if (AudioModeModule.useConnectionService()) {
+ nativeModules.add(new RNConnectionService(reactContext));
+ }
+
+ return nativeModules;
+ }
+
+ private static List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+
+ static List getReactNativePackages() {
+ List packages
+ = new ArrayList<>(Arrays.asList(
+ new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
+ new com.ocetnik.timer.BackgroundTimerPackage(),
+ new com.calendarevents.RNCalendarEventsPackage(),
+ new com.sayem.keepawake.KCKeepAwakePackage(),
+ new com.facebook.react.shell.MainReactPackage(),
+ new com.reactnativecommunity.clipboard.ClipboardPackage(),
+ new com.reactnativecommunity.netinfo.NetInfoPackage(),
+ new com.reactnativepagerview.PagerViewPackage(),
+ new com.oblador.performance.PerformancePackage(),
+ new com.reactnativecommunity.slider.ReactSliderPackage(),
+ new com.brentvatne.react.ReactVideoPackage(),
+ new com.reactnativecommunity.webview.RNCWebViewPackage(),
+ new com.kevinresol.react_native_default_preference.RNDefaultPreferencePackage(),
+ new com.learnium.RNDeviceInfo.RNDeviceInfo(),
+ new com.oney.WebRTCModule.WebRTCModulePackage(),
+ new com.swmansion.gesturehandler.RNGestureHandlerPackage(),
+ new org.linusu.RNGetRandomValuesPackage(),
+ new com.rnimmersivemode.RNImmersiveModePackage(),
+ new com.swmansion.rnscreens.RNScreensPackage(),
+ new com.zmxv.RNSound.RNSoundPackage(),
+ new com.th3rdwave.safeareacontext.SafeAreaContextPackage(),
+ new com.horcrux.svg.SvgPackage(),
+ new org.wonday.orientation.OrientationPackage(),
+ new com.splashview.SplashViewPackage(),
+ new ReactPackageAdapter() {
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ return ReactInstanceManagerHolder.createNativeModules(reactContext);
+ }
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return ReactInstanceManagerHolder.createViewManagers(reactContext);
+ }
+ }));
+
+ // AmplitudeReactNativePackage
+ try {
+ Class> amplitudePackageClass = Class.forName("com.amplitude.reactnative.AmplitudeReactNativePackage");
+ Constructor> constructor = amplitudePackageClass.getConstructor();
+ packages.add((ReactPackage)constructor.newInstance());
+ } catch (Exception e) {
+ // Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
+ JitsiMeetLogger.d(TAG, "Not loading AmplitudeReactNativePackage");
+ }
+
+ // GiphyReactNativeSdkPackage
+ try {
+ Class> giphyPackageClass = Class.forName("com.giphyreactnativesdk.RTNGiphySdkPackage");
+ Constructor> constructor = giphyPackageClass.getConstructor();
+ packages.add((ReactPackage)constructor.newInstance());
+ } catch (Exception e) {
+ // Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
+ JitsiMeetLogger.d(TAG, "Not loading GiphyReactNativeSdkPackage");
+ }
+
+ // RNGoogleSignInPackage
+ try {
+ Class> googlePackageClass = Class.forName("com.reactnativegooglesignin.RNGoogleSigninPackage");
+ Constructor> constructor = googlePackageClass.getConstructor();
+ packages.add((ReactPackage)constructor.newInstance());
+ } catch (Exception e) {
+ // Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
+ JitsiMeetLogger.d(TAG, "Not loading RNGoogleSignInPackage");
+ }
+
+ return packages;
+ }
+
+ /**
+ * Helper function to send an event to JavaScript.
+ *
+ * @param eventName {@code String} containing the event name.
+ * @param data {@code Object} optional ancillary data for the event.
+ */
+ static void emitEvent(
+ String eventName,
+ @Nullable Object data) {
+ ReactInstanceManager reactInstanceManager
+ = ReactInstanceManagerHolder.getReactInstanceManager();
+
+ if (reactInstanceManager != null) {
+ @SuppressLint("VisibleForTests") ReactContext reactContext
+ = reactInstanceManager.getCurrentReactContext();
+
+ if (reactContext != null) {
+ reactContext
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit(eventName, data);
+ }
+ }
+ }
+
+ /**
+ * Finds a native React module for given class.
+ *
+ * @param nativeModuleClass the native module's class for which an instance
+ * is to be retrieved from the {@link #reactInstanceManager}.
+ * @param the module's type.
+ * @return {@link NativeModule} instance for given interface type or
+ * {@code null} if no instance for this interface is available, or if
+ * {@link #reactInstanceManager} has not been initialized yet.
+ */
+ static T getNativeModule(
+ Class nativeModuleClass) {
+ @SuppressLint("VisibleForTests") ReactContext reactContext
+ = reactInstanceManager != null
+ ? reactInstanceManager.getCurrentReactContext() : null;
+
+ return reactContext != null
+ ? reactContext.getNativeModule(nativeModuleClass) : null;
+ }
+
+ static ReactInstanceManager getReactInstanceManager() {
+ return reactInstanceManager;
+ }
+
+ /**
+ * Internal method to initialize the React Native instance manager. We
+ * create a single instance in order to load the JavaScript bundle a single
+ * time. All {@code ReactRootView} instances will be tied to the one and
+ * only {@code ReactInstanceManager}.
+ *
+ * @param app {@code Application}
+ */
+ static void initReactInstanceManager(Application app) {
+ if (reactInstanceManager != null) {
+ return;
+ }
+
+ // Initialize the WebRTC module options.
+ WebRTCModuleOptions options = WebRTCModuleOptions.getInstance();
+ options.enableMediaProjectionService = true;
+ if (options.videoDecoderFactory == null || options.videoEncoderFactory == null) {
+ EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
+ if (options.videoDecoderFactory == null) {
+ options.videoDecoderFactory = new JitsiVideoDecoderFactory(eglContext);
+ }
+ if (options.videoEncoderFactory == null) {
+ options.videoEncoderFactory = new JitsiVideoEncoderFactory(eglContext);
+ }
+ }
+
+ JitsiMeetLogger.d(TAG, "initializing RN");
+
+ reactInstanceManager
+ = ReactInstanceManager.builder()
+ .setApplication(app)
+ .setCurrentActivity(null)
+ .setBundleAssetName("index.android.bundle")
+ .setJSMainModulePath("index.android")
+ .setJavaScriptExecutorFactory(new HermesExecutorFactory())
+ .addPackages(getReactNativePackages())
+ .setUseDeveloperSupport(BuildConfig.DEBUG)
+ .setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
+ .build();
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactPackageAdapter.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactPackageAdapter.java
new file mode 100644
index 0000000..fe9c4b8
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactPackageAdapter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright @ 2017-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.sdk;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.Collections;
+import java.util.List;
+
+class ReactPackageAdapter
+ implements ReactPackage {
+
+ @Override
+ public List createNativeModules(
+ ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createViewManagers(
+ ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetBaseLogHandler.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetBaseLogHandler.java
new file mode 100644
index 0000000..0a8b288
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetBaseLogHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk.log;
+
+import android.util.Log;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.text.MessageFormat;
+
+import timber.log.Timber;
+
+/**
+ * Base class for all custom log handlers. Implementations must inherit from this class and
+ * implement a `doLog` method which does the actual logging, in addition with a `getTag` method
+ * with which to tag all logs coming into this logger.
+ *
+ * See {@link JitsiMeetDefaultLogHandler} for an example.
+ */
+public abstract class JitsiMeetBaseLogHandler extends Timber.Tree {
+ @Override
+ protected void log(int priority, @Nullable String tag, @NotNull String msg, @Nullable Throwable t) {
+ String errmsg = Log.getStackTraceString(t);
+ if (errmsg.isEmpty()) {
+ doLog(priority, getDefaultTag(), msg);
+ } else {
+ doLog(priority, getDefaultTag(), MessageFormat.format("{0}\n{1}", msg, errmsg));
+ }
+ }
+
+ protected abstract void doLog(int priority, @NotNull String tag, @NotNull String msg);
+
+ protected abstract String getDefaultTag();
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetDefaultLogHandler.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetDefaultLogHandler.java
new file mode 100644
index 0000000..09fb63c
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetDefaultLogHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk.log;
+
+import android.util.Log;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Default implementation of a {@link JitsiMeetBaseLogHandler}. This is the main SDK logger, which
+ * logs using the Android util.Log module.
+ */
+public class JitsiMeetDefaultLogHandler extends JitsiMeetBaseLogHandler {
+ private static final String TAG = "JitsiMeetSDK";
+
+ @Override
+ protected void doLog(int priority, @NotNull String tag, @NotNull String msg) {
+ Log.println(priority, tag, msg);
+ }
+
+ @Override
+ protected String getDefaultTag() {
+ return TAG;
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetLogger.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetLogger.java
new file mode 100644
index 0000000..b0765a9
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/log/JitsiMeetLogger.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright @ 2019-present 8x8, Inc.
+ *
+ * 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.sdk.log;
+
+import timber.log.Timber;
+
+public class JitsiMeetLogger {
+ static {
+ addHandler(new JitsiMeetDefaultLogHandler());
+ }
+
+ public static void addHandler(JitsiMeetBaseLogHandler handler) {
+ if (!Timber.forest().contains(handler)) {
+ try {
+ Timber.plant(handler);
+ } catch (Throwable t) {
+ Timber.w(t, "Couldn't add log handler");
+ }
+
+ }
+ }
+
+ public static void removeHandler(JitsiMeetBaseLogHandler handler) {
+ if (Timber.forest().contains(handler)) {
+ try {
+ Timber.uproot(handler);
+ } catch (Throwable t) {
+ Timber.w(t, "Couldn't remove log handler");
+ }
+ }
+ }
+
+ public static void v(String message, Object... args) {
+ Timber.v(message, args);
+ }
+
+ public static void v(Throwable t, String message, Object... args) {
+ Timber.v(t, message, args);
+ }
+
+ public static void v(Throwable t) {
+ Timber.v(t);
+ }
+
+ public static void d(String message, Object... args) {
+ Timber.d(message, args);
+ }
+
+ public static void d(Throwable t, String message, Object... args) {
+ Timber.d(t, message, args);
+ }
+
+ public static void d(Throwable t) {
+ Timber.d(t);
+ }
+
+ public static void i(String message, Object... args) {
+ Timber.i(message, args);
+ }
+
+ public static void i(Throwable t, String message, Object... args) {
+ Timber.i(t, message, args);
+ }
+
+ public static void i(Throwable t) {
+ Timber.i(t);
+ }
+
+ public static void w(String message, Object... args) {
+ Timber.w(message, args);
+ }
+
+ public static void w(Throwable t, String message, Object... args) {
+ Timber.w(t, message, args);
+ }
+
+ public static void w(Throwable t) {
+ Timber.w(t);
+ }
+
+ public static void e(String message, Object... args) {
+ Timber.e(message, args);
+ }
+
+ public static void e(Throwable t, String message, Object... args) {
+ Timber.e(t, message, args);
+ }
+
+ public static void e(Throwable t) {
+ Timber.e(t);
+ }
+
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfo.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfo.java
new file mode 100644
index 0000000..389ab9d
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfo.java
@@ -0,0 +1,238 @@
+/*
+ * 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.sdk.net;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Constructs IPv6 addresses for IPv4 addresses in the NAT64 environment.
+ *
+ * NAT64 translates IPv4 to IPv6 addresses by adding "well known" prefix and
+ * suffix configured by the administrator. Those are figured out by discovering
+ * both IPv6 and IPv4 addresses of a host and then trying to find a place where
+ * the IPv4 address fits into the format described here:
+ * https://tools.ietf.org/html/rfc6052#section-2.2
+ */
+public class NAT64AddrInfo {
+ /**
+ * Coverts bytes array to upper case HEX string.
+ *
+ * @param bytes an array of bytes to be converted
+ * @return ex. "010AFF" for an array of {1, 10, 255}.
+ */
+ static String bytesToHexString(byte[] bytes) {
+ StringBuilder hexStr = new StringBuilder();
+
+ for (byte b : bytes) {
+ hexStr.append(String.format("%02X", b));
+ }
+
+ return hexStr.toString();
+ }
+
+ /**
+ * Tries to discover the NAT64 prefix/suffix based on the IPv4 and IPv6
+ * addresses resolved for given {@code host}.
+ *
+ * @param host the host for which the code will try to discover IPv4 and
+ * IPv6 addresses which then will be used to figure out the NAT64 prefix.
+ * @return {@link NAT64AddrInfo} instance if the NAT64 prefix/suffix was
+ * successfully discovered or {@code null} if it failed for any reason.
+ * @throws UnknownHostException thrown by {@link InetAddress#getAllByName}.
+ */
+ public static NAT64AddrInfo discover(String host)
+ throws UnknownHostException {
+ InetAddress ipv4 = null;
+ InetAddress ipv6 = null;
+
+ for(InetAddress addr : InetAddress.getAllByName(host)) {
+ byte[] bytes = addr.getAddress();
+
+ if (bytes.length == 4) {
+ ipv4 = addr;
+ } else if (bytes.length == 16) {
+ ipv6 = addr;
+ }
+ }
+
+ if (ipv4 != null && ipv6 != null) {
+ return figureOutNAT64AddrInfo(ipv4.getAddress(), ipv6.getAddress());
+ }
+
+ return null;
+ }
+
+ /**
+ * Based on IPv4 and IPv6 addresses of the same host, the method will make
+ * an attempt to figure out what are the NAT64 prefix and suffix.
+ *
+ * @param ipv4AddrBytes the IPv4 address of the same host in NAT64 network,
+ * as returned by {@link InetAddress#getAddress()}.
+ * @param ipv6AddrBytes the IPv6 address of the same host in NAT64 network,
+ * as returned by {@link InetAddress#getAddress()}.
+ * @return {@link NAT64AddrInfo} instance which contains the prefix/suffix
+ * of the current NAT64 network or {@code null} if the prefix could not be
+ * found.
+ */
+ static NAT64AddrInfo figureOutNAT64AddrInfo(
+ byte[] ipv4AddrBytes,
+ byte[] ipv6AddrBytes) {
+ String ipv6Str = bytesToHexString(ipv6AddrBytes);
+ String ipv4Str = bytesToHexString(ipv4AddrBytes);
+
+ // NAT64 address format:
+ // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------|
+ // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // |32| prefix |v4(32) | u | suffix |
+ // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // |40| prefix |v4(24) | u |(8)| suffix |
+ // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // |48| prefix |v4(16) | u | (16) | suffix |
+ // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // |56| prefix |(8)| u | v4(24) | suffix |
+ // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // |64| prefix | u | v4(32) | suffix |
+ // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // |96| prefix | v4(32) |
+ // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ int prefixLength = 96;
+ int suffixLength = 0;
+ String prefix = null;
+ String suffix = null;
+
+ if (ipv4Str.equalsIgnoreCase(ipv6Str.substring(prefixLength / 4))) {
+ prefix = ipv6Str.substring(0, prefixLength / 4);
+ } else {
+ // Cut out the 'u' octet
+ ipv6Str = ipv6Str.substring(0, 16) + ipv6Str.substring(18);
+
+ for (prefixLength = 64, suffixLength = 6; prefixLength >= 32; ) {
+ if (ipv4Str.equalsIgnoreCase(
+ ipv6Str.substring(
+ prefixLength / 4, prefixLength / 4 + 8))) {
+ prefix = ipv6Str.substring(0, prefixLength / 4);
+ suffix = ipv6Str.substring(ipv6Str.length() - suffixLength);
+ break;
+ }
+
+ prefixLength -= 8;
+ suffixLength += 2;
+ }
+ }
+
+ return prefix != null ? new NAT64AddrInfo(prefix, suffix) : null;
+ }
+
+ /**
+ * An overload for {@link #hexStringToIPv6String(StringBuilder)}.
+ *
+ * @param hexStr a hex representation of IPv6 address bytes.
+ * @return an IPv6 address string.
+ */
+ static String hexStringToIPv6String(String hexStr) {
+ return hexStringToIPv6String(new StringBuilder(hexStr));
+ }
+
+ /**
+ * Converts from HEX representation of IPv6 address bytes into IPv6 address
+ * string which includes the ':' signs.
+ *
+ * @param str a hex representation of IPv6 address bytes.
+ * @return eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C
+ */
+ static String hexStringToIPv6String(StringBuilder str) {
+ for (int i = 32 - 4; i > 0; i -= 4) {
+ str.insert(i, ":");
+ }
+
+ return str.toString().toUpperCase();
+ }
+
+ /**
+ * Parses an IPv4 address string and returns it's byte array representation.
+ *
+ * @param ipv4Address eg. '192.168.3.23'
+ * @return byte representation of given IPv4 address string.
+ * @throws IllegalArgumentException if the address is not in valid format.
+ */
+ static byte[] ipv4AddressStringToBytes(String ipv4Address) {
+ InetAddress address;
+
+ try {
+ address = InetAddress.getByName(ipv4Address);
+ } catch (UnknownHostException e) {
+ throw new IllegalArgumentException(
+ "Invalid IP address: " + ipv4Address, e);
+ }
+
+ byte[] bytes = address.getAddress();
+
+ if (bytes.length != 4) {
+ throw new IllegalArgumentException(
+ "Not an IPv4 address: " + ipv4Address);
+ }
+
+ return bytes;
+ }
+
+ /**
+ * The NAT64 prefix added to construct IPv6 from an IPv4 address.
+ */
+ private final String prefix;
+
+ /**
+ * The NAT64 suffix (if any) used to construct IPv6 from an IPv4 address.
+ */
+ private final String suffix;
+
+ /**
+ * Creates new instance of {@link NAT64AddrInfo}.
+ *
+ * @param prefix the NAT64 prefix.
+ * @param suffix the NAT64 suffix.
+ */
+ private NAT64AddrInfo(String prefix, String suffix) {
+ this.prefix = prefix;
+ this.suffix = suffix;
+ }
+
+ /**
+ * Based on the NAT64 prefix and suffix will create an IPv6 representation
+ * of the given IPv4 address.
+ *
+ * @param ipv4Address eg. '192.34.2.3'
+ * @return IPv6 address string eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C
+ * @throws IllegalArgumentException if given string is not a valid IPv4
+ * address.
+ */
+ public String getIPv6Address(String ipv4Address) {
+ byte[] ipv4AddressBytes = ipv4AddressStringToBytes(ipv4Address);
+ StringBuilder newIPv6Str = new StringBuilder();
+
+ newIPv6Str.append(prefix);
+ newIPv6Str.append(bytesToHexString(ipv4AddressBytes));
+
+ if (suffix != null) {
+ // Insert the 'u' octet.
+ newIPv6Str.insert(16, "00");
+ newIPv6Str.append(suffix);
+ }
+
+ return hexStringToIPv6String(newIPv6Str);
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfoModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfoModule.java
new file mode 100644
index 0000000..d88872f
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfoModule.java
@@ -0,0 +1,123 @@
+/*
+ * 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.sdk.net;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.module.annotations.ReactModule;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+import java.net.UnknownHostException;
+
+/**
+ * This module exposes the functionality of creating an IPv6 representation
+ * of IPv4 addresses in NAT64 environment.
+ *
+ * See[1] and [2] for more info on what NAT64 is.
+ * [1]: https://tools.ietf.org/html/rfc6146
+ * [2]: https://tools.ietf.org/html/rfc6052
+ */
+@ReactModule(name = NAT64AddrInfoModule.NAME)
+public class NAT64AddrInfoModule
+ extends ReactContextBaseJavaModule {
+
+ public final static String NAME = "NAT64AddrInfo";
+
+ /**
+ * The host for which the module wil try to resolve both IPv4 and IPv6
+ * addresses in order to figure out the NAT64 prefix.
+ */
+ private final static String HOST = "ipv4only.arpa";
+
+ /**
+ * How long is the {@link NAT64AddrInfo} instance valid.
+ */
+ private final static long INFO_LIFETIME = 60 * 1000;
+
+ /**
+ * The {@code Log} tag {@code NAT64AddrInfoModule} is to log messages with.
+ */
+ private final static String TAG = NAME;
+
+ /**
+ * The {@link NAT64AddrInfo} instance which holds NAT64 prefix/suffix.
+ */
+ private NAT64AddrInfo info;
+
+ /**
+ * When {@link #info} was created.
+ */
+ private long infoTimestamp;
+
+ /**
+ * Creates new {@link NAT64AddrInfoModule}.
+ *
+ * @param reactContext the react context to be used by the new module
+ * instance.
+ */
+ public NAT64AddrInfoModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ /**
+ * Tries to obtain IPv6 address for given IPv4 address in NAT64 environment.
+ *
+ * @param ipv4Address IPv4 address string.
+ * @param promise a {@link Promise} which will be resolved either with IPv6
+ * address for given IPv4 address or with {@code null} if no
+ * {@link NAT64AddrInfo} was resolved for the current network. Will be
+ * rejected if given {@code ipv4Address} is not a valid IPv4 address.
+ */
+ @ReactMethod
+ public void getIPv6Address(String ipv4Address, final Promise promise) {
+ // Reset if cached for too long.
+ if (System.currentTimeMillis() - infoTimestamp > INFO_LIFETIME) {
+ info = null;
+ }
+
+ if (info == null) {
+ String host = HOST;
+
+ try {
+ info = NAT64AddrInfo.discover(host);
+ } catch (UnknownHostException e) {
+ JitsiMeetLogger.e(e, TAG + " NAT64AddrInfo.discover: " + host);
+ }
+ infoTimestamp = System.currentTimeMillis();
+ }
+
+ String result;
+
+ try {
+ result = info == null ? null : info.getIPv6Address(ipv4Address);
+ } catch (IllegalArgumentException exc) {
+ JitsiMeetLogger.e(exc, TAG + " Failed to get IPv6 address for: " + ipv4Address);
+
+ // We don't want to reject. It's not a big deal if there's no IPv6
+ // address resolved.
+ result = null;
+ }
+ promise.resolve(result);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/android/sdk/src/main/java/org/webrtc/JitsiPlatformVideoDecoderFactory.java b/android/sdk/src/main/java/org/webrtc/JitsiPlatformVideoDecoderFactory.java
new file mode 100644
index 0000000..97522a3
--- /dev/null
+++ b/android/sdk/src/main/java/org/webrtc/JitsiPlatformVideoDecoderFactory.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc;
+
+import android.media.MediaCodecInfo;
+import androidx.annotation.Nullable;
+
+/** Factory for Android platform software VideoDecoders. */
+public class JitsiPlatformVideoDecoderFactory extends MediaCodecVideoDecoderFactory {
+ /**
+ * Default allowed predicate.
+ */
+ private static final Predicate defaultAllowedPredicate =
+ codecInfo -> {
+ // We only want to use the platform software codecs.
+ return MediaCodecUtils.isSoftwareOnly(codecInfo);
+ };
+
+ /**
+ * Creates a PlatformSoftwareVideoDecoderFactory that supports surface texture rendering.
+ *
+ * @param sharedContext The textures generated will be accessible from this context. May be null,
+ * this disables texture support.
+ */
+ public JitsiPlatformVideoDecoderFactory(@Nullable EglBase.Context sharedContext) {
+ super(sharedContext, defaultAllowedPredicate);
+ }
+
+ public JitsiPlatformVideoDecoderFactory(@Nullable EglBase.Context sharedContext, @Nullable Predicate codecAllowedPredicate) {
+ super(sharedContext, codecAllowedPredicate == null ? defaultAllowedPredicate : codecAllowedPredicate.and(defaultAllowedPredicate));
+ }
+}
diff --git a/android/sdk/src/main/res/drawable-hdpi/ic_notification.png b/android/sdk/src/main/res/drawable-hdpi/ic_notification.png
new file mode 100644
index 0000000..da701e5
Binary files /dev/null and b/android/sdk/src/main/res/drawable-hdpi/ic_notification.png differ
diff --git a/android/sdk/src/main/res/drawable-mdpi/ic_notification.png b/android/sdk/src/main/res/drawable-mdpi/ic_notification.png
new file mode 100644
index 0000000..7540f39
Binary files /dev/null and b/android/sdk/src/main/res/drawable-mdpi/ic_notification.png differ
diff --git a/android/sdk/src/main/res/drawable-xhdpi/ic_notification.png b/android/sdk/src/main/res/drawable-xhdpi/ic_notification.png
new file mode 100644
index 0000000..bed1734
Binary files /dev/null and b/android/sdk/src/main/res/drawable-xhdpi/ic_notification.png differ
diff --git a/android/sdk/src/main/res/drawable-xxhdpi/ic_notification.png b/android/sdk/src/main/res/drawable-xxhdpi/ic_notification.png
new file mode 100644
index 0000000..88bf389
Binary files /dev/null and b/android/sdk/src/main/res/drawable-xxhdpi/ic_notification.png differ
diff --git a/android/sdk/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/sdk/src/main/res/drawable-xxxhdpi/ic_notification.png
new file mode 100644
index 0000000..f071fac
Binary files /dev/null and b/android/sdk/src/main/res/drawable-xxxhdpi/ic_notification.png differ
diff --git a/android/sdk/src/main/res/layout/activity_jitsi_meet.xml b/android/sdk/src/main/res/layout/activity_jitsi_meet.xml
new file mode 100644
index 0000000..e2213b0
--- /dev/null
+++ b/android/sdk/src/main/res/layout/activity_jitsi_meet.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/sdk/src/main/res/values-ru/strings.xml b/android/sdk/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000..48573ce
--- /dev/null
+++ b/android/sdk/src/main/res/values-ru/strings.xml
@@ -0,0 +1,9 @@
+
+
+ Текущая встреча
+ Нажмите, чтобы вернуться к встрече.
+ Отключиться
+ Отключить звук
+ Включить звук
+ Ongoing Conference Notifications
+
\ No newline at end of file
diff --git a/android/sdk/src/main/res/values/strings.xml b/android/sdk/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8bc4c5b
--- /dev/null
+++ b/android/sdk/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+
+ Jitsi Meet SDK
+
+ Media projection
+ You are currently sharing your screen.
+ Ongoing meeting
+ You are currently in a meeting. Tap to return to it.
+ Hang up
+ Mute
+ Unmute
+ Ongoing Conference Notifications
+
diff --git a/android/sdk/src/main/res/values/styles.xml b/android/sdk/src/main/res/values/styles.xml
new file mode 100644
index 0000000..83cac6b
--- /dev/null
+++ b/android/sdk/src/main/res/values/styles.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/android/sdk/src/test/java/org/jitsi/meet/sdk/net/NAT64AddrInfoTest.java b/android/sdk/src/test/java/org/jitsi/meet/sdk/net/NAT64AddrInfoTest.java
new file mode 100644
index 0000000..c01ecaf
--- /dev/null
+++ b/android/sdk/src/test/java/org/jitsi/meet/sdk/net/NAT64AddrInfoTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright @ 2017-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.sdk.net;
+
+import org.junit.Test;
+
+import java.math.BigInteger;
+import java.net.UnknownHostException;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link NAT64AddrInfo} class.
+ */
+public class NAT64AddrInfoTest {
+ /**
+ * Test case for the 96 prefix length.
+ */
+ @Test
+ public void test96Prefix() {
+ testPrefixSuffix(
+ "260777000000000400000000", "", "203.0.113.1", "23.17.23.3");
+ }
+
+ /**
+ * Test case for the 64 prefix length.
+ */
+ @Test
+ public void test64Prefix() {
+ String prefix = "1FF2A227B3AAF3D2";
+ String suffix = "BB87C8";
+
+ testPrefixSuffix(prefix, suffix, "48.46.87.34", "23.87.145.4");
+ }
+
+ /**
+ * Test case for the 56 prefix length.
+ */
+ @Test
+ public void test56Prefix() {
+ String prefix = "1FF2A227B3AAF3";
+ String suffix = "A2BB87C8";
+
+ testPrefixSuffix(prefix, suffix, "34.72.234.255", "1.235.3.65");
+ }
+
+ /**
+ * Test case for the 48 prefix length.
+ */
+ @Test
+ public void test48Prefix() {
+ String prefix = "1FF2A227B3AA";
+ String suffix = "72A2BB87C8";
+
+ testPrefixSuffix(prefix, suffix, "97.54.3.23", "77.49.0.33");
+ }
+
+ /**
+ * Test case for the 40 prefix length.
+ */
+ @Test
+ public void test40Prefix() {
+ String prefix = "1FF2A227B3";
+ String suffix = "D972A2BB87C8";
+
+ testPrefixSuffix(prefix, suffix, "10.23.56.121", "97.65.32.21");
+ }
+
+ /**
+ * Test case for the 32 prefix length.
+ */
+ @Test
+ public void test32Prefix()
+ throws UnknownHostException {
+ String prefix = "1FF2A227";
+ String suffix = "20D972A2BB87C8";
+
+ testPrefixSuffix(prefix, suffix, "162.63.65.189", "135.222.84.206");
+ }
+
+ private static String buildIPv6Addr(
+ String prefix, String suffix, String ipv4Hex) {
+ String ipv6Str = prefix + ipv4Hex + suffix;
+
+ if (suffix.length() > 0) {
+ ipv6Str = new StringBuilder(ipv6Str).insert(16, "00").toString();
+ }
+
+ return ipv6Str;
+ }
+
+ private void testPrefixSuffix(
+ String prefix, String suffix, String ipv4, String otherIPv4) {
+ byte[] ipv4Bytes = NAT64AddrInfo.ipv4AddressStringToBytes(ipv4);
+ String ipv4String = NAT64AddrInfo.bytesToHexString(ipv4Bytes);
+ String ipv6Str = buildIPv6Addr(prefix, suffix, ipv4String);
+
+ BigInteger ipv6Address = new BigInteger(ipv6Str, 16);
+
+ NAT64AddrInfo nat64AddrInfo
+ = NAT64AddrInfo.figureOutNAT64AddrInfo(
+ ipv4Bytes, ipv6Address.toByteArray());
+
+ assertNotNull("Failed to figure out NAT64 info", nat64AddrInfo);
+
+ String newIPv6 = nat64AddrInfo.getIPv6Address(ipv4);
+
+ assertEquals(
+ NAT64AddrInfo.hexStringToIPv6String(ipv6Address.toString(16)),
+ newIPv6);
+
+ byte[] ipv4Addr2 = NAT64AddrInfo.ipv4AddressStringToBytes(otherIPv4);
+ String ipv4Addr2Hex = NAT64AddrInfo.bytesToHexString(ipv4Addr2);
+
+ newIPv6 = nat64AddrInfo.getIPv6Address(otherIPv4);
+
+ assertEquals(
+ NAT64AddrInfo.hexStringToIPv6String(
+ buildIPv6Addr(prefix, suffix, ipv4Addr2Hex)),
+ newIPv6);
+ }
+
+ @Test
+ public void testInvalidIPv4Format() {
+ testInvalidIPv4Format("256.1.2.3");
+ testInvalidIPv4Format("FE80:CD00:0000:0CDA:1357:0000:212F:749C");
+ }
+
+ private void testInvalidIPv4Format(String ipv4Str) {
+ try {
+ NAT64AddrInfo.ipv4AddressStringToBytes(ipv4Str);
+ fail("Did not throw IllegalArgumentException");
+ } catch (IllegalArgumentException exc) {
+ /* OK */
+ }
+ }
+}
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..43bb140
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,54 @@
+include ':app', ':sdk'
+
+include ':react-native-amplitude'
+project(':react-native-amplitude').projectDir = new File(rootProject.projectDir, '../node_modules/@amplitude/analytics-react-native/android')
+include ':react-native-async-storage'
+project(':react-native-async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-async-storage/async-storage/android')
+include ':react-native-background-timer'
+project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
+include ':react-native-calendar-events'
+project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')
+include ':react-native-community_clipboard'
+project(':react-native-community_clipboard').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-clipboard/clipboard/android')
+include ':react-native-community_netinfo'
+project(':react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
+include ':react-native-default-preference'
+project(':react-native-default-preference').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-default-preference/android')
+include ':react-native-device-info'
+project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
+include ':react-native-gesture-handler'
+project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
+include ':react-native-get-random-values'
+project(':react-native-get-random-values').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-get-random-values/android')
+include ':react-native-giphy'
+project(':react-native-giphy').projectDir = new File(rootProject.projectDir, '../node_modules/@giphy/react-native-sdk/android')
+include ':react-native-google-signin'
+project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-google-signin/google-signin/android')
+include ':react-native-immersive-mode'
+project(':react-native-immersive-mode').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive-mode/android')
+include ':react-native-keep-awake'
+project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/@sayem314/react-native-keep-awake/android')
+include ':react-native-orientation-locker'
+project(':react-native-orientation-locker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation-locker/android')
+include ':react-native-pager-view'
+project(':react-native-pager-view').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-pager-view/android')
+include ':react-native-performance'
+project(':react-native-performance').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-performance/android')
+include ':react-native-safe-area-context'
+project(':react-native-safe-area-context').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-safe-area-context/android')
+include ':react-native-screens'
+project(':react-native-screens').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-screens/android')
+include ':react-native-slider'
+project(':react-native-slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
+include ':react-native-sound'
+project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
+include ':react-native-splash-view'
+project(':react-native-splash-view').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-view/android')
+include ':react-native-svg'
+project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
+include ':react-native-video'
+project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
+include ':react-native-webrtc'
+project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
+include ':react-native-webview'
+project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..4505556
--- /dev/null
+++ b/app.js
@@ -0,0 +1,70 @@
+/* Jitsi Meet app main entrypoint. */
+
+// Re-export jQuery
+// FIXME: Remove this requirement from torture tests.
+import $ from 'jquery';
+
+window.$ = window.jQuery = $;
+
+import '@matrix-org/olm';
+
+import 'focus-visible';
+
+/*
+* Safari polyfill for createImageBitmap
+* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap
+*
+* Support source image types: Canvas.
+*/
+if (!('createImageBitmap' in window)) {
+ window.createImageBitmap = function(data) {
+ return new Promise((resolve, reject) => {
+ let dataURL;
+
+ if (data instanceof HTMLCanvasElement) {
+ dataURL = data.toDataURL();
+ } else {
+ reject(new Error('createImageBitmap does not handle the provided image source type'));
+ }
+ const img = document.createElement('img');
+
+ img.addEventListener('load', () => {
+ resolve(img);
+ });
+ img.src = dataURL;
+ });
+ };
+}
+
+// We need to setup the jitsi-local-storage as early as possible so that we can start using it.
+// NOTE: If jitsi-local-storage is used before the initial setup is performed this will break the use case when we use
+// the local storage from the parent page when the localStorage is disabled. Also the setup is relying that
+// window.location is not changed and still has all URL parameters.
+import './react/features/base/jitsi-local-storage/setup';
+import conference from './conference';
+import API from './modules/API';
+import UI from './modules/UI/UI';
+import translation from './modules/translation/translation';
+
+// Initialize Olm as early as possible.
+if (window.Olm) {
+ window.Olm.init().catch(e => {
+ console.error('Failed to initialize Olm, E2EE will be disabled', e);
+ delete window.Olm;
+ });
+}
+
+window.APP = {
+ API,
+ conference,
+ translation,
+ UI
+};
+
+// TODO The execution of the mobile app starts from react/index.native.js.
+// Similarly, the execution of the Web app should start from react/index.web.js
+// for the sake of consistency and ease of understanding. Temporarily though
+// because we are at the beginning of introducing React into the Web app, allow
+// the execution of the Web app to start from app.js in order to reduce the
+// complexity of the beginning step.
+import './react';
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..c2e341d
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,16 @@
+module.exports = {
+ presets: [ 'module:@react-native/babel-preset' ],
+ env: {
+ production: {
+ plugins: [ 'react-native-paper/babel' ]
+ }
+ },
+
+ // This happens because react native has conflict with @babel/plugin-transform-private-methods plugin
+ // https://github.com/ethers-io/ethers.js/discussions/4309#discussioncomment-6694524
+ plugins: [ 'optional-require',
+ [ '@babel/plugin-transform-private-methods', {
+ 'loose': true
+ } ]
+ ]
+};
diff --git a/base.html b/base.html
new file mode 100644
index 0000000..f5d75e2
--- /dev/null
+++ b/base.html
@@ -0,0 +1 @@
+
diff --git a/body.html b/body.html
new file mode 100644
index 0000000..e69de29
diff --git a/conference.js b/conference.js
new file mode 100644
index 0000000..e8477c7
--- /dev/null
+++ b/conference.js
@@ -0,0 +1,2349 @@
+/* global APP, JitsiMeetJS, config, interfaceConfig */
+
+import { jitsiLocalStorage } from '@jitsi/js-utils';
+import Logger from '@jitsi/logger';
+
+import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
+import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
+import Recorder from './modules/recorder/Recorder';
+import { createTaskQueue } from './modules/util/helpers';
+import {
+ createDeviceChangedEvent,
+ createScreenSharingEvent,
+ createStartSilentEvent,
+ createTrackMutedEvent
+} from './react/features/analytics/AnalyticsEvents';
+import { sendAnalytics } from './react/features/analytics/functions';
+import {
+ maybeRedirectToWelcomePage,
+ reloadWithStoredParams
+} from './react/features/app/actions';
+import {
+ _conferenceWillJoin,
+ authStatusChanged,
+ conferenceFailed,
+ conferenceJoinInProgress,
+ conferenceJoined,
+ conferenceLeft,
+ conferencePropertiesChanged,
+ conferenceSubjectChanged,
+ conferenceTimestampChanged,
+ conferenceUniqueIdSet,
+ conferenceWillInit,
+ conferenceWillLeave,
+ dataChannelClosed,
+ dataChannelOpened,
+ e2eRttChanged,
+ endpointMessageReceived,
+ kickedOut,
+ lockStateChanged,
+ nonParticipantMessageReceived,
+ onStartMutedPolicyChanged,
+ p2pStatusChanged
+} from './react/features/base/conference/actions';
+import {
+ AVATAR_URL_COMMAND,
+ CONFERENCE_LEAVE_REASONS,
+ EMAIL_COMMAND
+} from './react/features/base/conference/constants';
+import {
+ commonUserJoinedHandling,
+ commonUserLeftHandling,
+ getConferenceOptions,
+ sendLocalParticipant,
+ updateTrackMuteState
+} from './react/features/base/conference/functions';
+import { getReplaceParticipant, getSsrcRewritingFeatureFlag } from './react/features/base/config/functions';
+import { connect } from './react/features/base/connection/actions.web';
+import {
+ checkAndNotifyForNewDevice,
+ getAvailableDevices,
+ notifyCameraError,
+ notifyMicError,
+ updateDeviceList
+} from './react/features/base/devices/actions.web';
+import {
+ areDevicesDifferent,
+ filterIgnoredDevices,
+ flattenAvailableDevices,
+ getDefaultDeviceId,
+ logDevices,
+ setAudioOutputDeviceId
+} from './react/features/base/devices/functions.web';
+import {
+ JitsiConferenceErrors,
+ JitsiConferenceEvents,
+ JitsiE2ePingEvents,
+ JitsiMediaDevicesEvents,
+ JitsiTrackEvents,
+ browser
+} from './react/features/base/lib-jitsi-meet';
+import {
+ gumPending,
+ setAudioAvailable,
+ setAudioMuted,
+ setAudioUnmutePermissions,
+ setInitialGUMPromise,
+ setVideoAvailable,
+ setVideoMuted,
+ setVideoUnmutePermissions
+} from './react/features/base/media/actions';
+import { MEDIA_TYPE, VIDEO_MUTISM_AUTHORITY, VIDEO_TYPE } from './react/features/base/media/constants';
+import {
+ getStartWithAudioMuted,
+ getStartWithVideoMuted,
+ isVideoMutedByUser
+} from './react/features/base/media/functions';
+import { IGUMPendingState } from './react/features/base/media/types';
+import {
+ dominantSpeakerChanged,
+ localParticipantAudioLevelChanged,
+ localParticipantRoleChanged,
+ participantKicked,
+ participantMutedUs,
+ participantPresenceChanged,
+ participantRoleChanged,
+ participantSourcesUpdated,
+ participantUpdated,
+ screenshareParticipantDisplayNameChanged,
+ updateRemoteParticipantFeatures
+} from './react/features/base/participants/actions';
+import {
+ getLocalParticipant,
+ getNormalizedDisplayName,
+ getParticipantByIdOrUndefined,
+ getVirtualScreenshareParticipantByOwnerId
+} from './react/features/base/participants/functions';
+import { updateSettings } from './react/features/base/settings/actions';
+import {
+ addLocalTrack,
+ createInitialAVTracks,
+ destroyLocalTracks,
+ displayErrorsForCreateInitialLocalTracks,
+ replaceLocalTrack,
+ setGUMPendingStateOnFailedTracks,
+ toggleScreensharing as toggleScreensharingA,
+ trackAdded,
+ trackRemoved
+} from './react/features/base/tracks/actions';
+import {
+ createLocalTracksF,
+ getLocalJitsiAudioTrack,
+ getLocalJitsiVideoTrack,
+ getLocalVideoTrack,
+ isLocalTrackMuted,
+ isUserInteractionRequiredForUnmute
+} from './react/features/base/tracks/functions';
+import { downloadJSON } from './react/features/base/util/downloadJSON';
+import { getJitsiMeetGlobalNSConnectionTimes } from './react/features/base/util/helpers';
+import { openLeaveReasonDialog } from './react/features/conference/actions.web';
+import { showDesktopPicker } from './react/features/desktop-picker/actions';
+import { appendSuffix } from './react/features/display-name/functions';
+import { maybeOpenFeedbackDialog, submitFeedback } from './react/features/feedback/actions';
+import { maybeSetLobbyChatMessageListener } from './react/features/lobby/actions.any';
+import { setNoiseSuppressionEnabled } from './react/features/noise-suppression/actions';
+import {
+ hideNotification,
+ showErrorNotification,
+ showNotification,
+ showWarningNotification
+} from './react/features/notifications/actions';
+import {
+ DATA_CHANNEL_CLOSED_NOTIFICATION_ID,
+ NOTIFICATION_TIMEOUT_TYPE
+} from './react/features/notifications/constants';
+import { suspendDetected } from './react/features/power-monitor/actions';
+import { initPrejoin, isPrejoinPageVisible } from './react/features/prejoin/functions';
+import { disableReceiver, stopReceiver } from './react/features/remote-control/actions';
+import { setScreenAudioShareState } from './react/features/screen-share/actions.web';
+import { isScreenAudioShared } from './react/features/screen-share/functions';
+import { toggleScreenshotCaptureSummary } from './react/features/screenshot-capture/actions';
+import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
+import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
+import { handleToggleVideoMuted } from './react/features/toolbox/actions.any';
+import { transcriberJoined, transcriberLeft } from './react/features/transcribing/actions';
+import { muteLocal } from './react/features/video-menu/actions.any';
+
+const logger = Logger.getLogger(__filename);
+let room;
+
+/*
+ * Logic to open a desktop picker put on the window global for
+ * lib-jitsi-meet to detect and invoke.
+ *
+ * TODO: remove once the Electron SDK supporting gDM has been out for a while.
+ */
+window.JitsiMeetScreenObtainer = {
+ openDesktopPicker(options, onSourceChoose) {
+ APP.store.dispatch(showDesktopPicker(options, onSourceChoose));
+ }
+};
+
+/**
+ * Known custom conference commands.
+ */
+const commands = {
+ AVATAR_URL: AVATAR_URL_COMMAND,
+ CUSTOM_ROLE: 'custom-role',
+ EMAIL: EMAIL_COMMAND,
+ ETHERPAD: 'etherpad'
+};
+
+/**
+ * Share data to other users.
+ * @param command the command
+ * @param {string} value new value
+ */
+function sendData(command, value) {
+ if (!room) {
+ return;
+ }
+
+ room.removeCommand(command);
+ room.sendCommand(command, { value });
+}
+
+/**
+ * A queue for the async replaceLocalTrack action so that multiple audio
+ * replacements cannot happen simultaneously. This solves the issue where
+ * replaceLocalTrack is called multiple times with an oldTrack of null, causing
+ * multiple local tracks of the same type to be used.
+ *
+ * @private
+ * @type {Object}
+ */
+const _replaceLocalAudioTrackQueue = createTaskQueue();
+
+/**
+ * A task queue for replacement local video tracks. This separate queue exists
+ * so video replacement is not blocked by audio replacement tasks in the queue
+ * {@link _replaceLocalAudioTrackQueue}.
+ *
+ * @private
+ * @type {Object}
+ */
+const _replaceLocalVideoTrackQueue = createTaskQueue();
+
+/**
+ *
+ */
+class ConferenceConnector {
+ /**
+ *
+ */
+ constructor(resolve, reject, conference) {
+ this._conference = conference;
+ this._resolve = resolve;
+ this._reject = reject;
+ this.reconnectTimeout = null;
+ room.on(JitsiConferenceEvents.CONFERENCE_JOINED,
+ this._handleConferenceJoined.bind(this));
+ room.on(JitsiConferenceEvents.CONFERENCE_FAILED,
+ this._onConferenceFailed.bind(this));
+ }
+
+ /**
+ *
+ */
+ _handleConferenceFailed(err) {
+ this._unsubscribe();
+ this._reject(err);
+ }
+
+ /**
+ *
+ */
+ _onConferenceFailed(err, ...params) {
+ APP.store.dispatch(conferenceFailed(room, err, ...params));
+ logger.error('CONFERENCE FAILED:', err, ...params);
+
+ switch (err) {
+
+ case JitsiConferenceErrors.RESERVATION_ERROR: {
+ const [ code, msg ] = params;
+
+ APP.store.dispatch(showErrorNotification({
+ descriptionArguments: {
+ code,
+ msg
+ },
+ descriptionKey: 'dialog.reservationErrorMsg',
+ titleKey: 'dialog.reservationError'
+ }));
+ break;
+ }
+
+ case JitsiConferenceErrors.GRACEFUL_SHUTDOWN:
+ APP.store.dispatch(showErrorNotification({
+ descriptionKey: 'dialog.gracefulShutdown',
+ titleKey: 'dialog.serviceUnavailable'
+ }));
+ break;
+
+ // FIXME FOCUS_DISCONNECTED is a confusing event name.
+ // What really happens there is that the library is not ready yet,
+ // because Jicofo is not available, but it is going to give it another
+ // try.
+ case JitsiConferenceErrors.FOCUS_DISCONNECTED: {
+ const [ focus, retrySec ] = params;
+
+ APP.store.dispatch(showNotification({
+ descriptionKey: focus,
+ titleKey: retrySec
+ }, NOTIFICATION_TIMEOUT_TYPE.SHORT));
+ break;
+ }
+
+ case JitsiConferenceErrors.FOCUS_LEFT:
+ case JitsiConferenceErrors.ICE_FAILED:
+ case JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
+ case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
+ APP.store.dispatch(conferenceWillLeave(room));
+
+ // FIXME the conference should be stopped by the library and not by
+ // the app. Both the errors above are unrecoverable from the library
+ // perspective.
+ room.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).then(() => APP.connection.disconnect());
+ break;
+
+ case JitsiConferenceErrors.INCOMPATIBLE_SERVER_VERSIONS:
+ APP.store.dispatch(reloadWithStoredParams());
+ break;
+
+ default:
+ this._handleConferenceFailed(err, ...params);
+ }
+ }
+
+ /**
+ *
+ */
+ _unsubscribe() {
+ room.off(
+ JitsiConferenceEvents.CONFERENCE_JOINED,
+ this._handleConferenceJoined);
+ room.off(
+ JitsiConferenceEvents.CONFERENCE_FAILED,
+ this._onConferenceFailed);
+ if (this.reconnectTimeout !== null) {
+ clearTimeout(this.reconnectTimeout);
+ }
+ }
+
+ /**
+ *
+ */
+ _handleConferenceJoined() {
+ this._unsubscribe();
+ this._resolve();
+ }
+
+ /**
+ *
+ */
+ connect() {
+ const replaceParticipant = getReplaceParticipant(APP.store.getState());
+
+ // the local storage overrides here and in connection.js can be used by jibri
+ room.join(jitsiLocalStorage.getItem('xmpp_conference_password_override'), replaceParticipant);
+ }
+}
+
+/**
+ * Disconnects the connection.
+ * @returns resolved Promise. We need this in order to make the Promise.all
+ * call in hangup() to resolve when all operations are finished.
+ */
+function disconnect() {
+ const onDisconnected = () => {
+ APP.API.notifyConferenceLeft(APP.conference.roomName);
+
+ return Promise.resolve();
+ };
+
+ if (!APP.connection) {
+ return onDisconnected();
+ }
+
+ return APP.connection.disconnect().then(onDisconnected, onDisconnected);
+}
+
+export default {
+ /**
+ * Flag used to delay modification of the muted status of local media tracks
+ * until those are created (or not, but at that point it's certain that
+ * the tracks won't exist).
+ */
+ _localTracksInitialized: false,
+
+ /**
+ * Flag used to prevent the creation of another local video track in this.muteVideo if one is already in progress.
+ */
+ isCreatingLocalTrack: false,
+
+ isSharingScreen: false,
+
+ /**
+ * Returns an object containing a promise which resolves with the created tracks &
+ * the errors resulting from that process.
+ * @param {object} options
+ * @param {boolean} options.startAudioOnly=false - if true then
+ * only audio track will be created and the audio only mode will be turned
+ * on.
+ * @param {boolean} options.startScreenSharing=false - if true
+ * should start with screensharing instead of camera video.
+ * @param {boolean} options.startWithAudioMuted - will start the conference
+ * without any audio tracks.
+ * @param {boolean} options.startWithVideoMuted - will start the conference
+ * without any video tracks.
+ * @param {boolean} recordTimeMetrics - If true time metrics will be recorded.
+ * @returns {Promise, Object}
+ */
+ createInitialLocalTracks(options = {}, recordTimeMetrics = false) {
+ const errors = {};
+
+ // Always get a handle on the audio input device so that we have statistics (such as "No audio input" or
+ // "Are you trying to speak?" ) even if the user joins the conference muted.
+ const initialDevices = config.startSilent || config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ];
+ const requestedAudio = !config.disableInitialGUM;
+ let requestedVideo = false;
+
+ if (!config.disableInitialGUM
+ && !options.startWithVideoMuted
+ && !options.startAudioOnly
+ && !options.startScreenSharing) {
+ initialDevices.push(MEDIA_TYPE.VIDEO);
+ requestedVideo = true;
+ }
+
+ let tryCreateLocalTracks = Promise.resolve([]);
+
+ // On Electron there is no permission prompt for granting permissions. That's why we don't need to
+ // spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
+ // probably never resolve.
+ const timeout = browser.isElectron() ? 15000 : 60000;
+ const audioOptions = {
+ devices: [ MEDIA_TYPE.AUDIO ],
+ timeout
+ };
+
+ // Spot uses the _desktopSharingSourceDevice config option to use an external video input device label as
+ // screenshare and calls getUserMedia instead of getDisplayMedia for capturing the media.
+ if (options.startScreenSharing && config._desktopSharingSourceDevice) {
+ tryCreateLocalTracks = this._createDesktopTrack()
+ .then(([ desktopStream ]) => {
+ if (!requestedAudio) {
+ return [ desktopStream ];
+ }
+
+ return createLocalTracksF(audioOptions)
+ .then(([ audioStream ]) =>
+ [ desktopStream, audioStream ])
+ .catch(error => {
+ errors.audioOnlyError = error;
+
+ return [ desktopStream ];
+ });
+ })
+ .catch(error => {
+ logger.error('Failed to obtain desktop stream', error);
+ errors.screenSharingError = error;
+
+ return requestedAudio ? createLocalTracksF(audioOptions) : [];
+ })
+ .catch(error => {
+ errors.audioOnlyError = error;
+
+ return [];
+ });
+ } else if (requestedAudio || requestedVideo) {
+ tryCreateLocalTracks = APP.store.dispatch(createInitialAVTracks({
+ devices: initialDevices,
+ timeout
+ }, recordTimeMetrics)).then(({ tracks, errors: pErrors }) => {
+ Object.assign(errors, pErrors);
+
+ return tracks;
+ });
+ }
+
+ return {
+ tryCreateLocalTracks,
+ errors
+ };
+ },
+
+ startConference(tracks) {
+ tracks.forEach(track => {
+ if ((track.isAudioTrack() && this.isLocalAudioMuted())
+ || (track.isVideoTrack() && this.isLocalVideoMuted())) {
+ const mediaType = track.getType();
+
+ sendAnalytics(
+ createTrackMutedEvent(mediaType, 'initial mute'));
+ logger.log(`${mediaType} mute: initially muted.`);
+ track.mute();
+ }
+ });
+
+ this._createRoom(tracks);
+
+ // if user didn't give access to mic or camera or doesn't have
+ // them at all, we mark corresponding toolbar buttons as muted,
+ // so that the user can try unmute later on and add audio/video
+ // to the conference
+ if (!tracks.find(t => t.isAudioTrack())) {
+ this.updateAudioIconEnabled();
+ }
+
+ if (!tracks.find(t => t.isVideoTrack())) {
+ this.setVideoMuteStatus();
+ }
+
+ if (config.iAmRecorder) {
+ this.recorder = new Recorder();
+ }
+
+ if (config.startSilent) {
+ sendAnalytics(createStartSilentEvent());
+ APP.store.dispatch(showNotification({
+ descriptionKey: 'notify.startSilentDescription',
+ titleKey: 'notify.startSilentTitle'
+ }, NOTIFICATION_TIMEOUT_TYPE.LONG));
+ }
+
+ // XXX The API will take care of disconnecting from the XMPP
+ // server (and, thus, leaving the room) on unload.
+ return new Promise((resolve, reject) => {
+ new ConferenceConnector(resolve, reject, this).connect();
+ });
+ },
+
+ /**
+ * Open new connection and join the conference when prejoin page is not enabled.
+ * If prejoin page is enabled open an new connection in the background
+ * and create local tracks.
+ *
+ * @param {{ roomName: string, shouldDispatchConnect }} options
+ * @returns {Promise}
+ */
+ async init({ roomName, shouldDispatchConnect }) {
+ const state = APP.store.getState();
+ const initialOptions = {
+ startAudioOnly: config.startAudioOnly,
+ startScreenSharing: config.startScreenSharing,
+ startWithAudioMuted: getStartWithAudioMuted(state) || isUserInteractionRequiredForUnmute(state),
+ startWithVideoMuted: getStartWithVideoMuted(state) || isUserInteractionRequiredForUnmute(state)
+ };
+ const connectionTimes = getJitsiMeetGlobalNSConnectionTimes();
+ const startTime = window.performance.now();
+
+ connectionTimes['conference.init.start'] = startTime;
+
+ logger.debug(`Executed conference.init with roomName: ${roomName} (performance.now=${startTime})`);
+
+ this.roomName = roomName;
+
+ try {
+ // Initialize the device list first. This way, when creating tracks based on preferred devices, loose label
+ // matching can be done in cases where the exact ID match is no longer available, such as -
+ // 1. When the camera device has switched USB ports.
+ // 2. When in startSilent mode we want to start with audio muted
+ await this._initDeviceList();
+ } catch (error) {
+ logger.warn('initial device list initialization failed', error);
+ }
+
+ // Filter out the local tracks based on various config options, i.e., when user joins muted or is muted by
+ // focus. However, audio track will always be created even though it is not added to the conference since we
+ // want audio related features (noisy mic, talk while muted, etc.) to work even if the mic is muted.
+ const handleInitialTracks = (options, tracks) => {
+ let localTracks = tracks;
+
+ if (options.startWithAudioMuted) {
+ // Always add the track on Safari because of a known issue where audio playout doesn't happen
+ // if the user joins audio and video muted, i.e., if there is no local media capture.
+ if (browser.isWebKitBased()) {
+ this.muteAudio(true, true);
+ } else {
+ localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
+ }
+ }
+
+ return localTracks;
+ };
+ const { dispatch, getState } = APP.store;
+ const createLocalTracksStart = window.performance.now();
+
+ connectionTimes['conference.init.createLocalTracks.start'] = createLocalTracksStart;
+
+ logger.debug(`(TIME) createInitialLocalTracks: ${createLocalTracksStart} `);
+
+ const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions, true);
+
+ tryCreateLocalTracks.then(tr => {
+ const createLocalTracksEnd = window.performance.now();
+
+ connectionTimes['conference.init.createLocalTracks.end'] = createLocalTracksEnd;
+ logger.debug(`(TIME) createInitialLocalTracks finished: ${createLocalTracksEnd} `);
+ const tracks = handleInitialTracks(initialOptions, tr);
+
+ this._initDeviceList(true);
+
+ const { initialGUMPromise } = getState()['features/base/media'];
+
+ if (isPrejoinPageVisible(getState())) {
+ dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
+
+ // Since the conference is not yet created in redux this function will execute synchronous
+ // which will guarantee us that the local tracks are added to redux before we proceed.
+ initPrejoin(tracks, errors, dispatch);
+
+ connectionTimes['conference.init.end'] = window.performance.now();
+
+ // resolve the initialGUMPromise in case connect have finished so that we can proceed to join.
+ if (initialGUMPromise) {
+ logger.debug('Resolving the initialGUM promise! (prejoinVisible=true)');
+ initialGUMPromise.resolve({
+ tracks,
+ errors
+ });
+ }
+
+ logger.debug('Clear the initialGUM promise! (prejoinVisible=true)');
+
+ // For prejoin we don't need the initial GUM promise since the tracks are already added to the store
+ // via initPrejoin
+ dispatch(setInitialGUMPromise());
+ } else {
+ APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
+ setGUMPendingStateOnFailedTracks(tracks, APP.store.dispatch);
+
+ connectionTimes['conference.init.end'] = window.performance.now();
+ if (initialGUMPromise) {
+ logger.debug('Resolving the initialGUM promise!');
+ initialGUMPromise.resolve({
+ tracks,
+ errors
+ });
+ }
+ }
+ });
+
+ if (shouldDispatchConnect) {
+ logger.info('Dispatching connect from init since prejoin is not visible.');
+ dispatch(connect());
+ }
+ },
+
+ /**
+ * Check if id is id of the local user.
+ * @param {string} id id to check
+ * @returns {boolean}
+ */
+ isLocalId(id) {
+ return this.getMyUserId() === id;
+ },
+
+ /**
+ * Tells whether the local video is muted or not.
+ * @return {boolean}
+ */
+ isLocalVideoMuted() {
+ // If the tracks are not ready, read from base/media state
+ return this._localTracksInitialized
+ ? isLocalTrackMuted(APP.store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO)
+ : isVideoMutedByUser(APP.store);
+ },
+
+ /**
+ * Verify if there is an ongoing system audio sharing session and apply to the provided track
+ * as a AudioMixer effect.
+ *
+ * @param {*} localAudioTrack - track to which system audio track will be applied as an effect, most likely
+ * microphone local audio track.
+ */
+ async _maybeApplyAudioMixerEffect(localAudioTrack) {
+
+ // At the time of writing this comment there were two separate flows for toggling screen-sharing
+ // and system audio sharing, the first is the legacy method using the functionality from conference.js
+ // the second is used when both sendMultipleVideoStreams and sourceNameSignaling flags are set to true.
+ // The second flow uses functionality from base/conference/middleware.web.js.
+ // We check if system audio sharing was done using the first flow by verifying this._desktopAudioStream and
+ // for the second by checking 'features/screen-share' state.
+ const { desktopAudioTrack } = APP.store.getState()['features/screen-share'];
+ const currentDesktopAudioTrack = this._desktopAudioStream || desktopAudioTrack;
+
+ // If system audio is already being sent, mix it with the provided audio track.
+ if (currentDesktopAudioTrack) {
+ // In case system audio sharing was done in the absence of an initial mic audio track, there is no
+ // AudioMixerEffect so we have to remove system audio track from the room before setting it as an effect.
+ await room.replaceTrack(currentDesktopAudioTrack, null);
+ this._mixerEffect = new AudioMixerEffect(currentDesktopAudioTrack);
+ logger.debug('Mixing new audio track with existing screen audio track.');
+ await localAudioTrack.setEffect(this._mixerEffect);
+ }
+ },
+
+ /**
+ * Simulates toolbar button click for audio mute. Used by shortcuts and API.
+ *
+ * @param {boolean} mute true for mute and false for unmute.
+ * dialogs in case of media permissions error.
+ * @returns {Promise}
+ */
+ async muteAudio(mute) {
+ const state = APP.store.getState();
+
+ if (!mute
+ && isUserInteractionRequiredForUnmute(state)) {
+ logger.error('Unmuting audio requires user interaction');
+
+ return;
+ }
+
+ await APP.store.dispatch(setAudioMuted(mute, true));
+ },
+
+ /**
+ * Returns whether local audio is muted or not.
+ * @returns {boolean}
+ */
+ isLocalAudioMuted() {
+ // If the tracks are not ready, read from base/media state
+ return this._localTracksInitialized
+ ? isLocalTrackMuted(
+ APP.store.getState()['features/base/tracks'],
+ MEDIA_TYPE.AUDIO)
+ : Boolean(
+ APP.store.getState()['features/base/media'].audio.muted);
+ },
+
+ /**
+ * Simulates toolbar button click for audio mute. Used by shortcuts
+ * and API.
+ * @param {boolean} [showUI] when set to false will not display any error
+ * dialogs in case of media permissions error.
+ */
+ toggleAudioMuted(showUI = true) {
+ this.muteAudio(!this.isLocalAudioMuted(), showUI);
+ },
+
+ /**
+ * Simulates toolbar button click for video mute. Used by shortcuts and API.
+ * @param mute true for mute and false for unmute.
+ * dialogs in case of media permissions error.
+ */
+ muteVideo(mute) {
+ const state = APP.store.getState();
+
+ if (!mute
+ && isUserInteractionRequiredForUnmute(state)) {
+ logger.error('Unmuting video requires user interaction');
+
+ return;
+ }
+
+ APP.store.dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.USER, true));
+ },
+
+ /**
+ * Simulates toolbar button click for video mute. Used by shortcuts and API.
+ * @param {boolean} [showUI] when set to false will not display any error
+ * dialogs in case of media permissions error.
+ * @param {boolean} ensureTrack - True if we want to ensure that a new track is
+ * created if missing.
+ */
+ toggleVideoMuted(showUI = true, ensureTrack = false) {
+ const mute = !this.isLocalVideoMuted();
+
+ APP.store.dispatch(handleToggleVideoMuted(mute, showUI, ensureTrack));
+ },
+
+ /**
+ * Retrieve list of ids of conference participants (without local user).
+ * @returns {string[]}
+ */
+ listMembersIds() {
+ return room.getParticipants().map(p => p.getId());
+ },
+
+ /**
+ * Checks whether the participant identified by id is a moderator.
+ * @id id to search for participant
+ * @return {boolean} whether the participant is moderator
+ */
+ isParticipantModerator(id) {
+ const user = room.getParticipantById(id);
+
+ return user && user.isModerator();
+ },
+
+ /**
+ * Retrieve list of conference participants (without local user).
+ * @returns {JitsiParticipant[]}
+ *
+ * NOTE: Used by jitsi-meet-torture!
+ */
+ listMembers() {
+ return room.getParticipants();
+ },
+
+ /**
+ * Used by Jibri to detect when it's alone and the meeting should be terminated.
+ */
+ get membersCount() {
+ return room.getParticipants()
+ .filter(p => !p.isHidden() || !(config.iAmRecorder && p.isHiddenFromRecorder())).length + 1;
+ },
+
+ /**
+ * Get speaker stats that track total dominant speaker time.
+ *
+ * @returns {object} A hash with keys being user ids and values being the
+ * library's SpeakerStats model used for calculating time as dominant
+ * speaker.
+ */
+ getSpeakerStats() {
+ return room.getSpeakerStats();
+ },
+
+ // used by torture currently
+ isJoined() {
+ return room && room.isJoined();
+ },
+ getConnectionState() {
+ return room && room.getConnectionState();
+ },
+
+ /**
+ * Obtains current P2P ICE connection state.
+ * @return {string|null} ICE connection state or null if there's no
+ * P2P connection
+ */
+ getP2PConnectionState() {
+ return room && room.getP2PConnectionState();
+ },
+
+ /**
+ * Starts P2P (for tests only)
+ * @private
+ */
+ _startP2P() {
+ try {
+ room && room.startP2PSession();
+ } catch (error) {
+ logger.error('Start P2P failed', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Stops P2P (for tests only)
+ * @private
+ */
+ _stopP2P() {
+ try {
+ room && room.stopP2PSession();
+ } catch (error) {
+ logger.error('Stop P2P failed', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Checks whether or not our connection is currently in interrupted and
+ * reconnect attempts are in progress.
+ *
+ * @returns {boolean} true if the connection is in interrupted state or
+ * false otherwise.
+ */
+ isConnectionInterrupted() {
+ return room.isConnectionInterrupted();
+ },
+
+ /**
+ * Finds JitsiParticipant for given id.
+ *
+ * @param {string} id participant's identifier(MUC nickname).
+ *
+ * @returns {JitsiParticipant|null} participant instance for given id or
+ * null if not found.
+ */
+ getParticipantById(id) {
+ return room ? room.getParticipantById(id) : null;
+ },
+
+ getMyUserId() {
+ return room && room.myUserId();
+ },
+
+ /**
+ * Will be filled with values only when config.testing.testMode is true.
+ * Its used by torture to check audio levels.
+ */
+ audioLevelsMap: {},
+
+ /**
+ * Returns the stored audio level (stored only if config.debug is enabled)
+ * @param id the id for the user audio level to return (the id value is
+ * returned for the participant using getMyUserId() method)
+ */
+ getPeerSSRCAudioLevel(id) {
+ return this.audioLevelsMap[id];
+ },
+
+ /**
+ * @return {number} the number of participants in the conference with at
+ * least one track.
+ */
+ getNumberOfParticipantsWithTracks() {
+ return room.getParticipants()
+ .filter(p => p.getTracks().length > 0)
+ .length;
+ },
+
+ /**
+ * Returns the stats.
+ */
+ getStats() {
+ return room.connectionQuality.getStats();
+ },
+
+ // end used by torture
+
+ /**
+ * Download logs, a function that can be called from console while
+ * debugging.
+ * @param filename (optional) specify target filename
+ */
+ saveLogs(filename = 'meetlog.json') {
+ // this can be called from console and will not have reference to this
+ // that's why we reference the global var
+ const logs = APP.connection.getLogs();
+
+ downloadJSON(logs, filename);
+ },
+
+ /**
+ * Download app state, a function that can be called from console while debugging.
+ * @param filename (optional) specify target filename
+ */
+ saveState(filename = 'meet-state.json') {
+ downloadJSON(APP.store.getState(), filename);
+ },
+
+ /**
+ * Exposes a Command(s) API on this instance. It is necessitated by (1) the
+ * desire to keep room private to this instance and (2) the need of other
+ * modules to send and receive commands to and from participants.
+ * Eventually, this instance remains in control with respect to the
+ * decision whether the Command(s) API of room (i.e. lib-jitsi-meet's
+ * JitsiConference) is to be used in the implementation of the Command(s)
+ * API of this instance.
+ */
+ commands: {
+ /**
+ * Known custom conference commands.
+ */
+ defaults: commands,
+
+ /**
+ * Receives notifications from other participants about commands aka
+ * custom events (sent by sendCommand or sendCommandOnce methods).
+ * @param command {String} the name of the command
+ * @param handler {Function} handler for the command
+ */
+ addCommandListener() {
+ // eslint-disable-next-line prefer-rest-params
+ room.addCommandListener(...arguments);
+ },
+
+ /**
+ * Removes command.
+ * @param name {String} the name of the command.
+ */
+ removeCommand() {
+ // eslint-disable-next-line prefer-rest-params
+ room.removeCommand(...arguments);
+ },
+
+ /**
+ * Sends command.
+ * @param name {String} the name of the command.
+ * @param values {Object} with keys and values that will be sent.
+ */
+ sendCommand() {
+ // eslint-disable-next-line prefer-rest-params
+ room.sendCommand(...arguments);
+ },
+
+ /**
+ * Sends command one time.
+ * @param name {String} the name of the command.
+ * @param values {Object} with keys and values that will be sent.
+ */
+ sendCommandOnce() {
+ // eslint-disable-next-line prefer-rest-params
+ room.sendCommandOnce(...arguments);
+ }
+ },
+
+ /**
+ * Used by the Breakout Rooms feature to join a breakout room or go back to the main room.
+ */
+ async joinRoom(roomName, options) {
+ APP.store.dispatch(conferenceWillInit());
+
+ // Restore initial state.
+ this._localTracksInitialized = false;
+ this.roomName = roomName;
+
+ const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
+ const localTracks = await tryCreateLocalTracks;
+
+ APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
+ localTracks.forEach(track => {
+ if ((track.isAudioTrack() && this.isLocalAudioMuted())
+ || (track.isVideoTrack() && this.isLocalVideoMuted())) {
+ track.mute();
+ }
+ });
+ this._createRoom(localTracks);
+
+ return new Promise((resolve, reject) => {
+ new ConferenceConnector(resolve, reject, this).connect();
+ });
+ },
+
+ _createRoom(localTracks) {
+ room = APP.connection.initJitsiConference(APP.conference.roomName, this._getConferenceOptions());
+
+ // Filter out the tracks that are muted (except on Safari).
+ let tracks = localTracks;
+
+ if (!browser.isWebKitBased()) {
+ const mutedTrackTypes = [];
+
+ tracks = localTracks.filter(track => {
+ if (!track.isMuted()) {
+ return true;
+ }
+
+ if (track.getVideoType() !== VIDEO_TYPE.DESKTOP) {
+ mutedTrackTypes.push(track.getType());
+ }
+
+ return false;
+ });
+ APP.store.dispatch(gumPending(mutedTrackTypes, IGUMPendingState.NONE));
+ }
+
+ this._room = room; // FIXME do not use this
+
+ APP.store.dispatch(_conferenceWillJoin(room));
+
+ this._setLocalAudioVideoStreams(tracks);
+
+ sendLocalParticipant(APP.store, room);
+
+ this._setupListeners();
+ },
+
+ /**
+ * Sets local video and audio streams.
+ * @param {JitsiLocalTrack[]} tracks=[]
+ * @returns {Promise[]}
+ * @private
+ */
+ _setLocalAudioVideoStreams(tracks = []) {
+ const { dispatch } = APP.store;
+ const pendingGUMDevicesToRemove = [];
+ const promises = tracks.map(track => {
+ if (track.isAudioTrack()) {
+ pendingGUMDevicesToRemove.push(MEDIA_TYPE.AUDIO);
+
+ return this.useAudioStream(track);
+ } else if (track.isVideoTrack()) {
+ logger.debug(`_setLocalAudioVideoStreams is calling useVideoStream with track: ${track}`);
+ pendingGUMDevicesToRemove.push(MEDIA_TYPE.VIDEO);
+
+ return this.useVideoStream(track);
+ }
+
+ logger.error('Ignored not an audio nor a video track: ', track);
+
+ return Promise.resolve();
+
+ });
+
+ return Promise.allSettled(promises).then(() => {
+ if (pendingGUMDevicesToRemove.length > 0) {
+ dispatch(gumPending(pendingGUMDevicesToRemove, IGUMPendingState.NONE));
+ }
+
+ this._localTracksInitialized = true;
+ logger.log(`Initialized with ${tracks.length} local tracks`);
+ });
+ },
+
+ _getConferenceOptions() {
+ const options = getConferenceOptions(APP.store.getState());
+
+ options.createVADProcessor = createRnnoiseProcessor;
+
+ return options;
+ },
+
+ /**
+ * Start using provided video stream.
+ * Stops previous video stream.
+ * @param {JitsiLocalTrack} newTrack - new track to use or null
+ * @returns {Promise}
+ */
+ useVideoStream(newTrack) {
+ logger.debug(`useVideoStream: ${newTrack}`);
+
+ return new Promise((resolve, reject) => {
+ _replaceLocalVideoTrackQueue.enqueue(onFinish => {
+ const state = APP.store.getState();
+ const oldTrack = getLocalJitsiVideoTrack(state);
+
+ logger.debug(`useVideoStream: Replacing ${oldTrack} with ${newTrack}`);
+
+ if (oldTrack === newTrack || (!oldTrack && !newTrack)) {
+ resolve();
+ onFinish();
+
+ return;
+ }
+
+ // Add the track to the conference if there is no existing track, replace it otherwise.
+ const trackAction = oldTrack
+ ? replaceLocalTrack(oldTrack, newTrack, room)
+ : addLocalTrack(newTrack);
+
+ APP.store.dispatch(trackAction)
+ .then(() => {
+ this.setVideoMuteStatus();
+ })
+ .then(resolve)
+ .catch(error => {
+ logger.error(`useVideoStream failed: ${error}`);
+ reject(error);
+ })
+ .then(onFinish);
+ });
+ });
+ },
+
+ /**
+ * Start using provided audio stream.
+ * Stops previous audio stream.
+ * @param {JitsiLocalTrack} newTrack - new track to use or null
+ * @returns {Promise}
+ */
+ useAudioStream(newTrack) {
+ return new Promise((resolve, reject) => {
+ _replaceLocalAudioTrackQueue.enqueue(onFinish => {
+ const oldTrack = getLocalJitsiAudioTrack(APP.store.getState());
+
+ if (oldTrack === newTrack) {
+ resolve();
+ onFinish();
+
+ return;
+ }
+
+ APP.store.dispatch(replaceLocalTrack(oldTrack, newTrack, room))
+ .then(() => {
+ this.updateAudioIconEnabled();
+ })
+ .then(resolve)
+ .catch(reject)
+ .then(onFinish);
+ });
+ });
+ },
+
+ /**
+ * Returns whether or not the conference is currently in audio only mode.
+ *
+ * @returns {boolean}
+ */
+ isAudioOnly() {
+ return Boolean(APP.store.getState()['features/base/audio-only'].enabled);
+ },
+
+ /**
+ * This fields stores a handler which will create a Promise which turns off
+ * the screen sharing and restores the previous video state (was there
+ * any video, before switching to screen sharing ? was it muted ?).
+ *
+ * Once called this fields is cleared to null .
+ * @type {Function|null}
+ */
+ _untoggleScreenSharing: null,
+
+ /**
+ * Creates a Promise which turns off the screen sharing and restores
+ * the previous state described by the arguments.
+ *
+ * This method is bound to the appropriate values, after switching to screen
+ * sharing and stored in {@link _untoggleScreenSharing}.
+ *
+ * @param {boolean} didHaveVideo indicates if there was a camera video being
+ * used, before switching to screen sharing.
+ * @param {boolean} ignoreDidHaveVideo indicates if the camera video should be
+ * ignored when switching screen sharing off.
+ * @return {Promise} resolved after the screen sharing is turned off, or
+ * rejected with some error (no idea what kind of error, possible GUM error)
+ * in case it fails.
+ * @private
+ */
+ async _turnScreenSharingOff(didHaveVideo, ignoreDidHaveVideo) {
+ this._untoggleScreenSharing = null;
+
+ APP.store.dispatch(stopReceiver());
+
+ this._stopProxyConnection();
+
+ APP.store.dispatch(toggleScreenshotCaptureSummary(false));
+ const tracks = APP.store.getState()['features/base/tracks'];
+ const duration = getLocalVideoTrack(tracks)?.jitsiTrack.getDuration() ?? 0;
+
+ // If system audio was also shared stop the AudioMixerEffect and dispose of the desktop audio track.
+ if (this._mixerEffect) {
+ const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
+
+ await localAudio.setEffect(undefined);
+ await this._desktopAudioStream.dispose();
+ this._mixerEffect = undefined;
+ this._desktopAudioStream = undefined;
+
+ // In case there was no local audio when screen sharing was started the fact that we set the audio stream to
+ // null will take care of the desktop audio stream cleanup.
+ } else if (this._desktopAudioStream) {
+ await room.replaceTrack(this._desktopAudioStream, null);
+ this._desktopAudioStream.dispose();
+ this._desktopAudioStream = undefined;
+ }
+
+ APP.store.dispatch(setScreenAudioShareState(false));
+ let promise;
+
+ if (didHaveVideo && !ignoreDidHaveVideo) {
+ promise = createLocalTracksF({ devices: [ 'video' ] })
+ .then(([ stream ]) => {
+ logger.debug(`_turnScreenSharingOff using ${stream} for useVideoStream`);
+
+ return this.useVideoStream(stream);
+ })
+ .catch(error => {
+ logger.error('failed to switch back to local video', error);
+
+ return this.useVideoStream(null).then(() =>
+
+ // Still fail with the original err
+ Promise.reject(error)
+ );
+ });
+ } else {
+ promise = this.useVideoStream(null);
+ }
+
+ return promise.then(
+ () => {
+ sendAnalytics(createScreenSharingEvent('stopped',
+ duration === 0 ? null : duration));
+ logger.info('Screen sharing stopped.');
+ },
+ error => {
+ logger.error(`_turnScreenSharingOff failed: ${error}`);
+
+ throw error;
+ });
+ },
+
+ /**
+ * Creates desktop (screensharing) {@link JitsiLocalTrack}
+ *
+ * @return {Promise.} - A Promise resolved with
+ * {@link JitsiLocalTrack} for the screensharing or rejected with
+ * {@link JitsiTrackError}.
+ *
+ * @private
+ */
+ _createDesktopTrack() {
+ const didHaveVideo = !this.isLocalVideoMuted();
+
+ const getDesktopStreamPromise = createLocalTracksF({
+ desktopSharingSourceDevice: config._desktopSharingSourceDevice,
+ devices: [ 'desktop' ]
+ });
+
+ return getDesktopStreamPromise.then(desktopStreams => {
+ // Stores the "untoggle" handler which remembers whether was
+ // there any video before and whether was it muted.
+ this._untoggleScreenSharing
+ = this._turnScreenSharingOff.bind(this, didHaveVideo);
+
+ const desktopAudioStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
+
+ if (desktopAudioStream) {
+ desktopAudioStream.on(
+ JitsiTrackEvents.LOCAL_TRACK_STOPPED,
+ () => {
+ logger.debug('Local screensharing audio track stopped.');
+
+ // Handle case where screen share was stopped from the browsers 'screen share in progress'
+ // window. If audio screen sharing is stopped via the normal UX flow this point shouldn't
+ // be reached.
+ isScreenAudioShared(APP.store.getState())
+ && this._untoggleScreenSharing
+ && this._untoggleScreenSharing();
+ }
+ );
+ }
+
+ return desktopStreams;
+ }, error => {
+ throw error;
+ });
+ },
+
+ /**
+ * Setup interaction between conference and UI.
+ */
+ _setupListeners() {
+ // add local streams when joined to the conference
+ room.on(JitsiConferenceEvents.CONFERENCE_JOINED, () => {
+ this._onConferenceJoined();
+ });
+ room.on(
+ JitsiConferenceEvents.CONFERENCE_JOIN_IN_PROGRESS,
+ () => APP.store.dispatch(conferenceJoinInProgress(room)));
+
+ room.on(
+ JitsiConferenceEvents.CONFERENCE_LEFT,
+ (...args) => {
+ APP.store.dispatch(conferenceTimestampChanged(0));
+ APP.store.dispatch(conferenceLeft(room, ...args));
+ });
+
+ room.on(
+ JitsiConferenceEvents.CONFERENCE_UNIQUE_ID_SET,
+ (...args) => {
+ // Preserve the sessionId so that the value is accessible even after room
+ // is disconnected.
+ room.sessionId = room.getMeetingUniqueId();
+ APP.store.dispatch(conferenceUniqueIdSet(room, ...args));
+ });
+
+ // we want to ignore this event in case of tokenAuthUrl config
+ // we are deprecating this and at some point will get rid of it
+ if (!config.tokenAuthUrl) {
+ room.on(
+ JitsiConferenceEvents.AUTH_STATUS_CHANGED,
+ (authEnabled, authLogin) =>
+ APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
+ }
+
+ room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, user => {
+ APP.store.dispatch(updateRemoteParticipantFeatures(user));
+ });
+ room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
+ if (config.iAmRecorder && user.isHiddenFromRecorder()) {
+ return;
+ }
+
+ // The logic shared between RN and web.
+ commonUserJoinedHandling(APP.store, room, user);
+
+ if (user.isHidden()) {
+ return;
+ }
+
+ APP.store.dispatch(updateRemoteParticipantFeatures(user));
+ logger.log(`USER ${id} connected:`, user);
+ APP.UI.addUser(user);
+ });
+
+ room.on(JitsiConferenceEvents.USER_LEFT, (id, user) => {
+ // The logic shared between RN and web.
+ commonUserLeftHandling(APP.store, room, user);
+
+ if (user.isHidden()) {
+ return;
+ }
+
+ logger.log(`USER ${id} LEFT:`, user);
+ });
+
+ room.on(JitsiConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
+ APP.store.dispatch(participantPresenceChanged(id, status));
+
+ const user = room.getParticipantById(id);
+
+ if (user) {
+ APP.UI.updateUserStatus(user, status);
+ }
+ });
+
+ room.on(JitsiConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
+ if (this.isLocalId(id)) {
+ logger.info(`My role changed, new role: ${role}`);
+
+ if (role === 'moderator') {
+ APP.store.dispatch(maybeSetLobbyChatMessageListener());
+ }
+
+ APP.store.dispatch(localParticipantRoleChanged(role));
+ } else {
+ APP.store.dispatch(participantRoleChanged(id, role));
+ }
+ });
+
+ room.on(JitsiConferenceEvents.TRACK_ADDED, track => {
+ if (!track || track.isLocal()) {
+ return;
+ }
+
+ if (config.iAmRecorder) {
+ const participant = room.getParticipantById(track.getParticipantId());
+
+ if (participant.isHiddenFromRecorder()) {
+ return;
+ }
+ }
+
+ APP.store.dispatch(trackAdded(track));
+ });
+
+ room.on(JitsiConferenceEvents.TRACK_REMOVED, track => {
+ if (!track || track.isLocal()) {
+ return;
+ }
+
+ APP.store.dispatch(trackRemoved(track));
+ });
+
+ room.on(JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED, (id, lvl) => {
+ const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
+ let newLvl = lvl;
+
+ if (this.isLocalId(id)) {
+ APP.store.dispatch(localParticipantAudioLevelChanged(lvl));
+ }
+
+ if (this.isLocalId(id) && localAudio?.isMuted()) {
+ newLvl = 0;
+ }
+
+ if (config.testing?.testMode) {
+ this.audioLevelsMap[id] = newLvl;
+ if (config.testing?.debugAudioLevels) {
+ logger.log(`AudioLevel:${id}/${newLvl}`);
+ }
+ }
+
+ APP.UI.setAudioLevel(id, newLvl);
+ });
+
+ room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
+ if (participantThatMutedUs) {
+ APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
+ }
+ });
+
+ room.on(JitsiConferenceEvents.TRACK_UNMUTE_REJECTED, track => APP.store.dispatch(destroyLocalTracks(track)));
+
+ room.on(JitsiConferenceEvents.SUBJECT_CHANGED,
+ subject => APP.store.dispatch(conferenceSubjectChanged(subject)));
+
+ room.on(
+ JitsiConferenceEvents.LAST_N_ENDPOINTS_CHANGED,
+ (leavingIds, enteringIds) =>
+ APP.UI.handleLastNEndpoints(leavingIds, enteringIds));
+
+ room.on(
+ JitsiConferenceEvents.P2P_STATUS,
+ (jitsiConference, p2p) =>
+ APP.store.dispatch(p2pStatusChanged(p2p)));
+
+ room.on(
+ JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
+ (dominant, previous, silence) => {
+ APP.store.dispatch(dominantSpeakerChanged(dominant, previous, Boolean(silence), room));
+ });
+
+ room.on(
+ JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
+ conferenceTimestamp => {
+ APP.store.dispatch(conferenceTimestampChanged(conferenceTimestamp));
+ APP.API.notifyConferenceCreatedTimestamp(conferenceTimestamp);
+ }
+ );
+
+ room.on(
+ JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
+ (id, displayName) => {
+ const formattedDisplayName
+ = getNormalizedDisplayName(displayName);
+ const state = APP.store.getState();
+ const {
+ defaultRemoteDisplayName
+ } = state['features/base/config'];
+
+ APP.store.dispatch(participantUpdated({
+ conference: room,
+ id,
+ name: formattedDisplayName
+ }));
+
+ const virtualScreenshareParticipantId = getVirtualScreenshareParticipantByOwnerId(state, id)?.id;
+
+ if (virtualScreenshareParticipantId) {
+ APP.store.dispatch(
+ screenshareParticipantDisplayNameChanged(virtualScreenshareParticipantId, formattedDisplayName)
+ );
+ }
+
+ APP.API.notifyDisplayNameChanged(id, {
+ displayName: formattedDisplayName,
+ formattedDisplayName:
+ appendSuffix(
+ formattedDisplayName
+ || defaultRemoteDisplayName)
+ });
+ }
+ );
+
+ room.on(
+ JitsiConferenceEvents.SILENT_STATUS_CHANGED,
+ (id, isSilent) => {
+ APP.store.dispatch(participantUpdated({
+ conference: room,
+ id,
+ isSilent
+ }));
+ }
+ );
+
+ room.on(
+ JitsiConferenceEvents.BOT_TYPE_CHANGED,
+ (id, botType) => {
+
+ APP.store.dispatch(participantUpdated({
+ conference: room,
+ id,
+ botType
+ }));
+ }
+ );
+
+ room.on(
+ JitsiConferenceEvents.TRANSCRIPTION_STATUS_CHANGED,
+ (status, id, abruptly) => {
+ if (status === JitsiMeetJS.constants.transcriptionStatus.ON) {
+ APP.store.dispatch(transcriberJoined(id));
+ } else if (status === JitsiMeetJS.constants.transcriptionStatus.OFF) {
+ APP.store.dispatch(transcriberLeft(id, abruptly));
+ }
+ });
+
+ room.on(
+ JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
+ (participant, data) => {
+ APP.store.dispatch(endpointMessageReceived(participant, data));
+ if (data?.name === ENDPOINT_TEXT_MESSAGE_NAME) {
+ APP.API.notifyEndpointTextMessageReceived({
+ senderInfo: {
+ jid: participant.getJid(),
+ id: participant.getId()
+ },
+ eventData: data
+ });
+ }
+ });
+
+ room.on(
+ JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
+ (id, data) => {
+ APP.store.dispatch(nonParticipantMessageReceived(id, data));
+ APP.API.notifyNonParticipantMessageReceived(id, data);
+ });
+
+ room.on(
+ JitsiConferenceEvents.LOCK_STATE_CHANGED,
+ (...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
+
+ room.on(
+ JitsiConferenceEvents.PROPERTIES_CHANGED,
+ properties => APP.store.dispatch(conferencePropertiesChanged(properties)));
+
+ room.on(JitsiConferenceEvents.KICKED, (participant, reason, isReplaced) => {
+ if (isReplaced) {
+ // this event triggers when the local participant is kicked, `participant`
+ // is the kicker. In replace participant case, kicker is undefined,
+ // as the server initiated it. We mark in store the local participant
+ // as being replaced based on jwt.
+ const localParticipant = getLocalParticipant(APP.store.getState());
+
+ APP.store.dispatch(participantUpdated({
+ conference: room,
+ id: localParticipant.id,
+ isReplaced
+ }));
+
+ // we send readyToClose when kicked participant is replace so that
+ // embedding app can choose to dispose the iframe API on the handler.
+ APP.API.notifyReadyToClose();
+ }
+ APP.store.dispatch(kickedOut(room, participant));
+ });
+
+ room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
+ APP.store.dispatch(participantKicked(kicker, kicked));
+ });
+
+ room.on(JitsiConferenceEvents.PARTICIPANT_SOURCE_UPDATED,
+ jitsiParticipant => {
+ APP.store.dispatch(participantSourcesUpdated(jitsiParticipant));
+ });
+
+ room.on(JitsiConferenceEvents.SUSPEND_DETECTED, () => {
+ APP.store.dispatch(suspendDetected());
+ });
+
+ room.on(
+ JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
+ disableAudioMuteChange => {
+ APP.store.dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
+ });
+ room.on(
+ JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
+ disableVideoMuteChange => {
+ APP.store.dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
+ });
+
+ room.on(
+ JitsiE2ePingEvents.E2E_RTT_CHANGED,
+ (...args) => APP.store.dispatch(e2eRttChanged(...args)));
+
+ room.addCommandListener(this.commands.defaults.ETHERPAD,
+ ({ value }) => {
+ APP.UI.initEtherpad(value);
+ }
+ );
+
+ room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => {
+ APP.store.dispatch(participantUpdated({
+ conference: room,
+ id: from,
+ email: data.value
+ }));
+ });
+
+ room.addCommandListener(
+ this.commands.defaults.AVATAR_URL,
+ (data, from) => {
+ const participant = getParticipantByIdOrUndefined(APP.store, from);
+
+ // if already set from presence(jwt), skip the command processing
+ if (!participant?.avatarURL) {
+ APP.store.dispatch(
+ participantUpdated({
+ conference: room,
+ id: from,
+ avatarURL: data.value
+ }));
+ }
+ });
+
+ room.on(
+ JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
+ ({ audio, video }) => {
+ APP.store.dispatch(onStartMutedPolicyChanged(audio, video));
+
+ const state = APP.store.getState();
+
+ updateTrackMuteState(state, APP.store.dispatch, true);
+ updateTrackMuteState(state, APP.store.dispatch, false);
+ }
+ );
+
+ room.on(
+ JitsiConferenceEvents.DATA_CHANNEL_OPENED, () => {
+ APP.store.dispatch(dataChannelOpened());
+ APP.store.dispatch(hideNotification(DATA_CHANNEL_CLOSED_NOTIFICATION_ID));
+ }
+ );
+
+ room.on(
+ JitsiConferenceEvents.DATA_CHANNEL_CLOSED, ev => {
+ const state = APP.store.getState();
+ const { dataChannelOpen } = state['features/base/conference'];
+ const timeout = typeof dataChannelOpen === 'undefined' ? 15000 : 60000;
+
+ // Show the notification only when the data channel connection doesn't get re-established in 60 secs if
+ // it was already established at the beginning of the call, show it sooner otherwise. This notification
+ // can be confusing and alarming to users even when there is no significant impact to user experience
+ // if the the reconnect happens immediately.
+ setTimeout(() => {
+ const { dataChannelOpen: open } = APP.store.getState()['features/base/conference'];
+
+ if (!open) {
+ const descriptionKey = getSsrcRewritingFeatureFlag(state)
+ ? 'notify.dataChannelClosedDescriptionWithAudio' : 'notify.dataChannelClosedDescription';
+ const titleKey = getSsrcRewritingFeatureFlag(state)
+ ? 'notify.dataChannelClosedWithAudio' : 'notify.dataChannelClosed';
+
+ APP.store.dispatch(dataChannelClosed(ev.code, ev.reason));
+ APP.store.dispatch(showWarningNotification({
+ descriptionKey,
+ titleKey,
+ uid: DATA_CHANNEL_CLOSED_NOTIFICATION_ID
+ }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
+ }
+ }, timeout);
+ }
+ );
+
+ room.on(JitsiConferenceEvents.PERMISSIONS_RECEIVED, p => {
+ const localParticipant = getLocalParticipant(APP.store.getState());
+
+ APP.store.dispatch(participantUpdated({
+ id: localParticipant.id,
+ local: true,
+ features: p
+ }));
+ });
+ },
+
+ /**
+ * Handles audio device changes.
+ *
+ * @param {string} cameraDeviceId - The new device id.
+ * @returns {Promise}
+ */
+ async onAudioDeviceChanged(micDeviceId) {
+ const audioWasMuted = this.isLocalAudioMuted();
+
+ // Disable noise suppression if it was enabled on the previous track.
+ await APP.store.dispatch(setNoiseSuppressionEnabled(false));
+
+ // When the 'default' mic needs to be selected, we need to pass the real device id to gUM instead of
+ // 'default' in order to get the correct MediaStreamTrack from chrome because of the following bug.
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=997689.
+ const isDefaultMicSelected = micDeviceId === 'default';
+ const selectedDeviceId = isDefaultMicSelected
+ ? getDefaultDeviceId(APP.store.getState(), 'audioInput')
+ : micDeviceId;
+
+ logger.info(`Switching audio input device to ${selectedDeviceId}`);
+ sendAnalytics(createDeviceChangedEvent('audio', 'input'));
+ createLocalTracksF({
+ devices: [ 'audio' ],
+ micDeviceId: selectedDeviceId
+ })
+ .then(([ stream ]) => {
+ // if audio was muted before changing the device, mute
+ // with the new device
+ if (audioWasMuted) {
+ return stream.mute()
+ .then(() => stream);
+ }
+
+ return stream;
+ })
+ .then(async stream => {
+ await this._maybeApplyAudioMixerEffect(stream);
+
+ return this.useAudioStream(stream);
+ })
+ .then(() => {
+ const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
+
+ if (localAudio && isDefaultMicSelected) {
+ // workaround for the default device to be shown as selected in the
+ // settings even when the real device id was passed to gUM because of the
+ // above mentioned chrome bug.
+ localAudio._realDeviceId = localAudio.deviceId = 'default';
+ }
+ })
+ .catch(err => {
+ logger.error(`Failed to switch to selected audio input device ${selectedDeviceId}, error=${err}`);
+ APP.store.dispatch(notifyMicError(err));
+ });
+ },
+
+ /**
+ * Handles video device changes.
+ *
+ * @param {string} cameraDeviceId - The new device id.
+ * @returns {void}
+ */
+ onVideoDeviceChanged(cameraDeviceId) {
+ const videoWasMuted = this.isLocalVideoMuted();
+ const localVideoTrack = getLocalJitsiVideoTrack(APP.store.getState());
+
+ if (localVideoTrack?.getDeviceId() === cameraDeviceId) {
+ return;
+ }
+
+ sendAnalytics(createDeviceChangedEvent('video', 'input'));
+
+ createLocalTracksF({
+ devices: [ 'video' ],
+ cameraDeviceId
+ })
+ .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 => {
+ logger.info(`Switching the local video device to ${cameraDeviceId}.`);
+
+ return this.useVideoStream(stream);
+ })
+ .catch(error => {
+ logger.error(`Failed to switch to selected camera:${cameraDeviceId}, error:${error}`);
+
+ return APP.store.dispatch(notifyCameraError(error));
+ });
+ },
+
+ /**
+ * Handles audio only changes.
+ */
+ onToggleAudioOnly() {
+ // Immediately update the UI by having remote videos and the large video update themselves.
+ const displayedUserId = APP.UI.getLargeVideoID();
+
+ if (displayedUserId) {
+ APP.UI.updateLargeVideo(displayedUserId, true);
+ }
+ },
+
+ /**
+ * Cleanups local conference on suspend.
+ */
+ onSuspendDetected() {
+ // After wake up, we will be in a state where conference is left
+ // there will be dialog shown to user.
+ // We do not want video/audio as we show an overlay and after it
+ // user need to rejoin or close, while waking up we can detect
+ // camera wakeup as a problem with device.
+ // We also do not care about device change, which happens
+ // on resume after suspending PC.
+ if (this.deviceChangeListener) {
+ JitsiMeetJS.mediaDevices.removeEventListener(
+ JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
+ this.deviceChangeListener);
+ }
+ },
+
+ /**
+ * Callback invoked when the conference has been successfully joined.
+ * Initializes the UI and various other features.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onConferenceJoined() {
+ const { dispatch } = APP.store;
+
+ APP.UI.initConference();
+
+ dispatch(conferenceJoined(room));
+
+ const jwt = APP.store.getState()['features/base/jwt'];
+
+ if (jwt?.user?.hiddenFromRecorder) {
+ dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
+ dispatch(muteLocal(true, MEDIA_TYPE.VIDEO));
+ dispatch(setAudioUnmutePermissions(true, true));
+ dispatch(setVideoUnmutePermissions(true, true));
+ }
+ },
+
+ /**
+ * Updates the list of current devices.
+ * @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
+ * @private
+ * @returns {Promise}
+ */
+ _initDeviceList(setDeviceListChangeHandler = false) {
+ const { mediaDevices } = JitsiMeetJS;
+
+ if (mediaDevices.isDeviceChangeAvailable()) {
+ if (setDeviceListChangeHandler) {
+ this.deviceChangeListener = devices =>
+ window.setTimeout(() => this._onDeviceListChanged(devices), 0);
+ mediaDevices.addEventListener(
+ JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
+ this.deviceChangeListener);
+ }
+
+ const { dispatch } = APP.store;
+
+ return dispatch(getAvailableDevices())
+ .then(() => {
+ this.updateAudioIconEnabled();
+ this.updateVideoIconEnabled();
+ });
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Event listener for JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED to
+ * handle change of available media devices.
+ * @private
+ * @param {MediaDeviceInfo[]} devices
+ * @returns {Promise}
+ */
+ async _onDeviceListChanged(devices) {
+ const state = APP.store.getState();
+ const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices);
+ const oldDevices = state['features/base/devices'].availableDevices;
+
+ if (!areDevicesDifferent(flattenAvailableDevices(oldDevices), filteredDevices)) {
+ return Promise.resolve();
+ }
+
+ logDevices(ignoredDevices, 'Ignored devices on device list changed:');
+
+ const localAudio = getLocalJitsiAudioTrack(state);
+ const localVideo = getLocalJitsiVideoTrack(state);
+
+ APP.store.dispatch(updateDeviceList(filteredDevices));
+
+ // Firefox users can choose their preferred device in the gUM prompt. In that case
+ // we should respect that and not attempt to switch to the preferred device from
+ // our settings.
+ const newLabelsOnly = mediaDeviceHelper.newDeviceListAddedLabelsOnly(oldDevices, filteredDevices);
+ const newDevices
+ = mediaDeviceHelper.getNewMediaDevicesAfterDeviceListChanged(
+ filteredDevices,
+ localVideo,
+ localAudio,
+ newLabelsOnly);
+ const promises = [];
+ const requestedInput = {
+ audio: Boolean(newDevices.audioinput),
+ video: Boolean(newDevices.videoinput)
+ };
+
+ if (typeof newDevices.audiooutput !== 'undefined') {
+ const { dispatch } = APP.store;
+ const setAudioOutputPromise
+ = setAudioOutputDeviceId(newDevices.audiooutput, dispatch)
+ .catch(err => {
+ logger.error(`Failed to set the audio output device to ${newDevices.audiooutput} - ${err}`);
+ });
+
+ promises.push(setAudioOutputPromise);
+ }
+
+ // Handles the use case when the default device is changed (we are always stopping the streams because it's
+ // simpler):
+ // If the default device is changed we need to first stop the local streams and then call GUM. Otherwise GUM
+ // will return a stream using the old default device.
+ if (requestedInput.audio && localAudio) {
+ localAudio.stopStream();
+ }
+
+ if (requestedInput.video && localVideo) {
+ localVideo.stopStream();
+ }
+
+ // Let's handle unknown/non-preferred devices
+ const newAvailDevices = APP.store.getState()['features/base/devices'].availableDevices;
+ let newAudioDevices = [];
+ let oldAudioDevices = [];
+
+ if (typeof newDevices.audiooutput === 'undefined') {
+ newAudioDevices = newAvailDevices.audioOutput;
+ oldAudioDevices = oldDevices.audioOutput;
+ }
+
+ if (!requestedInput.audio) {
+ newAudioDevices = newAudioDevices.concat(newAvailDevices.audioInput);
+ oldAudioDevices = oldAudioDevices.concat(oldDevices.audioInput);
+ }
+
+ // check for audio
+ if (newAudioDevices.length > 0) {
+ APP.store.dispatch(checkAndNotifyForNewDevice(newAudioDevices, oldAudioDevices));
+ }
+
+ // check for video
+ if (requestedInput.video) {
+ APP.store.dispatch(checkAndNotifyForNewDevice(newAvailDevices.videoInput, oldDevices.videoInput));
+ }
+
+ // When the 'default' mic needs to be selected, we need to pass the real device id to gUM instead of 'default'
+ // in order to get the correct MediaStreamTrack from chrome because of the following bug.
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=997689
+ const hasDefaultMicChanged = newDevices.audioinput === 'default';
+
+ // When the local video is muted and a preferred device is connected, update the settings and remove the track
+ // from the conference. A new track will be created and replaced when the user unmutes their camera.
+ if (requestedInput.video && this.isLocalVideoMuted()) {
+ APP.store.dispatch(updateSettings({
+ cameraDeviceId: newDevices.videoinput
+ }));
+ requestedInput.video = false;
+ delete newDevices.videoinput;
+
+ // Remove the track from the conference.
+ if (localVideo) {
+ await this.useVideoStream(null);
+ logger.debug('_onDeviceListChanged: Removed the current video track.');
+ }
+ }
+
+ // When the local audio is muted and a preferred device is connected, update the settings and remove the track
+ // from the conference. A new track will be created and replaced when the user unmutes their mic.
+ if (requestedInput.audio && this.isLocalAudioMuted()) {
+ APP.store.dispatch(updateSettings({
+ micDeviceId: newDevices.audioinput
+ }));
+ requestedInput.audio = false;
+ delete newDevices.audioinput;
+
+ // Remove the track from the conference.
+ if (localAudio) {
+ await this.useAudioStream(null);
+ logger.debug('_onDeviceListChanged: Removed the current audio track.');
+ }
+ }
+
+ // Create the tracks and replace them only if the user is unmuted.
+ if (requestedInput.audio || requestedInput.video) {
+ let tracks = [];
+ const realAudioDeviceId = hasDefaultMicChanged
+ ? getDefaultDeviceId(APP.store.getState(), 'audioInput') : newDevices.audioinput;
+
+ try {
+ tracks = await mediaDeviceHelper.createLocalTracksAfterDeviceListChanged(
+ createLocalTracksF,
+ requestedInput.video ? newDevices.videoinput : null,
+ requestedInput.audio ? realAudioDeviceId : null
+ );
+ } catch (error) {
+ logger.error(`Track creation failed on device change, ${error}`);
+
+ return Promise.reject(error);
+ }
+
+ for (const track of tracks) {
+ if (track.isAudioTrack()) {
+ promises.push(
+ this.useAudioStream(track)
+ .then(() => {
+ hasDefaultMicChanged && (track._realDeviceId = track.deviceId = 'default');
+ }));
+ } else {
+ promises.push(
+ this.useVideoStream(track));
+ }
+ }
+ }
+
+ return Promise.all(promises)
+ .then(() => {
+ this.updateAudioIconEnabled();
+ this.updateVideoIconEnabled();
+ });
+ },
+
+ /**
+ * Determines whether or not the audio button should be enabled.
+ */
+ updateAudioIconEnabled() {
+ const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
+ const audioMediaDevices = APP.store.getState()['features/base/devices'].availableDevices.audioInput;
+ const audioDeviceCount = audioMediaDevices ? audioMediaDevices.length : 0;
+
+ // The audio functionality is considered available if there are any
+ // audio devices detected or if the local audio stream already exists.
+ const available = audioDeviceCount > 0 || Boolean(localAudio);
+
+ APP.store.dispatch(setAudioAvailable(available));
+ },
+
+ /**
+ * Determines whether or not the video button should be enabled.
+ */
+ updateVideoIconEnabled() {
+ const videoMediaDevices
+ = APP.store.getState()['features/base/devices'].availableDevices.videoInput;
+ const videoDeviceCount
+ = videoMediaDevices ? videoMediaDevices.length : 0;
+ const localVideo = getLocalJitsiVideoTrack(APP.store.getState());
+
+ // The video functionality is considered available if there are any
+ // video devices detected or if there is local video stream already
+ // active which could be either screensharing stream or a video track
+ // created before the permissions were rejected (through browser
+ // config).
+ const available = videoDeviceCount > 0 || Boolean(localVideo);
+
+ APP.store.dispatch(setVideoAvailable(available));
+ APP.API.notifyVideoAvailabilityChanged(available);
+ },
+
+ /**
+ * Disconnect from the conference and optionally request user feedback.
+ * @param {boolean} [requestFeedback=false] if user feedback should be
+ * @param {string} [hangupReason] the reason for leaving the meeting
+ * requested
+ * @param {boolean} [notifyOnConferenceTermination] whether to notify
+ * the user on conference termination
+ */
+ hangup(requestFeedback = false, hangupReason, notifyOnConferenceTermination) {
+ APP.store.dispatch(disableReceiver());
+
+ this._stopProxyConnection();
+
+ APP.store.dispatch(destroyLocalTracks());
+ this._localTracksInitialized = false;
+
+ // Remove unnecessary event listeners from firing callbacks.
+ if (this.deviceChangeListener) {
+ JitsiMeetJS.mediaDevices.removeEventListener(
+ JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
+ this.deviceChangeListener);
+ }
+
+ let feedbackResultPromise = Promise.resolve({});
+
+ if (requestFeedback) {
+ const feedbackDialogClosed = (feedbackResult = {}) => {
+ if (!feedbackResult.wasDialogShown && hangupReason && notifyOnConferenceTermination) {
+ return APP.store.dispatch(
+ openLeaveReasonDialog(hangupReason)).then(() => feedbackResult);
+ }
+
+ return Promise.resolve(feedbackResult);
+ };
+
+ feedbackResultPromise
+ = APP.store.dispatch(maybeOpenFeedbackDialog(room, hangupReason))
+ .then(feedbackDialogClosed, feedbackDialogClosed);
+ }
+
+ const leavePromise = this.leaveRoom().catch(() => Promise.resolve());
+
+ Promise.allSettled([ feedbackResultPromise, leavePromise ]).then(([ feedback, _ ]) => {
+ this._room = undefined;
+ room = undefined;
+
+ /**
+ * Don't call {@code notifyReadyToClose} if the promotional page flag is set
+ * and let the page take care of sending the message, since there will be
+ * a redirect to the page anyway.
+ */
+ if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
+ APP.API.notifyReadyToClose();
+ }
+
+ APP.store.dispatch(maybeRedirectToWelcomePage(feedback.value ?? {}));
+ });
+
+
+ },
+
+ /**
+ * Leaves the room.
+ *
+ * @param {boolean} doDisconnect - Whether leaving the room should also terminate the connection.
+ * @param {string} reason - reason for leaving the room.
+ * @returns {Promise}
+ */
+ leaveRoom(doDisconnect = true, reason = '') {
+ APP.store.dispatch(conferenceWillLeave(room));
+
+ const maybeDisconnect = () => {
+ if (doDisconnect) {
+ return disconnect();
+ }
+ };
+
+ if (room && room.isJoined()) {
+ return room.leave(reason).then(() => maybeDisconnect())
+ .catch(e => {
+ logger.error(e);
+
+ return maybeDisconnect();
+ });
+ }
+
+ return maybeDisconnect();
+ },
+
+ /**
+ * Changes the email for the local user
+ * @param email {string} the new email
+ */
+ changeLocalEmail(email = '') {
+ const formattedEmail = String(email).trim();
+
+ APP.store.dispatch(updateSettings({
+ email: formattedEmail
+ }));
+
+ sendData(commands.EMAIL, formattedEmail);
+ },
+
+ /**
+ * Changes the avatar url for the local user
+ * @param url {string} the new url
+ */
+ changeLocalAvatarUrl(url = '') {
+ const formattedUrl = String(url).trim();
+
+ APP.store.dispatch(updateSettings({
+ avatarURL: formattedUrl
+ }));
+
+ sendData(commands.AVATAR_URL, url);
+ },
+
+ /**
+ * Sends a message via the data channel.
+ * @param {string} to the id of the endpoint that should receive the
+ * message. If "" - the message will be sent to all participants.
+ * @param {object} payload the payload of the message.
+ * @throws NetworkError or InvalidStateError or Error if the operation
+ * fails.
+ */
+ sendEndpointMessage(to, payload) {
+ room.sendEndpointMessage(to, payload);
+ },
+
+ /**
+ * Callback invoked by the external api create or update a direct connection
+ * from the local client to an external client.
+ *
+ * @param {Object} event - The object containing information that should be
+ * passed to the {@code ProxyConnectionService}.
+ * @returns {void}
+ */
+ onProxyConnectionEvent(event) {
+ if (!this._proxyConnection) {
+ this._proxyConnection = new JitsiMeetJS.ProxyConnectionService({
+
+ /**
+ * Pass the {@code JitsiConnection} instance which will be used
+ * to fetch TURN credentials.
+ */
+ jitsiConnection: APP.connection,
+
+ /**
+ * The proxy connection feature is currently tailored towards
+ * taking a proxied video stream and showing it as a local
+ * desktop screen.
+ */
+ convertVideoToDesktop: true,
+
+ /**
+ * Callback invoked when the connection has been closed
+ * automatically. Triggers cleanup of screensharing if active.
+ *
+ * @returns {void}
+ */
+ onConnectionClosed: () => {
+ if (this._untoggleScreenSharing) {
+ this._untoggleScreenSharing();
+ }
+ },
+
+ /**
+ * Callback invoked to pass messages from the local client back
+ * out to the external client.
+ *
+ * @param {string} peerJid - The jid of the intended recipient
+ * of the message.
+ * @param {Object} data - The message that should be sent. For
+ * screensharing this is an iq.
+ * @returns {void}
+ */
+ onSendMessage: (peerJid, data) =>
+ APP.API.sendProxyConnectionEvent({
+ data,
+ to: peerJid
+ }),
+
+ /**
+ * Callback invoked when the remote peer of the proxy connection
+ * has provided a video stream, intended to be used as a local
+ * desktop stream.
+ *
+ * @param {JitsiLocalTrack} remoteProxyStream - The media
+ * stream to use as a local desktop stream.
+ * @returns {void}
+ */
+ onRemoteStream: desktopStream => {
+ if (desktopStream.videoType !== 'desktop') {
+ logger.warn('Received a non-desktop stream to proxy.');
+ desktopStream.dispose();
+
+ return;
+ }
+
+ APP.store.dispatch(toggleScreensharingA(undefined, false, { desktopStream }));
+ }
+ });
+ }
+
+ this._proxyConnection.processMessage(event);
+ },
+
+ /**
+ * Sets the video muted status.
+ */
+ setVideoMuteStatus() {
+ APP.UI.setVideoMuted(this.getMyUserId());
+ },
+
+ /**
+ * Dispatches the passed in feedback for submission. The submitted score
+ * should be a number inclusively between 1 through 5, or -1 for no score.
+ *
+ * @param {number} score - a number between 1 and 5 (inclusive) or -1 for no
+ * score.
+ * @param {string} message - An optional message to attach to the feedback
+ * in addition to the score.
+ * @returns {void}
+ */
+ submitFeedback(score = -1, message = '') {
+ if (score === -1 || (score >= 1 && score <= 5)) {
+ APP.store.dispatch(submitFeedback(score, message, room));
+ }
+ },
+
+ /**
+ * Terminates any proxy screensharing connection that is active.
+ *
+ * @private
+ * @returns {void}
+ */
+ _stopProxyConnection() {
+ if (this._proxyConnection) {
+ this._proxyConnection.stop();
+ }
+
+ this._proxyConnection = null;
+ }
+};
diff --git a/config.js b/config.js
new file mode 100644
index 0000000..961c8e4
--- /dev/null
+++ b/config.js
@@ -0,0 +1,1909 @@
+/* eslint-disable comma-dangle, no-unused-vars, no-var, prefer-template, vars-on-top */
+
+/*
+ * NOTE: If you add a new option please remember to document it here:
+ * https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-configuration
+ */
+
+var subdir = '';
+var subdomain = '';
+
+if (subdomain) {
+ subdomain = subdomain.substr(0, subdomain.length - 1).split('.')
+ .join('_')
+ .toLowerCase() + '.';
+}
+
+// In case of no ssi provided by the webserver, use empty strings
+if (subdir.startsWith('/' + subdir + 'conference-request/v1',
+
+ // Options related to the bridge (colibri) data channel
+ bridgeChannel: {
+ // If the backend advertises multiple colibri websockets, this options allows
+ // to filter some of them out based on the domain name. We use the first URL
+ // which does not match ignoreDomain, falling back to the first one that matches
+ // ignoreDomain. Has no effect if undefined.
+ // ignoreDomain: 'example.com',
+
+ // Prefer SCTP (WebRTC data channels over the media path) over a colibri websocket.
+ // If SCTP is available in the backend it will be used instead of a WS. Defaults to
+ // false (SCTP is used only if available and no WS are available).
+ // preferSctp: false
+ },
+
+ // Testing / experimental features.
+ //
+
+ testing: {
+ // Allows the setting of a custom bandwidth value from the UI.
+ // assumeBandwidth: true,
+
+ // Enables use of getDisplayMedia in electron
+ // electronUseGetDisplayMedia: false,
+
+ // Enables AV1 codec for FF. Note: By default it is disabled.
+ // enableAV1ForFF: false,
+
+ // Enables the use of the codec selection API supported by the browsers .
+ // enableCodecSelectionAPI: false,
+
+ // P2P test mode disables automatic switching to P2P when there are 2
+ // participants in the conference.
+ // p2pTestMode: false,
+
+ // Enables the test specific features consumed by jitsi-meet-torture
+ // testMode: false,
+
+ // Disables the auto-play behavior of *all* newly created video element.
+ // This is useful when the client runs on a host with limited resources.
+ // noAutoPlayVideo: false,
+
+ // Experiment: Whether to skip interim transcriptions.
+ // skipInterimTranscriptions: false,
+
+ // Dump transcripts to a element for debugging.
+ // dumpTranscript: false,
+
+ // Log the audio levels.
+ // debugAudioLevels: true,
+
+ // Will replace ice candidates IPs with invalid ones in order to fail ice.
+ // failICE: true,
+
+ // When running on Spot TV, this controls whether to show the recording consent dialog.
+ // If false (default), Spot instances will not show the recording consent dialog.
+ // If true, Spot instances will show the recording consent dialog like regular clients.
+ // showSpotConsentDialog: false,
+ },
+
+ // Disables moderator indicators.
+ // disableModeratorIndicator: false,
+
+ // Disables the reactions feature.
+ // disableReactions: true,
+
+ // Disables the reactions moderation feature.
+ // disableReactionsModeration: false,
+
+ // Disables the reactions in chat feature.
+ // disableReactionsInChat: false,
+
+ // Disables polls feature.
+ // disablePolls: false,
+
+ // Disables demote button from self-view
+ // disableSelfDemote: false,
+
+ // Disables self-view tile. (hides it from tile view and from filmstrip)
+ // disableSelfView: false,
+
+ // Disables self-view settings in UI
+ // disableSelfViewSettings: false,
+
+ // screenshotCapture : {
+ // Enables the screensharing capture feature.
+ // enabled: false,
+ //
+ // The mode for the screenshot capture feature.
+ // Can be either 'recording' - screensharing screenshots are taken
+ // only when the recording is also on,
+ // or 'always' - screensharing screenshots are always taken.
+ // mode: 'recording',
+ // }
+
+ // Disables ICE/UDP by filtering out local and remote UDP candidates in
+ // signalling.
+ // webrtcIceUdpDisable: false,
+
+ // Disables ICE/TCP by filtering out local and remote TCP candidates in
+ // signalling.
+ // webrtcIceTcpDisable: false,
+
+
+ // Media
+ //
+
+ // Audio
+
+ // Disable measuring of audio levels.
+ // disableAudioLevels: false,
+
+ // audioLevelsInterval: 200,
+
+ // Enabling this will run the lib-jitsi-meet no audio detection module which
+ // will notify the user if the current selected microphone has no audio
+ // input and will suggest another valid device if one is present.
+ enableNoAudioDetection: true,
+
+ // Enabling this will show a "Save Logs" link in the GSM popover that can be
+ // used to collect debug information (XMPP IQs, SDP offer/answer cycles)
+ // about the call.
+ // enableSaveLogs: false,
+
+ // Enabling this will hide the "Show More" link in the GSM popover that can be
+ // used to display more statistics about the connection (IP, Port, protocol, etc).
+ // disableShowMoreStats: true,
+
+ // Enabling this will run the lib-jitsi-meet noise detection module which will
+ // notify the user if there is noise, other than voice, coming from the current
+ // selected microphone. The purpose it to let the user know that the input could
+ // be potentially unpleasant for other meeting participants.
+ enableNoisyMicDetection: true,
+
+ // Start the conference in audio only mode (no video is being received nor
+ // sent).
+ // startAudioOnly: false,
+
+ // Every participant after the Nth will start audio muted.
+ // startAudioMuted: 10,
+
+ // Start calls with audio muted. Unlike the option above, this one is only
+ // applied locally. FIXME: having these 2 options is confusing.
+ // startWithAudioMuted: false,
+
+ // Enabling it (with #params) will disable local audio output of remote
+ // participants and to enable it back a reload is needed.
+ // startSilent: false,
+
+ // Enables support for opus-red (redundancy for Opus).
+ // enableOpusRed: false,
+
+ // Specify audio quality stereo and opusMaxAverageBitrate values in order to enable HD audio.
+ // Beware, by doing so, you are disabling echo cancellation, noise suppression and AGC.
+ // Specify enableOpusDtx to enable support for opus-dtx where
+ // audio packets won’t be transmitted while participant is silent or muted.
+ // audioQuality: {
+ // stereo: false,
+ // opusMaxAverageBitrate: null, // Value to fit the 6000 to 510000 range.
+ // enableOpusDtx: false,
+ // },
+
+ // Noise suppression configuration. By default rnnoise is used. Optionally Krisp
+ // can be used by enabling it below, but the Krisp JS SDK files must be supplied in your
+ // installation. Specifically, these files are needed:
+ // - https://meet.example.com/libs/krisp/krisp.mjs
+ // - https://meet.example.com/libs/krisp/models/model_8.kw
+ // - https://meet.example.com/libs/krisp/models/model_nc.kw
+ // - https://meet.example.com/libs/krisp/models/model_bvc.kw
+ // - https://meet.example.com/libs/krisp/assets/bvc-allowed.txt
+ // In case when you have known BVC supported devices and you want to extend allowed devices list
+ // - https://meet.example.com/libs/krisp/assets/bvc-allowed-ext.txt
+ // In case when you have known BVC supported devices and you want to extend allowed devices list
+ // - https://meet.example.com/libs/krisp/models/model_inbound_8.kw
+ // - https://meet.example.com/libs/krisp/models/model_inbound_16.kw
+ // In case when you want to use inbound noise suppression models
+ // NOTE: Krisp JS SDK v2.0.0 was tested.
+ // noiseSuppression: {
+ // krisp: {
+ // enabled: false,
+ // logProcessStats: false,
+ // debugLogs: false,
+ // useBVC: false,
+ // bufferOverflowMS: 1000,
+ // inboundModels: {
+ // modelInbound8: 'model_inbound_8.kef',
+ // modelInbound16: 'model_inbound_16.kef',
+ // },
+ // preloadInboundModels: {
+ // modelInbound8: 'model_inbound_8.kef',
+ // modelInbound16: 'model_inbound_16.kef',
+ // },
+ // preloadModels: {
+ // modelBVC: 'model_bvc.kef',
+ // model8: 'model_8.kef',
+ // modelNC: 'model_nc_mq.kef',
+ // },
+ // models: {
+ // modelBVC: 'model_bvc.kef',
+ // model8: 'model_8.kef',
+ // modelNV: 'model_nc_mq.kef',
+ // },
+ // bvc: {
+ // allowedDevices: 'bvc-allowed.txt',
+ // allowedDevicesExt: 'bvc-allowed-ext.txt',
+ // }
+ // },
+ // },
+
+ // Video
+
+ // Sets the default camera facing mode.
+ // cameraFacingMode: 'user',
+
+ // Sets the preferred resolution (height) for local video. Defaults to 720.
+ // resolution: 720,
+
+ // DEPRECATED. Please use raisedHands.disableRemoveRaisedHandOnFocus instead.
+ // Specifies whether the raised hand will hide when someone becomes a dominant speaker or not
+ // disableRemoveRaisedHandOnFocus: false,
+
+ // Specifies which raised hand related config should be set.
+ // raisedHands: {
+ // // Specifies whether the raised hand can be lowered by moderator.
+ // disableLowerHandByModerator: false,
+
+ // // Specifies whether there is a notification before hiding the raised hand
+ // // when someone becomes the dominant speaker.
+ // disableLowerHandNotification: true,
+
+ // // Specifies whether there is a notification when you are the next speaker in line.
+ // disableNextSpeakerNotification: false,
+
+ // // Specifies whether the raised hand will hide when someone becomes a dominant speaker or not.
+ // disableRemoveRaisedHandOnFocus: false,
+ // },
+
+ // speakerStats: {
+ // // Specifies whether the speaker stats is enable or not.
+ // disabled: false,
+
+ // // Specifies whether there will be a search field in speaker stats or not.
+ // disableSearch: false,
+
+ // // Specifies whether participants in speaker stats should be ordered or not, and with what priority.
+ // // 'role', <- Moderators on top.
+ // // 'name', <- Alphabetically by name.
+ // // 'hasLeft', <- The ones that have left in the bottom.
+ // order: [
+ // 'role',
+ // 'name',
+ // 'hasLeft',
+ // ],
+ // },
+
+ // DEPRECATED. Please use speakerStats.disableSearch instead.
+ // Specifies whether there will be a search field in speaker stats or not
+ // disableSpeakerStatsSearch: false,
+
+ // DEPRECATED. Please use speakerStats.order .
+ // Specifies whether participants in speaker stats should be ordered or not, and with what priority
+ // speakerStatsOrder: [
+ // 'role', <- Moderators on top
+ // 'name', <- Alphabetically by name
+ // 'hasLeft', <- The ones that have left in the bottom
+ // ], <- the order of the array elements determines priority
+
+ // How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
+ // Use -1 to disable.
+ // maxFullResolutionParticipants: 2,
+
+ // w3c spec-compliant video constraints to use for video capture. Currently
+ // used by browsers that return true from lib-jitsi-meet's
+ // util#browser#usesNewGumFlow. The constraints are independent from
+ // this config's resolution value. Defaults to requesting an ideal
+ // resolution of 720p.
+ // constraints: {
+ // video: {
+ // height: {
+ // ideal: 720,
+ // max: 720,
+ // min: 240,
+ // },
+ // },
+ // },
+
+ // Enable / disable simulcast support.
+ // disableSimulcast: false,
+
+ // Every participant after the Nth will start video muted.
+ // startVideoMuted: 10,
+
+ // Start calls with video muted. Unlike the option above, this one is only
+ // applied locally. FIXME: having these 2 options is confusing.
+ // startWithVideoMuted: false,
+
+ // Desktop sharing
+
+ // Optional desktop sharing frame rate options. Default value: min:5, max:5.
+ // desktopSharingFrameRate: {
+ // min: 5,
+ // max: 5,
+ // },
+
+ // Optional screenshare settings that give more control over screen capture in the browser.
+ // screenShareSettings: {
+ // // Show users the current tab is the preferred capture source, default: false.
+ // desktopPreferCurrentTab: false,
+ // // Allow users to select system audio, default: include.
+ // desktopSystemAudio: 'include',
+ // // Allow users to seamlessly switch which tab they are sharing without having to select the tab again.
+ // desktopSurfaceSwitching: 'include',
+ // // Allow a user to be shown a preference for what screen is to be captured, default: unset.
+ // desktopDisplaySurface: undefined,
+ // // Allow users to select the current tab as a capture source, default: exclude.
+ // desktopSelfBrowserSurface: 'exclude'
+ // },
+
+ // Recording
+
+ // Enable the dropbox integration.
+ // dropbox: {
+ // appKey: '', // Specify your app key here.
+ // // A URL to redirect the user to, after authenticating
+ // // by default uses:
+ // // 'https://jitsi-meet.example.com/static/oauth.html'
+ // redirectURI:
+ // 'https://jitsi-meet.example.com/subfolder/static/oauth.html',
+ // },
+
+ // configuration for all things recording related. Existing settings will be migrated here in the future.
+ // recordings: {
+ // // IF true (default) recording audio and video is selected by default in the recording dialog.
+ // // recordAudioAndVideo: true,
+ // // If true, shows a notification at the start of the meeting with a call to action button
+ // // to start recording (for users who can do so).
+ // // suggestRecording: true,
+ // // If true, shows a warning label in the prejoin screen to point out the possibility that
+ // // the call you're joining might be recorded.
+ // // showPrejoinWarning: true,
+ // // If true, the notification for recording start will display a link to download the cloud recording.
+ // // showRecordingLink: true,
+ // // If true, mutes audio and video when a recording begins and displays a dialog
+ // // explaining the effect of unmuting.
+ // // requireConsent: true,
+ // // If true consent will be skipped for users who are already in the meeting.
+ // // skipConsentInMeeting: true,
+ // // Link for the recording consent dialog's "Learn more" link.
+ // // consentLearnMoreLink: 'https://jitsi.org/meet/consent',
+ // },
+
+ // recordingService: {
+ // // When integrations like dropbox are enabled only that will be shown,
+ // // by enabling fileRecordingsServiceEnabled, we show both the integrations
+ // // and the generic recording service (its configuration and storage type
+ // // depends on jibri configuration)
+ // enabled: false,
+
+ // // Whether to show the possibility to share file recording with other people
+ // // (e.g. meeting participants), based on the actual implementation
+ // // on the backend.
+ // sharingEnabled: false,
+
+ // // Hide the warning that says we only store the recording for 24 hours.
+ // hideStorageWarning: false,
+ // },
+
+ // DEPRECATED. Use recordingService.enabled instead.
+ // fileRecordingsServiceEnabled: false,
+
+ // DEPRECATED. Use recordingService.sharingEnabled instead.
+ // fileRecordingsServiceSharingEnabled: false,
+
+ // Local recording configuration.
+ // localRecording: {
+ // // Whether to disable local recording or not.
+ // disable: false,
+
+ // // Whether to notify all participants when a participant is recording locally.
+ // notifyAllParticipants: false,
+
+ // // Whether to disable the self recording feature (only local participant streams).
+ // disableSelfRecording: false,
+ // },
+
+ // Customize the Live Streaming dialog. Can be modified for a non-YouTube provider.
+ // liveStreaming: {
+ // // Whether to enable live streaming or not.
+ // enabled: false,
+ // // Terms link
+ // termsLink: 'https://www.youtube.com/t/terms',
+ // // Data privacy link
+ // dataPrivacyLink: 'https://policies.google.com/privacy',
+ // // RegExp string that validates the stream key input field
+ // validatorRegExpString: '^(?:[a-zA-Z0-9]{4}(?:-(?!$)|$)){4}',
+ // // Documentation reference for the live streaming feature.
+ // helpLink: 'https://jitsi.org/live'
+ // },
+
+ // DEPRECATED. Use liveStreaming.enabled instead.
+ // liveStreamingEnabled: false,
+
+ // DEPRECATED. Use transcription.enabled instead.
+ // transcribingEnabled: false,
+
+ // DEPRECATED. Use transcription.useAppLanguage instead.
+ // transcribeWithAppLanguage: true,
+
+ // DEPRECATED. Use transcription.preferredLanguage instead.
+ // preferredTranscribeLanguage: 'en-US',
+
+ // DEPRECATED. Use transcription.autoTranscribeOnRecord instead.
+ // autoCaptionOnRecord: false,
+
+ // Transcription options.
+ // transcription: {
+ // // Whether the feature should be enabled or not.
+ // enabled: false,
+
+ // // Translation languages.
+ // // Available languages can be found in
+ // // ./lang/translation-languages.json.
+ // translationLanguages: ['en', 'es', 'fr', 'ro'],
+
+ // // Important languages to show on the top of the language list.
+ // translationLanguagesHead: ['en'],
+
+ // // If true transcriber will use the application language.
+ // // The application language is either explicitly set by participants in their settings or automatically
+ // // detected based on the environment, e.g. if the app is opened in a chrome instance which
+ // // is using french as its default language then transcriptions for that participant will be in french.
+ // // Defaults to true.
+ // useAppLanguage: true,
+
+ // // Transcriber language. This settings will only work if "useAppLanguage"
+ // // is explicitly set to false.
+ // // Available languages can be found in
+ // // ./src/react/features/transcribing/transcriber-langs.json.
+ // preferredLanguage: 'en-US',
+
+ // // Enables automatic turning on transcribing when recording is started
+ // autoTranscribeOnRecord: false,
+
+ // // Enables automatic request of subtitles when transcriber is present in the meeting, uses the default
+ // // language that is set
+ // autoCaptionOnTranscribe: false,
+ //
+ // // Disables everything related to closed captions - the tab in the chat area, the button in the menu,
+ // // subtitles on stage and the "Show subtitles on stage" checkbox in the settings.
+ // // Note: Starting transcriptions from the recording dialog will still work.
+ // disableClosedCaptions: false,
+
+ // // Whether to invite jigasi when backend transcriptions are enabled (asyncTranscription is true in metadata).
+ // // By default, we invite it.
+ // inviteJigasiOnBackendTranscribing: true,
+ // },
+
+ // Misc
+
+ // Default value for the channel "last N" attribute. -1 for unlimited.
+ channelLastN: -1,
+
+ // Connection indicators
+ // connectionIndicators: {
+ // autoHide: true,
+ // autoHideTimeout: 5000,
+ // disabled: false,
+ // disableDetails: false,
+ // inactiveDisabled: false
+ // },
+
+ // Provides a way for the lastN value to be controlled through the UI.
+ // When startLastN is present, conference starts with a last-n value of startLastN and channelLastN
+ // value will be used when the quality level is selected using "Manage Video Quality" slider.
+ // startLastN: 1,
+
+ // Specify the settings for video quality optimizations on the client.
+ // videoQuality: {
+ //
+ // // Provides a way to set the codec preference on desktop based endpoints.
+ // codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8', 'H264' ],
+ //
+ // // Provides a way to set the codec for screenshare.
+ // screenshareCodec: 'AV1',
+ // mobileScreenshareCodec: 'VP8',
+ //
+ // // Enables the adaptive mode in the client that will make runtime adjustments to selected codecs and received
+ // // videos for a better user experience. This mode will kick in only when CPU overuse is reported in the
+ // // WebRTC statistics for the outbound video streams.
+ // enableAdaptiveMode: false,
+ //
+ // // Codec specific settings for scalability modes and max bitrates.
+ // av1: {
+ // maxBitratesVideo: {
+ // low: 100000,
+ // standard: 300000,
+ // high: 1000000,
+ // fullHd: 2000000,
+ // ultraHd: 4000000,
+ // ssHigh: 2500000
+ // },
+ // scalabilityModeEnabled: true,
+ // useSimulcast: false,
+ // useKSVC: true
+ // },
+ // h264: {
+ // maxBitratesVideo: {
+ // low: 200000,
+ // standard: 500000,
+ // high: 1500000,
+ // fullHd: 3000000,
+ // ultraHd: 6000000,
+ // ssHigh: 2500000
+ // },
+ // scalabilityModeEnabled: true
+ // },
+ // vp8: {
+ // maxBitratesVideo: {
+ // low: 200000,
+ // standard: 500000,
+ // high: 1500000,
+ // fullHd: 3000000,
+ // ultraHd: 6000000,
+ // ssHigh: 2500000
+ // },
+ // scalabilityModeEnabled: false
+ // },
+ // vp9: {
+ // maxBitratesVideo: {
+ // low: 100000,
+ // standard: 300000,
+ // high: 1200000,
+ // fullHd: 2500000,
+ // ultraHd: 5000000,
+ // ssHigh: 2500000
+ // },
+ // scalabilityModeEnabled: true,
+ // useSimulcast: false,
+ // useKSVC: true
+ // },
+ //
+ // // The options can be used to override default thresholds of video thumbnail heights corresponding to
+ // // the video quality levels used in the application. At the time of this writing the allowed levels are:
+ // // 'low' - for the low quality level (180p at the time of this writing)
+ // // 'standard' - for the medium quality level (360p)
+ // // 'high' - for the high quality level (720p)
+ // // The keys should be positive numbers which represent the minimal thumbnail height for the quality level.
+ // //
+ // // With the default config value below the application will use 'low' quality until the thumbnails are
+ // // at least 360 pixels tall. If the thumbnail height reaches 720 pixels then the application will switch to
+ // // the high quality.
+ // minHeightForQualityLvl: {
+ // 360: 'standard',
+ // 720: 'high',
+ // },
+ //
+ // // Provides a way to set the codec preference on mobile devices, both on RN and mobile browser based endpoint
+ // mobileCodecPreferenceOrder: [ 'VP8', 'VP9', 'H264', 'AV1' ],
+ // },
+
+ // Notification timeouts
+ // notificationTimeouts: {
+ // short: 2500,
+ // medium: 5000,
+ // long: 10000,
+ // extraLong: 60000,
+ // sticky: 0,
+ // },
+
+ // // Options for the recording limit notification.
+ // recordingLimit: {
+ //
+ // // The recording limit in minutes. Note: This number appears in the notification text
+ // // but doesn't enforce the actual recording time limit. This should be configured in
+ // // jibri!
+ // limit: 60,
+ //
+ // // The name of the app with unlimited recordings.
+ // appName: 'Unlimited recordings APP',
+ //
+ // // The URL of the app with unlimited recordings.
+ // appURL: 'https://unlimited.recordings.app.com/',
+ // },
+
+ // Disables or enables RTX (RFC 4588) (defaults to false).
+ // disableRtx: false,
+
+ // Moves all Jitsi Meet 'beforeunload' logic (cleanup, leaving, disconnecting, etc) to the 'unload' event.
+ // disableBeforeUnloadHandlers: true,
+
+ // Disables or enables TCC support in this client (default: enabled).
+ // enableTcc: true,
+
+ // Disables or enables REMB support in this client (default: enabled).
+ // enableRemb: true,
+
+ // Enables forced reload of the client when the call is migrated as a result of
+ // the bridge going down.
+ // enableForcedReload: true,
+
+ // Use TURN/UDP servers for the jitsi-videobridge connection (by default
+ // we filter out TURN/UDP because it is usually not needed since the
+ // bridge itself is reachable via UDP)
+ // useTurnUdp: false
+
+ // Enable support for encoded transform in supported browsers. This allows
+ // E2EE to work in Safari if the corresponding flag is enabled in the browser.
+ // Experimental.
+ // enableEncodedTransformSupport: false,
+
+ // UI
+ //
+
+ // Disables responsive tiles.
+ // disableResponsiveTiles: false,
+
+ // DEPRECATED. Please use `securityUi?.hideLobbyButton` instead.
+ // Hides lobby button.
+ // hideLobbyButton: false,
+
+ // DEPRECATED. Please use `lobby?.autoKnock` instead.
+ // If Lobby is enabled starts knocking automatically.
+ // autoKnockLobby: false,
+
+ // DEPRECATED. Please use `lobby?.enableChat` instead.
+ // Enable lobby chat.
+ // enableLobbyChat: true,
+
+ // DEPRECATED! Use `breakoutRooms.hideAddRoomButton` instead.
+ // Hides add breakout room button
+ // hideAddRoomButton: false,
+
+ // Require users to always specify a display name.
+ // requireDisplayName: true,
+
+ // Enables webhid functionality for Audio.
+ // enableWebHIDFeature: false,
+
+ // DEPRECATED! Use 'welcomePage.disabled' instead.
+ // Whether to use a welcome page or not. In case it's false a random room
+ // will be joined when no room is specified.
+ // enableWelcomePage: true,
+
+ // Configs for welcome page.
+ // welcomePage: {
+ // // Whether to disable welcome page. In case it's disabled a random room
+ // // will be joined when no room is specified.
+ // disabled: false,
+ // // If set, landing page will redirect to this URL.
+ // customUrl: ''
+ // },
+
+ // Configs for the lobby screen.
+ // lobby: {
+ // // If Lobby is enabled, it starts knocking automatically. Replaces `autoKnockLobby`.
+ // autoKnock: false,
+ // // Enables the lobby chat. Replaces `enableLobbyChat`.
+ // enableChat: true,
+ // },
+
+ // Configs for the security related UI elements.
+ // securityUi: {
+ // // Hides the lobby button. Replaces `hideLobbyButton`.
+ // hideLobbyButton: false,
+ // // Hides the possibility to set and enter a lobby password.
+ // disableLobbyPassword: false,
+ // },
+
+ // Disable app shortcuts that are registered upon joining a conference
+ // disableShortcuts: false,
+
+ // Disable initial browser getUserMedia requests.
+ // This is useful for scenarios where users might want to start a conference for screensharing only
+ // disableInitialGUM: false,
+
+ // Enabling the close page will ignore the welcome page redirection when
+ // a call is hangup.
+ // enableClosePage: false,
+
+ // Disable hiding of remote thumbnails when in a 1-on-1 conference call.
+ // Setting this to null, will also disable showing the remote videos
+ // when the toolbar is shown on mouse movements
+ // disable1On1Mode: null | false | true,
+
+ // Default local name to be displayed
+ // defaultLocalDisplayName: 'me',
+
+ // Default remote name to be displayed
+ // defaultRemoteDisplayName: 'Fellow Jitster',
+
+ // Hides the display name from the participant thumbnail
+ // hideDisplayName: false,
+
+ // Hides the dominant speaker name badge that hovers above the toolbox
+ // hideDominantSpeakerBadge: false,
+
+ // Default language for the user interface. Cannot be overwritten.
+ // For iframe integrations, use the `lang` option directly instead.
+ // defaultLanguage: 'en',
+
+ // Disables profile and the edit of all fields from the profile settings (display name and email)
+ // disableProfile: false,
+
+ // Hides the email section under profile settings.
+ // hideEmailInSettings: false,
+
+ // When enabled the password used for locking a room is restricted to up to the number of digits specified
+ // default: roomPasswordNumberOfDigits: false,
+ // roomPasswordNumberOfDigits: 10,
+
+ // Message to show the users. Example: 'The service will be down for
+ // maintenance at 01:00 AM GMT,
+ // noticeMessage: '',
+
+ // Enables calendar integration, depends on googleApiApplicationClientID
+ // and microsoftApiApplicationClientID
+ // enableCalendarIntegration: false,
+
+ // Whether to notify when the conference is terminated because it was destroyed.
+ // notifyOnConferenceDestruction: true,
+
+ // The client id for the google APIs used for the calendar integration, youtube livestreaming, etc.
+ // googleApiApplicationClientID: '',
+
+ // Configs for prejoin page.
+ // prejoinConfig: {
+ // // When 'true', it shows an intermediate page before joining, where the user can configure their devices.
+ // enabled: true,
+ // // Hides the participant name editing field in the prejoin screen.
+ // // If requireDisplayName is also set as true, a name should still be provided through
+ // // either the jwt or the userInfo from the iframe api init object in order for this to have an effect.
+ // hideDisplayName: false,
+ // // List of buttons to hide from the extra join options dropdown.
+ // hideExtraJoinButtons: ['no-audio', 'by-phone'],
+ // // Configuration for pre-call test
+ // // By setting preCallTestEnabled, you enable the pre-call test in the prejoin page.
+ // // ICE server credentials need to be provided over the preCallTestICEUrl
+ // preCallTestEnabled: false,
+ // preCallTestICEUrl: ''
+ // },
+
+ // When 'true', the user cannot edit the display name.
+ // (Mainly useful when used in conjunction with the JWT so the JWT name becomes read only.)
+ // readOnlyName: false,
+
+ // If etherpad integration is enabled, setting this to true will
+ // automatically open the etherpad when a participant joins. This
+ // does not affect the mobile app since opening an etherpad
+ // obscures the conference controls -- it's better to let users
+ // choose to open the pad on their own in that case.
+ // openSharedDocumentOnJoin: false,
+
+ // If true, shows the unsafe room name warning label when a room name is
+ // deemed unsafe (due to the simplicity in the name) and a password is not
+ // set or the lobby is not enabled.
+ // enableInsecureRoomNameWarning: false,
+
+ // Array with avatar URL prefixes that need to use CORS.
+ // corsAvatarURLs: [ 'https://www.gravatar.com/avatar/' ],
+
+ // Base URL for a Gravatar-compatible service. Defaults to Gravatar.
+ // DEPRECATED! Use `gravatar.baseUrl` instead.
+ // gravatarBaseURL: 'https://www.gravatar.com/avatar/',
+
+ // Setup for Gravatar-compatible services.
+ // gravatar: {
+ // // Defaults to Gravatar.
+ // baseUrl: 'https://www.gravatar.com/avatar/',
+ // // True if Gravatar should be disabled.
+ // disabled: false,
+ // },
+
+ // App name to be displayed in the invitation email subject, as an alternative to
+ // interfaceConfig.APP_NAME.
+ // inviteAppName: null,
+
+ // Moved from interfaceConfig(TOOLBAR_BUTTONS).
+ // The name of the toolbar buttons to display in the toolbar, including the
+ // "More actions" menu. If present, the button will display. Exceptions are
+ // "livestreaming" and "recording" which also require being a moderator and
+ // some other values in config.js to be enabled. Also, the "profile" button will
+ // not display for users with a JWT.
+ // Notes:
+ // - it's possible to reorder the buttons in the maintoolbar by changing the order of the mainToolbarButtons
+ // - 'desktop' controls the "Share your screen" button
+ // - if `toolbarButtons` is undefined, we fallback to enabling all buttons on the UI
+ // toolbarButtons: [
+ // 'camera',
+ // 'chat',
+ // 'closedcaptions',
+ // 'desktop',
+ // 'download',
+ // 'embedmeeting',
+ // 'etherpad',
+ // 'feedback',
+ // 'filmstrip',
+ // 'fullscreen',
+ // 'hangup',
+ // 'help',
+ // 'highlight',
+ // 'invite',
+ // 'linktosalesforce',
+ // 'livestreaming',
+ // 'microphone',
+ // 'noisesuppression',
+ // 'participants-pane',
+ // 'profile',
+ // 'raisehand',
+ // 'recording',
+ // 'security',
+ // 'select-background',
+ // 'settings',
+ // 'shareaudio',
+ // 'sharedvideo',
+ // 'shortcuts',
+ // 'stats',
+ // 'tileview',
+ // 'toggle-camera',
+ // 'videoquality',
+ // 'whiteboard',
+ // ],
+
+ // Holds values related to toolbar visibility control.
+ // toolbarConfig: {
+ // // Moved from interfaceConfig.INITIAL_TOOLBAR_TIMEOUT
+ // // The initial number of milliseconds for the toolbar buttons to be visible on screen.
+ // initialTimeout: 20000,
+ // // Moved from interfaceConfig.TOOLBAR_TIMEOUT
+ // // Number of milliseconds for the toolbar buttons to be visible on screen.
+ // timeout: 4000,
+ // // Moved from interfaceConfig.TOOLBAR_ALWAYS_VISIBLE
+ // // Whether toolbar should be always visible or should hide after x milliseconds.
+ // alwaysVisible: false,
+ // // Indicates whether the toolbar should still autohide when chat is open
+ // autoHideWhileChatIsOpen: false,
+ // },
+
+ // Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed
+ // buttons varies from 2 buttons to 8 buttons. Every array in the mainToolbarButtons array will replace the
+ // corresponding default buttons configuration matched by the number of buttons specified in the array. Arrays with
+ // more than 8 buttons or less then 2 buttons will be ignored. When there there isn't an override for a certain
+ // configuration (for example when 3 buttons are displayed) the default jitsi-meet configuration will be used.
+ // The order of the buttons in the array is preserved.
+ // mainToolbarButtons: [
+ // [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants-pane', 'tileview' ],
+ // [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane', 'tileview' ],
+ // [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane' ],
+ // [ 'microphone', 'camera', 'desktop', 'chat', 'participants-pane' ],
+ // [ 'microphone', 'camera', 'chat', 'participants-pane' ],
+ // [ 'microphone', 'camera', 'chat' ],
+ // [ 'microphone', 'camera' ]
+ // ],
+
+ // Toolbar buttons which have their click/tap event exposed through the API on
+ // `toolbarButtonClicked`. Passing a string for the button key will
+ // prevent execution of the click/tap routine; passing an object with `key` and
+ // `preventExecution` flag on false will not prevent execution of the click/tap
+ // routine. Below array with mixed mode for passing the buttons.
+ // buttonsWithNotifyClick: [
+ // 'camera',
+ // {
+ // key: 'chat',
+ // preventExecution: false
+ // },
+ // {
+ // key: 'closedcaptions',
+ // preventExecution: true
+ // },
+ // 'desktop',
+ // 'download',
+ // 'embedmeeting',
+ // 'end-meeting',
+ // 'etherpad',
+ // 'feedback',
+ // 'filmstrip',
+ // 'fullscreen',
+ // 'hangup',
+ // 'hangup-menu',
+ // 'help',
+ // {
+ // key: 'invite',
+ // preventExecution: false
+ // },
+ // 'livestreaming',
+ // 'microphone',
+ // 'mute-everyone',
+ // 'mute-video-everyone',
+ // 'noisesuppression',
+ // 'participants-pane',
+ // 'profile',
+ // {
+ // key: 'raisehand',
+ // preventExecution: true
+ // },
+ // 'recording',
+ // 'security',
+ // 'select-background',
+ // 'settings',
+ // 'shareaudio',
+ // 'sharedvideo',
+ // 'shortcuts',
+ // 'stats',
+ // 'tileview',
+ // 'toggle-camera',
+ // 'videoquality',
+ // // The add passcode button from the security dialog.
+ // {
+ // key: 'add-passcode',
+ // preventExecution: false
+ // },
+ // 'whiteboard',
+ // ],
+
+ // Participant context menu buttons which have their click/tap event exposed through the API on
+ // `participantMenuButtonClick`. Passing a string for the button key will
+ // prevent execution of the click/tap routine; passing an object with `key` and
+ // `preventExecution` flag on false will not prevent execution of the click/tap
+ // routine. Below array with mixed mode for passing the buttons.
+ // participantMenuButtonsWithNotifyClick: [
+ // 'allow-video',
+ // {
+ // key: 'ask-unmute',
+ // preventExecution: false
+ // },
+ // 'conn-status',
+ // 'flip-local-video',
+ // 'grant-moderator',
+ // {
+ // key: 'kick',
+ // preventExecution: true
+ // },
+ // {
+ // key: 'hide-self-view',
+ // preventExecution: false
+ // },
+ // 'mute',
+ // 'mute-others',
+ // 'mute-others-video',
+ // 'mute-video',
+ // 'pinToStage',
+ // 'privateMessage',
+ // {
+ // key: 'remote-control',
+ // preventExecution: false
+ // },
+ // 'send-participant-to-room',
+ // 'verify',
+ // ],
+
+ // List of pre meeting screens buttons to hide. The values must be one or more of the 5 allowed buttons:
+ // 'microphone', 'camera', 'select-background', 'invite', 'settings'
+ // hiddenPremeetingButtons: [],
+
+ // An array with custom option buttons for the participant context menu
+ // type: Array<{ icon: string; id: string; text: string; }>
+ // customParticipantMenuButtons: [],
+
+ // An array with custom option buttons for the toolbar
+ // type: Array<{ icon: string; id: string; text: string; backgroundColor?: string; }>
+ // customToolbarButtons: [],
+
+ // Stats
+ //
+
+ // Whether to enable stats collection or not in the TraceablePeerConnection.
+ // This can be useful for debugging purposes (post-processing/analysis of
+ // the webrtc stats) as it is done in the jitsi-meet-torture bandwidth
+ // estimation tests.
+ // gatherStats: false,
+
+ // The interval at which PeerConnection.getStats() is called. Defaults to 10000
+ // pcStatsInterval: 10000,
+
+ // Enables sending participants' display names to stats
+ // enableDisplayNameInStats: false,
+
+ // Enables sending participants' emails (if available) to stats and other analytics
+ // enableEmailInStats: false,
+
+ // faceLandmarks: {
+ // // Enables sharing your face coordinates. Used for centering faces within a video.
+ // enableFaceCentering: false,
+
+ // // Enables detecting face expressions and sharing data with other participants
+ // enableFaceExpressionsDetection: false,
+
+ // // Enables displaying face expressions in speaker stats
+ // enableDisplayFaceExpressions: false,
+
+ // // Enable rtc stats for face landmarks
+ // enableRTCStats: false,
+
+ // // Minimum required face movement percentage threshold for sending new face centering coordinates data.
+ // faceCenteringThreshold: 10,
+
+ // // Milliseconds for processing a new image capture in order to detect face coordinates if they exist.
+ // captureInterval: 1000,
+ // },
+
+ // Controls the percentage of automatic feedback shown to participants.
+ // The default value is 100%. If set to 0, no automatic feedback will be requested
+ // feedbackPercentage: 100,
+
+ // Privacy
+ //
+
+ // If third party requests are disabled, no other server will be contacted.
+ // This means avatars will be locally generated and external stats integration
+ // will not function.
+ // disableThirdPartyRequests: false,
+
+
+ // Peer-To-Peer mode: used (if enabled) when there are just 2 participants.
+ //
+
+ p2p: {
+ // Enables peer to peer mode. When enabled the system will try to
+ // establish a direct connection when there are exactly 2 participants
+ // in the room. If that succeeds the conference will stop sending data
+ // through the JVB and use the peer to peer connection instead. When a
+ // 3rd participant joins the conference will be moved back to the JVB
+ // connection.
+ enabled: true,
+
+ // Sets the ICE transport policy for the p2p connection. At the time
+ // of this writing the list of possible values are 'all' and 'relay',
+ // but that is subject to change in the future. The enum is defined in
+ // the WebRTC standard:
+ // https://www.w3.org/TR/webrtc/#rtcicetransportpolicy-enum.
+ // If not set, the effective value is 'all'.
+ // iceTransportPolicy: 'all',
+
+ // Provides a way to set the codec preference on mobile devices, both on RN and mobile browser based
+ // endpoints.
+ // mobileCodecPreferenceOrder: [ 'H264', 'VP8', 'VP9', 'AV1' ],
+ //
+ // Provides a way to set the codec preference on desktop based endpoints.
+ // codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8', 'H264 ],
+
+ // Provides a way to set the codec for screenshare.
+ // screenshareCodec: 'AV1',
+ // mobileScreenshareCodec: 'VP8',
+
+ // How long we're going to wait, before going back to P2P after the 3rd
+ // participant has left the conference (to filter out page reload).
+ // backToP2PDelay: 5,
+
+ // The STUN servers that will be used in the peer to peer connections
+ stunServers: [
+
+ // { urls: 'stun:jitsi-meet.example.com:3478' },
+ { urls: 'stun:meet-jit-si-turnrelay.jitsi.net:443' },
+ ],
+ },
+
+ analytics: {
+ // True if the analytics should be disabled
+ // disabled: false,
+
+ // Matomo configuration:
+ // matomoEndpoint: 'https://your-matomo-endpoint/',
+ // matomoSiteID: '42',
+
+ // The Amplitude APP Key:
+ // amplitudeAPPKey: '',
+
+ // Obfuscates room name sent to analytics (amplitude, rtcstats)
+ // Default value is false.
+ // obfuscateRoomName: false,
+
+ // Configuration for the rtcstats server:
+ // By enabling rtcstats server every time a conference is joined the rtcstats
+ // module connects to the provided rtcstatsEndpoint and sends statistics regarding
+ // PeerConnection states along with getStats metrics polled at the specified
+ // interval.
+ // rtcstatsEnabled: false,
+ // rtcstatsStoreLogs: false,
+
+ // In order to enable rtcstats one needs to provide a endpoint url.
+ // rtcstatsEndpoint: wss://rtcstats-server-pilot.jitsi.net/,
+
+ // The interval at which rtcstats will poll getStats, defaults to 10000ms.
+ // If the value is set to 0 getStats won't be polled and the rtcstats client
+ // will only send data related to RTCPeerConnection events.
+ // rtcstatsPollInterval: 10000,
+
+ // This determines if rtcstats sends the SDP to the rtcstats server or replaces
+ // all SDPs with an empty string instead.
+ // rtcstatsSendSdp: false,
+
+ // Array of script URLs to load as lib-jitsi-meet "analytics handlers".
+ // scriptURLs: [
+ // "https://example.com/my-custom-analytics.js",
+ // ],
+
+ // By enabling watchRTCEnabled option you would want to use watchRTC feature
+ // This would also require to configure watchRTCConfigParams.
+ // Please remember to keep rtcstatsEnabled disabled for watchRTC to work.
+ // watchRTCEnabled: false,
+ },
+
+ // Logs that should go be passed through the 'log' event if a handler is defined for it
+ // apiLogLevels: ['warn', 'log', 'error', 'info', 'debug'],
+
+ // Information about the jitsi-meet instance we are connecting to, including
+ // the user region as seen by the server.
+ // deploymentInfo: {
+ // shard: "shard1",
+ // region: "europe",
+ // userRegion: "asia",
+ // },
+
+ // Array of disabled sounds.
+ // Possible values:
+ // - 'ASKED_TO_UNMUTE_SOUND'
+ // - 'E2EE_OFF_SOUND'
+ // - 'E2EE_ON_SOUND'
+ // - 'INCOMING_MSG_SOUND'
+ // - 'KNOCKING_PARTICIPANT_SOUND'
+ // - 'LIVE_STREAMING_OFF_SOUND'
+ // - 'LIVE_STREAMING_ON_SOUND'
+ // - 'NO_AUDIO_SIGNAL_SOUND'
+ // - 'NOISY_AUDIO_INPUT_SOUND'
+ // - 'OUTGOING_CALL_EXPIRED_SOUND'
+ // - 'OUTGOING_CALL_REJECTED_SOUND'
+ // - 'OUTGOING_CALL_RINGING_SOUND'
+ // - 'OUTGOING_CALL_START_SOUND'
+ // - 'PARTICIPANT_JOINED_SOUND'
+ // - 'PARTICIPANT_LEFT_SOUND'
+ // - 'RAISE_HAND_SOUND'
+ // - 'REACTION_SOUND'
+ // - 'RECORDING_OFF_SOUND'
+ // - 'RECORDING_ON_SOUND'
+ // - 'TALK_WHILE_MUTED_SOUND'
+ // disabledSounds: [],
+
+ // DEPRECATED! Use `disabledSounds` instead.
+ // Decides whether the start/stop recording audio notifications should play on record.
+ // disableRecordAudioNotification: false,
+
+ // DEPRECATED! Use `disabledSounds` instead.
+ // Disables the sounds that play when other participants join or leave the
+ // conference (if set to true, these sounds will not be played).
+ // disableJoinLeaveSounds: false,
+
+ // DEPRECATED! Use `disabledSounds` instead.
+ // Disables the sounds that play when a chat message is received.
+ // disableIncomingMessageSound: false,
+
+ // Information for the chrome extension banner
+ // chromeExtensionBanner: {
+ // // The chrome extension to be installed address
+ // url: 'https://chrome.google.com/webstore/detail/jitsi-meetings/kglhbbefdnlheedjiejgomgmfplipfeb',
+ // edgeUrl: 'https://microsoftedge.microsoft.com/addons/detail/jitsi-meetings/eeecajlpbgjppibfledfihobcabccihn',
+
+ // // Extensions info which allows checking if they are installed or not
+ // chromeExtensionsInfo: [
+ // {
+ // id: 'kglhbbefdnlheedjiejgomgmfplipfeb',
+ // path: 'jitsi-logo-48x48.png',
+ // },
+ // // Edge extension info
+ // {
+ // id: 'eeecajlpbgjppibfledfihobcabccihn',
+ // path: 'jitsi-logo-48x48.png',
+ // },
+ // ]
+ // },
+
+ // e2ee: {
+ // labels: {
+ // description: '',
+ // label: '',
+ // tooltip: '',
+ // warning: '',
+ // },
+ // externallyManagedKey: false,
+ // disabled: false,
+ // },
+
+ // Options related to end-to-end (participant to participant) ping.
+ // e2eping: {
+ // // Whether ene-to-end pings should be enabled.
+ // enabled: false,
+ //
+ // // The number of responses to wait for.
+ // numRequests: 5,
+ //
+ // // The max conference size in which e2e pings will be sent.
+ // maxConferenceSize: 200,
+ //
+ // // The maximum number of e2e ping messages per second for the whole conference to aim for.
+ // // This is used to control the pacing of messages in order to reduce the load on the backend.
+ // maxMessagesPerSecond: 250,
+ // },
+
+ // If set, will attempt to use the provided video input device label when
+ // triggering a screenshare, instead of proceeding through the normal flow
+ // for obtaining a desktop stream.
+ // NOTE: This option is experimental and is currently intended for internal
+ // use only.
+ // _desktopSharingSourceDevice: 'sample-id-or-label',
+
+ // DEPRECATED! Use deeplinking.disabled instead.
+ // If true, any checks to handoff to another application will be prevented
+ // and instead the app will continue to display in the current browser.
+ // disableDeepLinking: false,
+
+ // The deeplinking config.
+ // deeplinking: {
+ //
+ // // The desktop deeplinking config, disabled by default.
+ // desktop: {
+ // appName: 'Jitsi Meet',
+ // appScheme: 'jitsi-meet,
+ // download: {
+ // linux:
+ // 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet-x86_64.AppImage',
+ // macos: 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet.dmg',
+ // windows: 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet.exe'
+ // },
+ // enabled: false
+ // },
+ // // If true, any checks to handoff to another application will be prevented
+ // // and instead the app will continue to display in the current browser.
+ // disabled: false,
+
+ // // whether to hide the logo on the deep linking pages.
+ // hideLogo: false,
+
+ // // The ios deeplinking config.
+ // ios: {
+ // appName: 'Jitsi Meet',
+ // // Specify mobile app scheme for opening the app from the mobile browser.
+ // appScheme: 'org.jitsi.meet',
+ // // Custom URL for downloading ios mobile app.
+ // downloadLink: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905',
+ // },
+
+ // // The android deeplinking config.
+ // android: {
+ // appName: 'Jitsi Meet',
+ // // Specify mobile app scheme for opening the app from the mobile browser.
+ // appScheme: 'org.jitsi.meet',
+ // // Custom URL for downloading android mobile app.
+ // downloadLink: 'https://play.google.com/store/apps/details?id=org.jitsi.meet',
+ // // Android app package name.
+ // appPackage: 'org.jitsi.meet',
+ // fDroidUrl: 'https://f-droid.org/en/packages/org.jitsi.meet/',
+ // }
+ // },
+
+ // // The terms, privacy and help centre URL's.
+ // legalUrls: {
+ // helpCentre: 'https://web-cdn.jitsi.net/faq/meet-faq.html',
+ // privacy: 'https://jitsi.org/meet/privacy',
+ // terms: 'https://jitsi.org/meet/terms'
+ // },
+
+ // A property to disable the right click context menu for localVideo
+ // the menu has option to flip the locally seen video for local presentations
+ // disableLocalVideoFlip: false,
+
+ // A property used to unset the default flip state of the local video.
+ // When it is set to 'true', the local(self) video will not be mirrored anymore.
+ // doNotFlipLocalVideo: false,
+
+ // Mainly privacy related settings
+
+ // Disables all invite functions from the app (share, invite, dial out...etc)
+ // disableInviteFunctions: true,
+
+ // Disables storing the room name to the recents list. When in an iframe this is ignored and
+ // the room is never stored in the recents list.
+ // doNotStoreRoom: true,
+
+ // Deployment specific URLs.
+ // deploymentUrls: {
+ // // If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
+ // // user documentation.
+ // userDocumentationURL: 'https://docs.example.com/video-meetings.html',
+ // // If specified a 'Download our apps' button will be displayed in the overflow menu with a link
+ // // to the specified URL for an app download page.
+ // downloadAppsUrl: 'https://docs.example.com/our-apps.html',
+ // },
+
+ // Options related to the remote participant menu.
+ // remoteVideoMenu: {
+ // // Whether the remote video context menu to be rendered or not.
+ // disabled: true,
+ // // If set to true the 'Switch to visitor' button will be disabled.
+ // disableDemote: true,
+ // // If set to true the 'Kick out' button will be disabled.
+ // disableKick: true,
+ // // If set to true the 'Grant moderator' button will be disabled.
+ // disableGrantModerator: true,
+ // // If set to 'all' the 'Private chat' button will be disabled for all participants.
+ // // If set to 'allow-moderator-chat' the 'Private chat' button will be available for chats with moderators.
+ // // If set to 'disable-visitor-chat' the 'Private chat' button will be disabled for visitor-main participant
+ // // conversations.
+ // disablePrivateChat: 'all' | 'allow-moderator-chat' | 'disable-visitor-chat',
+ // },
+
+
+ // If set to true all muting operations of remote participants will be disabled.
+ // disableRemoteMute: true,
+
+ /**
+ External API url used to receive branding specific information.
+ If there is no url set or there are missing fields, the defaults are applied.
+ The config file should be in JSON.
+ None of the fields are mandatory and the response must have the shape:
+ {
+ // Whether participant can only send group chat message if `send-groupchat` feature is enabled in jwt.
+ groupChatRequiresPermission: false,
+ // Whether participant can only create polls if `create-polls` feature is enabled in jwt.
+ pollCreationRequiresPermission: false,
+ // The domain url to apply (will replace the domain in the sharing conference link/embed section)
+ inviteDomain: 'example-company.org',
+ // The hex value for the colour used as background
+ backgroundColor: '#fff',
+ // The url for the image used as background
+ backgroundImageUrl: 'https://example.com/background-img.png',
+ // The anchor url used when clicking the logo image
+ logoClickUrl: 'https://example-company.org',
+ // The url used for the image used as logo
+ logoImageUrl: 'https://example.com/logo-img.png',
+ // Endpoint that enables support for salesforce integration with in-meeting resource linking
+ // This is required for:
+ // listing the most recent records - salesforceUrl/records/recents
+ // searching records - salesforceUrl/records?text=${text}
+ // retrieving record details - salesforceUrl/records/${id}?type=${type}
+ // and linking the meeting - salesforceUrl/sessions/${sessionId}/records/${id}
+ // salesforceUrl: 'https://api.example.com/',
+ // Overwrite for pool of background images for avatars
+ avatarBackgrounds: ['url(https://example.com/avatar-background-1.png)', '#FFF'],
+ // The lobby/prejoin screen background
+ premeetingBackground: 'url(https://example.com/premeeting-background.png)',
+ // A list of images that can be used as video backgrounds.
+ // When this field is present, the default images will be replaced with those provided.
+ virtualBackgrounds: ['https://example.com/img.jpg'],
+ // Object containing customized icons that should replace the default ones.
+ // The keys need to be the exact same icon names used in here:
+ // https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/icons/svg/index.ts
+ // To avoid having the icons trimmed or displayed in an unexpected way, please provide svg
+ // files containing svg xml icons in the size that the default icons come in.
+ customIcons: {
+ IconArrowUp: 'https://example.com/arrow-up.svg',
+ IconDownload: 'https://example.com/download.svg',
+ IconRemoteControlStart: 'https://example.com/remote-start.svg',
+ },
+ // Object containing a theme's properties. It also supports partial overwrites of the main theme.
+ // For a list of all possible theme tokens and their current defaults, please check:
+ // https://github.com/jitsi/jitsi-meet/tree/master/resources/custom-theme/custom-theme.json
+ // For a short explanations on each of the tokens, please check:
+ // https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/ui/Tokens.ts
+ // IMPORTANT!: This is work in progress so many of the various tokens are not yet applied in code
+ // or they are partially applied.
+ customTheme: {
+ palette: {
+ ui01: "orange !important",
+ ui02: "maroon",
+ surface02: 'darkgreen',
+ ui03: "violet",
+ ui04: "magenta",
+ ui05: "blueviolet",
+ action01: 'green',
+ action01Hover: 'lightgreen',
+ disabled01: 'beige',
+ success02: 'cadetblue',
+ action02Hover: 'aliceblue',
+ },
+ typography: {
+ labelRegular: {
+ fontSize: 25,
+ lineHeight: 30,
+ fontWeight: 500,
+ }
+ }
+ }
+ }
+ */
+ // dynamicBrandingUrl: '',
+
+ // A list of allowed URL domains for shared video.
+ //
+ // NOTE:
+ // '*' is allowed value and it will allow any URL to be used for shared video. We do not recommend using '*',
+ // use it at your own risk!
+ // sharedVideoAllowedURLDomains: [ ],
+
+ // Options related to the participants pane.
+ // participantsPane: {
+ // // Enables feature
+ // enabled: true,
+ // // Hides the moderator settings tab.
+ // hideModeratorSettingsTab: false,
+ // // Hides the more actions button.
+ // hideMoreActionsButton: false,
+ // // Hides the mute all button.
+ // hideMuteAllButton: false,
+ // },
+
+ // Options related to the breakout rooms feature.
+ // breakoutRooms: {
+ // // Hides the add breakout room button. This replaces `hideAddRoomButton`.
+ // hideAddRoomButton: false,
+ // // Hides the auto assign participants button.
+ // hideAutoAssignButton: false,
+ // // Hides the join breakout room button.
+ // hideJoinRoomButton: false,
+ // },
+
+ // When true, virtual background feature will be disabled.
+ // disableVirtualBackground: false,
+
+ // When true the user cannot add more images to be used as virtual background.
+ // Only the default ones from will be available.
+ // disableAddingBackgroundImages: false,
+
+ // Sets the background transparency level. '0' is fully transparent, '1' is opaque.
+ // backgroundAlpha: 1,
+
+ // The URL of the moderated rooms microservice, if available. If it
+ // is present, a link to the service will be rendered on the welcome page,
+ // otherwise the app doesn't render it.
+ // moderatedRoomServiceUrl: 'https://moderated.jitsi-meet.example.com',
+
+ // If true, tile view will not be enabled automatically when the participants count threshold is reached.
+ // disableTileView: true,
+
+ // If true, the tiles will be displayed contained within the available space rather than enlarged to cover it,
+ // with a 16:9 aspect ratio (old behaviour).
+ // disableTileEnlargement: true,
+
+ // Controls the visibility and behavior of the top header conference info labels.
+ // If a label's id is not in any of the 2 arrays, it will not be visible at all on the header.
+ // conferenceInfo: {
+ // // those labels will not be hidden in tandem with the toolbox.
+ // alwaysVisible: ['recording', 'raised-hands-count'],
+ // // those labels will be auto-hidden in tandem with the toolbox buttons.
+ // autoHide: [
+ // 'subject',
+ // 'conference-timer',
+ // 'participants-count',
+ // 'e2ee',
+ // 'video-quality',
+ // 'insecure-room',
+ // 'highlight-moment',
+ // 'top-panel-toggle',
+ // ]
+ // },
+
+ // Hides the conference subject
+ // hideConferenceSubject: false,
+
+ // Hides the conference timer.
+ // hideConferenceTimer: false,
+
+ // Hides the recording label
+ // hideRecordingLabel: false,
+
+ // Hides the participants stats
+ // hideParticipantsStats: true,
+
+ // Sets the conference subject
+ // subject: 'Conference Subject',
+
+ // Sets the conference local subject
+ // localSubject: 'Conference Local Subject',
+
+ // This property is related to the use case when jitsi-meet is used via the IFrame API. When the property is true
+ // jitsi-meet will use the local storage of the host page instead of its own. This option is useful if the browser
+ // is not persisting the local storage inside the iframe.
+ // useHostPageLocalStorage: true,
+
+ // Etherpad ("shared document") integration.
+ //
+ // If set, add a "Open shared document" link to the bottom right menu that
+ // will open an etherpad document.
+ // etherpad_base: 'https://your-etherpad-installati.on/p/',
+
+ // To enable information about dial-in access to meetings you need to provide
+ // dialInNumbersUrl and dialInConfCodeUrl.
+ // dialInNumbersUrl returns a json array of numbers that can be used for dial-in.
+ // {"countryCode":"US","tollFree":false,"formattedNumber":"+1 123-456-7890"}
+ // dialInConfCodeUrl is the conference mapper converting a meeting id to a PIN used for dial-in
+ // or the other way around (more info in resources/cloud-api.swagger)
+
+ // You can use external service for authentication that will redirect back passing a jwt token
+ // You can use tokenAuthUrl config to point to a URL of such service.
+ // The URL for the service supports few params which will be filled in by the code.
+ // tokenAuthUrl:
+ // 'https://myservice.com/auth/{room}?code_challenge_method=S256&code_challenge={code_challenge}&state={state}'
+ // Supported parameters in tokenAuthUrl:
+ // {room} - will be replaced with the room name
+ // {code_challenge} - (A web only). A oauth 2.0 code challenge that will be sent to the service. See:
+ // https://datatracker.ietf.org/doc/html/rfc7636. The code verifier will be saved in the sessionStorage
+ // under key: 'code_verifier'.
+ // {state} - A json with the current state before redirecting. Keys that are included in the state:
+ // - room (The current room name as shown in the address bar)
+ // - roomSafe (the backend safe room name to use (lowercase), that is passed to the backend)
+ // - tenant (The tenant if any)
+ // - config.xxx (all config overrides)
+ // - interfaceConfig.xxx (all interfaceConfig overrides)
+ // - ios=true (in case ios mobile app is used)
+ // - android=true (in case android mobile app is used)
+ // - electron=true (when web is loaded in electron app)
+ // If there is a logout service you can specify its URL with:
+ // tokenLogoutUrl: 'https://myservice.com/logout'
+ // You can enable tokenAuthUrlAutoRedirect which will detect that you have logged in successfully before
+ // and will automatically redirect to the token service to get the token for the meeting.
+ // tokenAuthUrlAutoRedirect: false
+ // An option to respect the context.tenant jwt field compared to the current tenant from the url
+ // tokenRespectTenant: false,
+ // An option to get for user info (name, picture, email) in the token outside the user context.
+ // Can be used with Firebase tokens.
+ // tokenGetUserInfoOutOfContext: false,
+
+ // You can put an array of values to target different entity types in the invite dialog.
+ // Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"
+ // peopleSearchQueryTypes: ["user", "email"],
+ // Directory endpoint which is called for invite dialog autocomplete
+ // peopleSearchUrl: "https://myservice.com/api/people",
+ // Endpoint which is called to send invitation requests
+ // inviteServiceUrl: "https://myservice.com/api/invite",
+
+ // For external entities (e. g. email), the localStorage key holding the token value for directory authentication
+ // peopleSearchTokenLocation: "mytoken",
+
+
+ // Options related to visitors.
+ // visitors: {
+ // // Starts audio/video when the participant is promoted from visitor.
+ // enableMediaOnPromote: {
+ // audio: true,
+ // video: true
+ // },
+ // },
+ // The default type of desktop sharing sources that will be used in the electron app.
+ // desktopSharingSources: ['screen', 'window'],
+
+ // Disables the echo cancelation for local audio tracks.
+ // disableAEC: true,
+
+ // Disables the auto gain control for local audio tracks.
+ // disableAGC: true,
+
+ // Disables the audio processing (echo cancelation, auto gain control and noise suppression) for local audio tracks.
+ // disableAP: true,
+
+ // Disables the anoise suppression for local audio tracks.
+ // disableNS: true,
+
+ // Replaces the display name with the JID of the participants.
+ // displayJids: true,
+
+ // Enables disables talk while muted detection.
+ // enableTalkWhileMuted: true,
+
+ // Sets the peer connection ICE transport policy to "relay".
+ // forceTurnRelay: true,
+
+ // List of undocumented settings used in jitsi-meet
+ /**
+ _immediateReloadThreshold
+ deploymentInfo
+ dialOutAuthUrl
+ dialOutCodesUrl
+ dialOutRegionUrl
+ disableRemoteControl
+ iAmRecorder
+ iAmSipGateway
+ microsoftApiApplicationClientID
+ */
+
+ /**
+ * This property can be used to alter the generated meeting invite links (in combination with a branding domain
+ * which is retrieved internally by jitsi meet) (e.g. https://meet.jit.si/someMeeting
+ * can become https://brandedDomain/roomAlias)
+ */
+ // brandingRoomAlias: null,
+
+ // List of undocumented settings used in lib-jitsi-meet
+ /**
+ _peerConnStatusOutOfLastNTimeout
+ _peerConnStatusRtcMuteTimeout
+ avgRtpStatsN
+ desktopSharingSources
+ disableLocalStats
+ hiddenDomain
+ hiddenFromRecorderFeatureEnabled
+ ignoreStartMuted
+ websocketKeepAlive
+ websocketKeepAliveUrl
+ */
+
+ /**
+ * Default interval (milliseconds) for triggering mouseMoved iframe API event
+ */
+ mouseMoveCallbackInterval: 1000,
+
+ /**
+ Use this array to configure which notifications will be shown to the user
+ The items correspond to the title or description key of that notification
+ Some of these notifications also depend on some other internal logic to be displayed or not,
+ so adding them here will not ensure they will always be displayed
+
+ A falsy value for this prop will result in having all notifications enabled (e.g null, undefined, false)
+ */
+ // notifications: [
+ // 'connection.CONNFAIL', // shown when the connection fails,
+ // 'dialog.cameraConstraintFailedError', // shown when the camera failed
+ // 'dialog.cameraNotSendingData', // shown when there's no feed from user's camera
+ // 'dialog.kickTitle', // shown when user has been kicked
+ // 'dialog.liveStreaming', // livestreaming notifications (pending, on, off, limits)
+ // 'dialog.lockTitle', // shown when setting conference password fails
+ // 'dialog.maxUsersLimitReached', // shown when maximmum users limit has been reached
+ // 'dialog.micNotSendingData', // shown when user's mic is not sending any audio
+ // 'dialog.passwordNotSupportedTitle', // shown when setting conference password fails due to password format
+ // 'dialog.recording', // recording notifications (pending, on, off, limits)
+ // 'dialog.remoteControlTitle', // remote control notifications (allowed, denied, start, stop, error)
+ // 'dialog.reservationError',
+ // 'dialog.screenSharingFailedTitle', // shown when the screen sharing failed
+ // 'dialog.serviceUnavailable', // shown when server is not reachable
+ // 'dialog.sessTerminated', // shown when there is a failed conference session
+ // 'dialog.sessionRestarted', // show when a client reload is initiated because of bridge migration
+ // 'dialog.tokenAuthFailed', // show when an invalid jwt is used
+ // 'dialog.tokenAuthFailedWithReasons', // show when an invalid jwt is used with the reason behind the error
+ // 'dialog.transcribing', // transcribing notifications (pending, off)
+ // 'dialOut.statusMessage', // shown when dial out status is updated.
+ // 'liveStreaming.busy', // shown when livestreaming service is busy
+ // 'liveStreaming.failedToStart', // shown when livestreaming fails to start
+ // 'liveStreaming.unavailableTitle', // shown when livestreaming service is not reachable
+ // 'lobby.joinRejectedMessage', // shown when while in a lobby, user's request to join is rejected
+ // 'lobby.notificationTitle', // shown when lobby is toggled and when join requests are allowed / denied
+ // 'notify.audioUnmuteBlockedTitle', // shown when mic unmute blocked
+ // 'notify.chatMessages', // shown when receiving chat messages while the chat window is closed
+ // 'notify.connectedOneMember', // show when a participant joined
+ // 'notify.connectedThreePlusMembers', // show when more than 2 participants joined simultaneously
+ // 'notify.connectedTwoMembers', // show when two participants joined simultaneously
+ // 'notify.dataChannelClosed', // shown when the bridge channel has been disconnected
+ // 'notify.hostAskedUnmute', // shown to participant when host asks them to unmute
+ // 'notify.invitedOneMember', // shown when 1 participant has been invited
+ // 'notify.invitedThreePlusMembers', // shown when 3+ participants have been invited
+ // 'notify.invitedTwoMembers', // shown when 2 participants have been invited
+ // 'notify.kickParticipant', // shown when a participant is kicked
+ // 'notify.leftOneMember', // show when a participant left
+ // 'notify.leftThreePlusMembers', // show when more than 2 participants left simultaneously
+ // 'notify.leftTwoMembers', // show when two participants left simultaneously
+ // 'notify.linkToSalesforce', // shown when joining a meeting with salesforce integration
+ // 'notify.localRecordingStarted', // shown when the local recording has been started
+ // 'notify.localRecordingStopped', // shown when the local recording has been stopped
+ // 'notify.moderationInEffectCSTitle', // shown when user attempts to share content during AV moderation
+ // 'notify.moderationInEffectTitle', // shown when user attempts to unmute audio during AV moderation
+ // 'notify.moderationInEffectVideoTitle', // shown when user attempts to enable video during AV moderation
+ // 'notify.moderator', // shown when user gets moderator privilege
+ // 'notify.mutedRemotelyTitle', // shown when user is muted by a remote party
+ // 'notify.mutedTitle', // shown when user has been muted upon joining,
+ // 'notify.newDeviceAudioTitle', // prompts the user to use a newly detected audio device
+ // 'notify.newDeviceCameraTitle', // prompts the user to use a newly detected camera
+ // 'notify.noiseSuppressionFailedTitle', // shown when failed to start noise suppression
+ // 'notify.participantWantsToJoin', // shown when lobby is enabled and participant requests to join meeting
+ // 'notify.participantsWantToJoin', // shown when lobby is enabled and participants request to join meeting
+ // 'notify.passwordRemovedRemotely', // shown when a password has been removed remotely
+ // 'notify.passwordSetRemotely', // shown when a password has been set remotely
+ // 'notify.raisedHand', // shown when a participant used raise hand,
+ // 'notify.screenShareNoAudio', // shown when the audio could not be shared for the selected screen
+ // 'notify.screenSharingAudioOnlyTitle', // shown when the best performance has been affected by screen sharing
+ // 'notify.selfViewTitle', // show "You can always un-hide the self-view from settings"
+ // 'notify.startSilentTitle', // shown when user joined with no audio
+ // 'notify.suboptimalExperienceTitle', // show the browser warning
+ // 'notify.unmute', // shown to moderator when user raises hand during AV moderation
+ // 'notify.videoMutedRemotelyTitle', // shown when user's video is muted by a remote party,
+ // 'notify.videoUnmuteBlockedTitle', // shown when camera unmute and desktop sharing are blocked
+ // 'prejoin.errorDialOut',
+ // 'prejoin.errorDialOutDisconnected',
+ // 'prejoin.errorDialOutFailed',
+ // 'prejoin.errorDialOutStatus',
+ // 'prejoin.errorStatusCode',
+ // 'prejoin.errorValidation',
+ // 'recording.busy', // shown when recording service is busy
+ // 'recording.failedToStart', // shown when recording fails to start
+ // 'recording.unavailableTitle', // shown when recording service is not reachable
+ // 'toolbar.noAudioSignalTitle', // shown when a broken mic is detected
+ // 'toolbar.noisyAudioInputTitle', // shown when noise is detected for the current microphone
+ // 'toolbar.talkWhileMutedPopup', // shown when user tries to speak while muted
+ // 'transcribing.failed', // shown when transcribing fails
+ // ],
+
+ // List of notifications to be disabled. Works in tandem with the above setting.
+ // disabledNotifications: [],
+
+ // Prevent the filmstrip from autohiding when screen width is under a certain threshold
+ // disableFilmstripAutohiding: false,
+
+ // filmstrip: {
+ // // Disable the vertical/horizontal filmstrip.
+ // disabled: false,
+ // // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
+ // // (width, tiles aspect ratios) through the interfaceConfig options.
+ // disableResizable: false,
+
+ // // Disables the stage filmstrip
+ // // (displaying multiple participants on stage besides the vertical filmstrip)
+ // disableStageFilmstrip: false,
+
+ // // Default number of participants that can be displayed on stage.
+ // // The user can change this in settings. Number must be between 1 and 6.
+ // stageFilmstripParticipants: 1,
+
+ // // Disables the top panel (only shown when a user is sharing their screen).
+ // disableTopPanel: false,
+
+ // // The minimum number of participants that must be in the call for
+ // // the top panel layout to be used.
+ // minParticipantCountForTopPanel: 50,
+
+ // // The width of the filmstrip on joining meeting. Can be resized afterwards.
+ // initialWidth: 400,
+
+ // // Whether the draggable resize bar of the filmstrip is always visible. Setting this to true will make
+ // // the filmstrip always visible in case `disableResizable` is false.
+ // alwaysShowResizeBar: true,
+ // },
+
+ // Tile view related config options.
+ // tileView: {
+ // // Whether tileview should be disabled.
+ // disabled: false,
+ // // The optimal number of tiles that are going to be shown in tile view. Depending on the screen size it may
+ // // not be possible to show the exact number of participants specified here.
+ // numberOfVisibleTiles: 25,
+ // },
+
+ // Specifies whether the chat emoticons are disabled or not
+ // disableChatSmileys: false,
+
+ // Settings for the GIPHY integration.
+ // giphy: {
+ // // Whether the feature is enabled or not.
+ // enabled: false,
+ // // SDK API Key from Giphy.
+ // sdkKey: '',
+ // // Display mode can be one of:
+ // // - tile: show the GIF on the tile of the participant that sent it.
+ // // - chat: show the GIF as a message in chat
+ // // - all: all of the above. This is the default option
+ // displayMode: 'all',
+ // // How long the GIF should be displayed on the tile (in milliseconds).
+ // tileTime: 5000,
+ // // Limit results by rating: g, pg, pg-13, r. Default value: g.
+ // rating: 'pg',
+ // },
+
+ // Logging
+ // logging: {
+ // // Default log level for the app and lib-jitsi-meet.
+ // defaultLogLevel: 'trace',
+ // // Option to disable LogCollector.
+ // //disableLogCollector: true,
+ // // Individual loggers are customizable.
+ // loggers: {
+ // // The following are too verbose in their logging with the default level.
+ // 'modules/RTC/TraceablePeerConnection.js': 'info',
+ // 'modules/xmpp/strophe.util.js': 'log',
+ // },
+ // },
+
+ // Application logo url
+ // defaultLogoUrl: 'images/watermark.svg',
+
+ // Settings for the Excalidraw whiteboard integration.
+ // whiteboard: {
+ // // Whether the feature is enabled or not.
+ // enabled: true,
+ // // The server used to support whiteboard collaboration.
+ // // https://github.com/jitsi/excalidraw-backend
+ // collabServerBaseUrl: 'https://excalidraw-backend.example.com',
+ // // The user access limit to the whiteboard, introduced as a means
+ // // to control the performance.
+ // userLimit: 25,
+ // // The url for more info about the whiteboard and its usage limitations.
+ // limitUrl: 'https://example.com/blog/whiteboard-limits',
+ // },
+
+ // The watchRTC initialize config params as described :
+ // https://testrtc.com/docs/installing-the-watchrtc-javascript-sdk/#h-set-up-the-sdk
+ // https://www.npmjs.com/package/@testrtc/watchrtc-sdk
+ // watchRTCConfigParams: {
+ // /** Watchrtc api key */
+ // rtcApiKey: string;
+ // /** Identifier for the session */
+ // rtcRoomId?: string;
+ // /** Identifier for the current peer */
+ // rtcPeerId?: string;
+ // /**
+ // * ["tag1", "tag2", "tag3"]
+ // * @deprecated use 'keys' instead
+ // */
+ // rtcTags?: string[];
+ // /** { "key1": "value1", "key2": "value2"} */
+ // keys?: any;
+ // /** Enables additional logging */
+ // debug?: boolean;
+ // rtcToken?: string;
+ // /**
+ // * @deprecated No longer needed. Use "proxyUrl" instead.
+ // */
+ // wsUrl?: string;
+ // proxyUrl?: string;
+ // console?: {
+ // level: string;
+ // override: boolean;
+ // };
+ // allowBrowserLogCollection?: boolean;
+ // collectionInterval?: number;
+ // logGetStats?: boolean;
+ // },
+
+ // Hide login button on auth dialog, you may want to enable this if you are using JWT tokens to authenticate users
+ // hideLoginButton: true,
+
+ // If true remove the tint foreground on focused user camera in filmstrip
+ // disableCameraTintForeground: false,
+
+ // File sharign service.
+ // fileSharing: {
+ // // The URL of the file sharing service API. See resources/file-sharing.yaml for more details.
+ // apiUrl: 'https://example.com',
+ // // Whether the file sharing service is enabled or not.
+ // enabled: true,
+ // // Maximum file size limit (-1 value disables any file size limit check)
+ // maxFileSize: 50,
+ // },
+};
+
+// Set the default values for JaaS customers
+if (enableJaaS) {
+ config.dialInNumbersUrl = 'https://conference-mapper.jitsi.net/v1/access/dids';
+ config.dialInConfCodeUrl = 'https://conference-mapper.jitsi.net/v1/access';
+ config.roomPasswordNumberOfDigits = 10; // skip re-adding it (do not remove comment)
+}
diff --git a/css/404.scss b/css/404.scss
new file mode 100644
index 0000000..9c2a956
--- /dev/null
+++ b/css/404.scss
@@ -0,0 +1,15 @@
+.error_page {
+ width: 60%;
+ margin: 20% auto;
+ text-align: center;
+
+ h2 {
+ font-size: 3rem;
+ color : #f2f2f2;
+ }
+
+ &__message {
+ font-size: 1.5rem;
+ margin-top: 20px;
+ }
+}
diff --git a/css/_base.scss b/css/_base.scss
new file mode 100644
index 0000000..ea5a83d
--- /dev/null
+++ b/css/_base.scss
@@ -0,0 +1,190 @@
+/**
+ * Safari will limit input in input elements to one character when user-select
+ * none is applied. Other browsers already support selecting within inputs while
+ * user-select is none. As such, disallow user-select except on inputs.
+ */
+* {
+ -webkit-user-select: none;
+ user-select: none;
+
+ // Firefox only
+ scrollbar-width: thin;
+ scrollbar-color: rgba(0, 0, 0, .5) transparent;
+}
+
+input,
+textarea {
+ -webkit-user-select: text;
+ user-select: text;
+}
+
+html {
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+}
+
+body {
+ margin: 0px;
+ width: 100%;
+ height: 100%;
+ font-size: 0.75rem;
+ font-weight: 400;
+ overflow: hidden;
+ color: #F1F1F1;
+ background: #040404; // should match DEFAULT_BACKGROUND from interface_config
+}
+
+/**
+ * This will hide the focus indicator if an element receives focus via the mouse,
+ * but it will still show up on keyboard focus, thus preserving accessibility.
+ */
+.js-focus-visible :focus:not(.focus-visible) {
+ outline: none;
+}
+
+.jitsi-icon {
+ &-default svg {
+ fill: white;
+ }
+}
+
+.disabled .jitsi-icon svg {
+ fill: #929292;
+}
+
+.jitsi-icon.gray svg {
+ fill: #5E6D7A;
+ cursor: pointer;
+}
+
+p {
+ margin: 0;
+}
+
+body, input, textarea, keygen, select, button {
+ font-family: $baseFontFamily !important;
+}
+
+button, input, select, textarea {
+ margin: 0;
+ vertical-align: baseline;
+ font-size: 1em;
+}
+
+button, select, input[type="button"],
+input[type="reset"], input[type="submit"] {
+ cursor: pointer;
+}
+
+textarea {
+ word-wrap: break-word;
+ resize: none;
+ line-height: 1.5em;
+}
+
+input[type='text'], input[type='password'], textarea {
+ outline: none; /* removes the default outline */
+ resize: none; /* prevents the user-resizing, adjust to taste */
+}
+
+button {
+ color: #FFF;
+ background-color: #44A5FF;
+ border-radius: $borderRadius;
+
+ &.no-icon {
+ padding: 0 1em;
+ }
+}
+
+button,
+form {
+ display: block;
+}
+
+.watermark {
+ display: block;
+ position: absolute;
+ top: 15;
+ width: $watermarkWidth;
+ height: $watermarkHeight;
+ background-size: contain;
+ background-repeat: no-repeat;
+ z-index: $watermarkZ;
+}
+
+.leftwatermark {
+ max-width: 140px;
+ max-height:70px;
+ left: 32px;
+ top: 32px;
+ background-position: center left;
+ background-repeat: no-repeat;
+ background-size: contain;
+
+ &.no-margin {
+ left:0;
+ top:0;
+ }
+}
+
+.rightwatermark {
+ right: 32px;
+ top: 32px;
+ background-position: center right;
+}
+
+.poweredby {
+ position: absolute;
+ left: 25;
+ bottom: 7;
+ font-size: 0.875rem;
+ color: rgba(255,255,255,.50);
+ text-decoration: none;
+ z-index: $watermarkZ;
+}
+
+/**
+ * Re-style default OS scrollbar.
+ */
+::-webkit-scrollbar {
+ background: transparent;
+ width: 7px;
+ height: $scrollHeight;
+}
+
+::-webkit-scrollbar-button {
+ display: none;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-track-piece {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #3D3D3D;
+ border-radius: 4px;
+}
+
+/* Necessary for the new icons to work properly. */
+.jitsi-icon svg path {
+ fill: inherit !important;
+}
+
+.sr-only {
+ border: 0 !important;
+ clip: rect(1px, 1px, 1px, 1px) !important;
+ clip-path: inset(50%) !important;
+ height: 1px !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ padding: 0 !important;
+ position: absolute !important;
+ width: 1px !important;
+ white-space: nowrap !important;
+}
diff --git a/css/_chat.scss b/css/_chat.scss
new file mode 100644
index 0000000..b59db4b
--- /dev/null
+++ b/css/_chat.scss
@@ -0,0 +1,220 @@
+@use './variables';
+
+#chat-conversation-container {
+ // extract message input height
+ box-sizing: border-box;
+ height: calc(100% - 64px);
+ overflow: hidden;
+ position: relative;
+}
+
+#chatconversation {
+ box-sizing: border-box;
+ flex: 1;
+ font-size: 0.75rem;
+ height: calc(100% - 10px);
+ line-height: 1.25rem;
+ overflow: auto;
+ padding: 16px;
+ text-align: left;
+ word-wrap: break-word;
+
+ display: flex;
+ flex-direction: column;
+
+ &.focus-visible {
+ outline: 0;
+ margin: 4px;
+ border-radius: 0 0 variables.$borderRadius variables.$borderRadius;
+ box-shadow: 0px 0px 0px 2px #4687ed; // focus01/primary07
+ }
+
+ & > :first-child {
+ margin-top: auto;
+ }
+
+ a {
+ display: block;
+ }
+
+ a:link {
+ color: rgb(184, 184, 184);
+ }
+
+ a:visited {
+ color: white;
+ }
+
+ a:hover {
+ color: rgb(213, 213, 213);
+ }
+
+ a:active {
+ color: black;
+ }
+}
+
+.chat-input-container {
+ padding: 0 16px 24px;
+}
+
+#chat-input {
+ display: flex;
+ align-items: flex-end;
+ position: relative;
+}
+
+.chat-input {
+ flex: 1;
+ margin-right: 8px;
+}
+
+#nickname {
+ text-align: center;
+ color: #9d9d9d;
+ font-size: 1rem;
+ margin: auto 0;
+ padding: 0 16px;
+
+ label[for="nickinput"] {
+ > div > span {
+ color: #B8C7E0;
+ }
+ }
+ input {
+ height: 40px;
+ }
+
+ label {
+ line-height: 1.5rem;
+ }
+}
+
+.mobile-browser {
+ #nickname {
+ input {
+ height: 48px;
+ }
+ }
+
+ .chatmessage .usermessage {
+ font-size: 1rem;
+ }
+}
+
+.chatmessage {
+ &.error {
+ border-radius: 0px;
+
+ .timestamp,
+ .display-name {
+ display: none;
+ }
+
+ .usermessage {
+ color: #ffffff;
+ padding: 0;
+ }
+ }
+
+ .messagecontent {
+ max-width: 100%;
+ overflow: hidden;
+ }
+}
+
+#smileys {
+ font-size: 1.625rem;
+ margin: auto;
+ cursor: pointer;
+}
+
+#smileys img {
+ width: 22px;
+ padding: 2px;
+}
+
+.smiley-input {
+ display: flex;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+#smileysContainer .smiley {
+ font-size: 1.625rem;
+}
+
+.smileyContainer {
+ width: 40px;
+ height: 40px;
+ display: inline-block;
+ text-align: center;
+}
+
+.smileyContainer:hover {
+ background-color: rgba(255, 255, 255, 0.15);
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+.chat-message-group {
+ &.local {
+ align-items: flex-end;
+
+ .display-name {
+ display: none;
+ }
+
+ .timestamp {
+ text-align: right;
+ }
+ }
+
+ &.error {
+ .display-name {
+ display: none;
+ }
+ }
+}
+
+.chat-dialog {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ margin-top: -5px; // Margin set by atlaskit.
+
+ &-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 16px;
+ width: calc(100% - 32px);
+ box-sizing: border-box;
+ color: #fff;
+ font-weight: 600;
+ font-size: 1.5rem;
+ line-height: 2rem;
+
+ .jitsi-icon {
+ cursor: pointer;
+ }
+ }
+
+ #chatconversation {
+ width: 100%;
+ }
+}
+
+
+/**
+ * Make header close button more easily tappable on mobile.
+ */
+.mobile-browser .chat-dialog-header .jitsi-icon {
+ display: grid;
+ place-items: center;
+ height: 48px;
+ width: 48px;
+ background: #36383C;
+ border-radius: 3px;
+}
diff --git a/css/_chrome-extension-banner.scss b/css/_chrome-extension-banner.scss
new file mode 100644
index 0000000..f65568f
--- /dev/null
+++ b/css/_chrome-extension-banner.scss
@@ -0,0 +1,93 @@
+.chrome-extension-banner {
+ position: fixed;
+ width: 406px;
+ height: $chromeExtensionBannerHeight;
+ background: #FFF;
+ box-shadow: 0px 2px 48px rgba(0, 0, 0, 0.25);
+ border-radius: 4px;
+ z-index: 1000;
+ float: right;
+ display: flex;
+ flex-direction: column;
+ padding: 20px 20px;
+ top: $chromeExtensionBannerTop;
+ right: $chromeExtensionBannerRight;
+ &__pos_in_meeting {
+ top: $chromeExtensionBannerTopInMeeting;
+ right: $chromeExtensionBannerRightInMeeeting;
+ }
+
+ &__container {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16px;
+ }
+
+ &__button-container {
+ display: flex;
+ }
+
+ &__checkbox-container {
+ display: $chromeExtensionBannerDontShowAgainDisplay;
+ margin-left: 45px;
+ margin-top: 16px;
+ }
+
+ &__checkbox-label {
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ display: flex;
+ align-items: center;
+ letter-spacing: -0.006em;
+ color: #1C2025;
+ }
+
+ &__icon-container {
+ display: flex;
+ background: url('../images/chromeLogo.svg');
+ background-repeat: no-repeat;
+ width: 27px;
+ height: 27px;
+ }
+
+ &__text-container {
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ display: flex;
+ align-items: center;
+ letter-spacing: -0.006em;
+ color: #151531;
+ width: 329px;
+ }
+
+ &__close-container {
+ display: flex;
+ width: 12px;
+ height: 12px;
+ }
+
+ &__gray-close-icon {
+ fill: #5E6D7A;
+ width: 12px;
+ height: 12px;
+ cursor: pointer;
+ }
+
+ &__button-open-url {
+ background: #0A57EB;
+ border-radius: 24px;
+ margin-left: 45px;
+ width: 236px;
+ height: 40px;
+ cursor: pointer;
+ }
+
+ &__button-text {
+ font-weight: 600;
+ font-size: 0.875rem;
+ line-height: 2.5rem;
+ text-align: center;
+ letter-spacing: -0.006em;
+ color: #FFFFFF;
+ }
+}
\ No newline at end of file
diff --git a/css/_functions.scss b/css/_functions.scss
new file mode 100644
index 0000000..8075204
--- /dev/null
+++ b/css/_functions.scss
@@ -0,0 +1,6 @@
+/* Functions */
+
+/* Pixels to Ems function */
+@function em($value, $base: 16) {
+ @return #{$value / $base}em;
+}
\ No newline at end of file
diff --git a/css/_inlay.scss b/css/_inlay.scss
new file mode 100644
index 0000000..f6593b0
--- /dev/null
+++ b/css/_inlay.scss
@@ -0,0 +1,30 @@
+.inlay {
+ margin-top: 14%;
+ @include border-radius(4px);
+ padding: 40px 38px 44px;
+ color: #fff;
+ background: lighten(#474747, 20%);
+ text-align: center;
+
+ &__title {
+ margin: 17px 0;
+ padding-bottom: 17px;
+ color: #ffffff;
+ font-size: 1.25rem;
+ letter-spacing: 0.3px;
+ border-bottom: 1px solid lighten(#FFFFFF, 10%);
+ }
+
+ &__text {
+ color: #ffffff;
+ display: block;
+ margin-top: 22px;
+ font-size: 1rem;
+ }
+
+ &__icon {
+ margin: 0 10px;
+ font-size: 3.125rem;
+ }
+
+}
diff --git a/css/_login_menu.scss b/css/_login_menu.scss
new file mode 100644
index 0000000..2326081
--- /dev/null
+++ b/css/_login_menu.scss
@@ -0,0 +1,4 @@
+a.disabled {
+ color: gray !important;
+ pointer-events: none;
+}
diff --git a/css/_meetings_list.scss b/css/_meetings_list.scss
new file mode 100644
index 0000000..9de962e
--- /dev/null
+++ b/css/_meetings_list.scss
@@ -0,0 +1,185 @@
+.meetings-list {
+ font-size: 0.875rem;
+ color: #253858;
+ line-height: 1.25rem;
+ text-align: left;
+ text-overflow: ellipsis;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow-y: auto;
+ flex-grow: 1;
+
+ .meetings-list-empty {
+ text-align: center;
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ flex-grow: 1;
+ flex-direction: column;
+
+ .description {
+ color: #2f3237;
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ margin-bottom: 16px;
+ max-width: 436px;
+ }
+ }
+
+ .meetings-list-empty-image {
+ text-align: center;
+ margin: 24px 0 20px 0;
+ }
+
+ .meetings-list-empty-button {
+ align-items: center;
+ color: #0163FF;
+ cursor: pointer;
+ display: flex;
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ margin: 24px 0 32px 0;
+ }
+
+ .meetings-list-empty-icon {
+ display: inline-block;
+ margin-right: 8px;
+ }
+
+ .button {
+ background: #0074E0;
+ border-radius: 4px;
+ color: #FFFFFF;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 8px;
+ cursor: pointer;
+ }
+
+ .calendar-action-buttons {
+ .button {
+ margin: 0px 10px;
+ }
+ }
+
+ .item {
+ background: #fff;
+ box-sizing: border-box;
+ border-radius: 4px;
+ display: inline-flex;
+ margin: 4px 4px 0 4px;
+ min-height: 60px;
+ width: calc(100% - 8px);
+ word-break: break-word;
+ display: flex;
+ flex-direction: row;
+ text-align: left;
+
+ &:first-child {
+ margin-top: 0px;
+ }
+
+ .left-column {
+ order: -1;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 0;
+ padding-left: 16px;
+ padding-top: 13px;
+ }
+
+ .right-column {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ flex-grow: 1;
+ padding-left: 16px;
+ padding-top: 13px;
+ position: relative;
+ }
+
+ .title {
+ font-size: 0.75rem;
+ font-weight: 600;
+ line-height: 1rem;
+ margin-bottom: 4px;
+ }
+
+ .subtitle {
+ color: #5E6D7A;
+ font-weight: normal;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ }
+
+
+ .actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-grow: 0;
+ margin-right: 16px;
+ }
+
+ &.with-click-handler {
+ cursor: pointer;
+ }
+
+ &.with-click-handler:hover,
+ &.with-click-handler:focus {
+ background-color: #c7ddff;
+ }
+
+ .add-button {
+ width: 30px;
+ height: 30px;
+ padding: 0px;
+ }
+
+ i {
+ cursor: inherit;
+ }
+
+ .join-button {
+ display: none;
+ }
+
+ &:hover .join-button {
+ display: block
+ }
+ }
+
+ .delete-meeting {
+ display: none;
+ margin-right: 16px;
+ position: absolute;
+
+ &>svg {
+ fill: #0074e0;
+ }
+ }
+
+ .item:hover,
+ .item:focus,
+ .item:focus-within {
+ .delete-meeting {
+ display: block;
+ }
+
+ .delete-meeting:hover {
+ &>svg {
+ fill: #4687ED;
+ }
+ }
+ }
+
+ @media (max-width: 1024px) { /* Targets iPads and smaller devices */
+ .item {
+ .delete-meeting {
+ display: block !important;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/css/_meter.scss b/css/_meter.scss
new file mode 100644
index 0000000..57b4281
--- /dev/null
+++ b/css/_meter.scss
@@ -0,0 +1,30 @@
+.jitsi-icon {
+ &.metr {
+ display: inline-block;
+
+ & > svg {
+ fill: #525252;
+ width: 38px;
+ }
+ }
+
+ &.metr--disabled {
+ & > svg {
+ fill: #525252;
+ }
+ }
+}
+
+.metr-l-0 {
+ rect:first-child {
+ fill: #1EC26A;
+ }
+}
+
+@for $i from 1 through 7 {
+ .metr-l-#{$i} {
+ rect:nth-child(-n+#{$i+1}) {
+ fill: #1EC26A;
+ }
+ }
+}
diff --git a/css/_mini_toolbox.scss b/css/_mini_toolbox.scss
new file mode 100644
index 0000000..14f8737
--- /dev/null
+++ b/css/_mini_toolbox.scss
@@ -0,0 +1,30 @@
+.always-on-top-toolbox {
+ background-color: $newToolbarBackgroundColor;
+ border-radius: 3px;
+ display: flex;
+ z-index: $toolbarZ;
+
+ .toolbox-icon {
+ cursor: pointer;
+ padding: 7px;
+ width: 22px;
+ height : 22px;
+
+ &.toggled {
+ background: none;
+ }
+
+ &.disabled {
+ cursor: initial;
+ }
+ }
+}
+
+.always-on-top-toolbox {
+ flex-direction: row;
+ left: 50%;
+ position: absolute;
+ bottom: 10px;
+ transform: translateX(-50%);
+ padding: 3px !important;
+}
diff --git a/css/_mixins.scss b/css/_mixins.scss
new file mode 100644
index 0000000..673cd54
--- /dev/null
+++ b/css/_mixins.scss
@@ -0,0 +1,209 @@
+/**
+ * Animation mixin.
+ */
+@mixin animation($animate...) {
+ $max: length($animate);
+ $animations: '';
+
+ @for $i from 1 through $max {
+ $animations: #{$animations + nth($animate, $i)};
+
+ @if $i < $max {
+ $animations: #{$animations + ", "};
+ }
+ }
+ -webkit-animation: $animations;
+ -moz-animation: $animations;
+ -o-animation: $animations;
+ animation: $animations;
+}
+
+@mixin flex() {
+ display: -webkit-box;
+ display: -moz-box;
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+}
+
+/**
+ * Keyframes mixin.
+ */
+@mixin keyframes($animationName) {
+ @-webkit-keyframes #{$animationName} {
+ @content;
+ }
+ @-moz-keyframes #{$animationName} {
+ @content;
+ }
+ @-o-keyframes #{$animationName} {
+ @content;
+ }
+ @keyframes #{$animationName} {
+ @content;
+ }
+}
+
+@mixin circle($diameter) {
+ width: $diameter;
+ height: $diameter;
+ border-radius: 50%;
+}
+
+/**
+* Absolute position the element at the top left corner
+**/
+@mixin topLeft() {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+@mixin absoluteAligning() {
+ top: 50%;
+ left: 50%;
+ position: absolute;
+ @include transform(translate(-50%, -50%));
+}
+
+/**
+* Defines the maximum width and height
+**/
+@mixin maxSize($value) {
+ max-width: $value;
+ max-height: $value;
+}
+
+@mixin transform($func) {
+ -moz-transform: $func;
+ -ms-transform: $func;
+ -webkit-transform: $func;
+ -o-transform: $func;
+ transform: $func;
+}
+
+@mixin transition($transition...) {
+ -moz-transition: $transition;
+ -o-transition: $transition;
+ -webkit-transition: $transition;
+ transition: $transition;
+}
+
+/**
+ * Mixin styling a placeholder.
+ **/
+@mixin placeholder() {
+ $selectors: (
+ "::-webkit-input-placeholder",
+ "::-moz-placeholder",
+ ":-moz-placeholder",
+ ":-ms-input-placeholder"
+ );
+
+ @each $selector in $selectors {
+ #{$selector} {
+ @content;
+ }
+ }
+}
+
+/**
+ * Mixin styling a slider track for different browsers.
+ **/
+@mixin slider() {
+ $selectors: (
+ "input[type=range]::-webkit-slider-runnable-track",
+ "input[type=range]::-moz-range-track",
+ "input[type=range]::-ms-track"
+ );
+
+ @each $selector in $selectors {
+ #{$selector} {
+ @content;
+ }
+ }
+}
+
+/**
+ * Mixin styling a slider thumb for different browsers.
+ **/
+@mixin slider-thumb() {
+ $selectors: (
+ "input[type=range]::-webkit-slider-thumb",
+ "input[type=range]::-moz-range-thumb",
+ "input[type=range]::-ms-thumb"
+ );
+
+ @each $selector in $selectors {
+ #{$selector} {
+ @content;
+ }
+ }
+}
+
+@mixin box-shadow($h, $y, $blur, $color, $inset: false) {
+ @if $inset {
+ -webkit-box-shadow: inset $h $y $blur $color;
+ -moz-box-shadow: inset $h $y $blur $color;
+ box-shadow: inset $h $y $blur $color;
+ } @else {
+ -webkit-box-shadow: $h $y $blur $color;
+ -moz-box-shadow: $h $y $blur $color;
+ box-shadow: $h $y $blur $color;
+ }
+}
+
+@mixin no-box-shadow {
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+}
+
+@mixin box-sizing($box-model) {
+ -webkit-box-sizing: $box-model; // Safari <= 5
+ -moz-box-sizing: $box-model; // Firefox <= 19
+ box-sizing: $box-model;
+}
+
+@mixin border-radius($radius) {
+ -webkit-border-radius: $radius;
+ border-radius: $radius;
+ /* stops bg color from leaking outside the border: */
+ background-clip: padding-box;
+}
+
+@mixin opacity($opacity) {
+ opacity: $opacity;
+ $opacity-ie: $opacity * 100;
+ -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=$opacity-ie);
+ filter: alpha(opacity=$opacity-ie); //IE8
+}
+
+@mixin text-truncate {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/**
+ * Creates a semi-transparent background with the given color and alpha
+ * (opacity) value.
+ */
+@mixin transparentBg($color, $alpha) {
+ background-color: rgba(red($color), green($color), blue($color), $alpha);
+}
+
+/**
+ * Change the direction of the current element to LTR, but do not change the direction
+ * of its children; Keep them RTL.
+ */
+@mixin ltr {
+ body[dir=rtl] & {
+ direction: ltr;
+
+ & > * {
+ direction: rtl;
+ }
+ }
+}
diff --git a/css/_navigate_section_list.scss b/css/_navigate_section_list.scss
new file mode 100644
index 0000000..ba1b454
--- /dev/null
+++ b/css/_navigate_section_list.scss
@@ -0,0 +1,77 @@
+%navigate-section-list-text {
+ width: 100%;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ color: $welcomePageTitleColor;
+ text-align: left;
+ font-family: 'open_sanslight', Helvetica, sans-serif;
+}
+%navigate-section-list-tile-text {
+ @extend %navigate-section-list-text;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ float: left;
+}
+.navigate-section-list-tile {
+ background-color: #1754A9;
+ border-radius: 4px;
+ box-sizing: border-box;
+ display: inline-flex;
+ margin-bottom: 8px;
+ margin-right: 8px;
+ min-height: 100px;
+ padding: 16px;
+ width: 100%;
+
+ &.with-click-handler {
+ cursor: pointer;
+ }
+
+ &.with-click-handler:hover {
+ background-color: #1a5dbb;
+ }
+
+ i {
+ cursor: inherit;
+ }
+
+ .element-after {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .join-button {
+ display: none;
+ }
+
+ &:hover .join-button {
+ display: block
+ }
+}
+.navigate-section-tile-body {
+ @extend %navigate-section-list-tile-text;
+ font-weight: normal;
+ line-height: 1.5rem;
+}
+.navigate-section-list-tile-info {
+ flex: 1;
+ word-break: break-word;
+}
+.navigate-section-tile-title {
+ @extend %navigate-section-list-tile-text;
+ font-weight: bold;
+ line-height: 1.5rem;
+}
+.navigate-section-section-header {
+ @extend %navigate-section-list-text;
+ font-weight: bold;
+ margin-bottom: 16px;
+ display: block;
+}
+.navigate-section-list {
+ position: relative;
+ margin-top: 36px;
+ margin-bottom: 36px;
+ width: 100%;
+}
diff --git a/css/_participants-pane.scss b/css/_participants-pane.scss
new file mode 100644
index 0000000..4ef4d3b
--- /dev/null
+++ b/css/_participants-pane.scss
@@ -0,0 +1,6 @@
+.jitsi-icon {
+ &-dominant-speaker {
+ background-color: #1EC26A;
+ border-radius: 3px;
+ }
+}
diff --git a/css/_plan-limit.scss b/css/_plan-limit.scss
new file mode 100644
index 0000000..e69de29
diff --git a/css/_policy.scss b/css/_policy.scss
new file mode 100644
index 0000000..a370f46
--- /dev/null
+++ b/css/_policy.scss
@@ -0,0 +1,15 @@
+.policy {
+ &__logo {
+ display: block;
+ width: 200px;
+ height: 50px;
+ margin: 30px auto 0;
+ }
+
+ &__text {
+ text-align: center;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 300;
+ }
+}
\ No newline at end of file
diff --git a/css/_popover.scss b/css/_popover.scss
new file mode 100644
index 0000000..c152590
--- /dev/null
+++ b/css/_popover.scss
@@ -0,0 +1,35 @@
+.popover {
+ z-index: 8;
+
+ .popover-content {
+ position: relative;
+ }
+
+ &.hover {
+ margin: -16px -24px;
+
+ .popover-content {
+ margin: 16px 24px;
+
+ &.top {
+ bottom: 8px;
+ }
+
+ &.bottom {
+ top: 4px;
+ }
+
+ &.left {
+ right: 4px;
+ }
+
+ &.right {
+ left: 4px;
+ }
+ }
+ }
+}
+
+.excalidraw .popover {
+ margin: 0;
+}
diff --git a/css/_popup_menu.scss b/css/_popup_menu.scss
new file mode 100644
index 0000000..17d1ca9
--- /dev/null
+++ b/css/_popup_menu.scss
@@ -0,0 +1,19 @@
+/**
+* Initialize
+**/
+
+.popupmenu__contents {
+ .popupmenu__volume-slider {
+ &::-webkit-slider-runnable-track {
+ background-color: #246FE5;
+ }
+
+ &::-moz-range-track {
+ background-color: #246FE5;
+ }
+
+ &::-ms-fill-lower {
+ background-color: #246FE5;
+ }
+ }
+}
diff --git a/css/_promotional-footer.scss b/css/_promotional-footer.scss
new file mode 100644
index 0000000..16aa956
--- /dev/null
+++ b/css/_promotional-footer.scss
@@ -0,0 +1 @@
+/** Insert custom CSS for any additional content in the promotional footer **/
diff --git a/css/_reactions-menu.scss b/css/_reactions-menu.scss
new file mode 100644
index 0000000..3406c25
--- /dev/null
+++ b/css/_reactions-menu.scss
@@ -0,0 +1,214 @@
+@use 'sass:math';
+
+.reactions-menu {
+ width: 330px;
+ background: #242528;
+ box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
+ border-radius: 6px;
+ padding: 16px;
+
+ &.with-gif {
+ width: 380px;
+
+ .reactions-row .toolbox-button:last-of-type {
+ top: 3px;
+
+ & .toolbox-icon.toggled {
+ background-color: #000000;
+ }
+ }
+ }
+
+ &.overflow {
+ width: 100%;
+
+ .toolbox-icon {
+ width: 48px;
+ height: 48px;
+
+ span.emoji {
+ width: 48px;
+ height: 48px;
+ }
+ }
+
+ .reactions-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+
+ .toolbox-button {
+ margin-right: 0;
+ }
+
+ .toolbox-button:last-of-type {
+ top: 0;
+ }
+ }
+ }
+
+ .toolbox-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+
+ span.emoji {
+ width: 40px;
+ height: 40px;
+ font-size: 1.375rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: font-size ease .1s;
+
+ @for $i from 1 through 12 {
+ &.increase-#{$i}{
+ font-size: calc(1.25rem + #{$i}px);
+ }
+ }
+ }
+ }
+
+ .reactions-row {
+ .toolbox-button {
+ margin-right: 8px;
+ touch-action: manipulation;
+ position: relative;
+ }
+
+ .toolbox-button:last-of-type {
+ margin-right: 0;
+ }
+ }
+
+ .raise-hand-row {
+ margin-top: 16px;
+
+ .toolbox-button {
+ width: 100%;
+ }
+
+ .toolbox-icon {
+ width: 100%;
+ flex-direction: row;
+ align-items: center;
+
+ span.text {
+ font-style: normal;
+ font-weight: 600;
+ font-size: 0.875rem;
+ line-height: 1.5rem;
+ margin-left: 8px;
+ }
+ }
+ }
+}
+
+.reactions-animations-overflow-container {
+ position: absolute;
+ width: 20%;
+ bottom: 0;
+ left: 40%;
+ height: 0;
+}
+
+.reactions-menu-popup-container {
+ display: inline-block;
+ position: relative;
+}
+
+.reactions-animations-container {
+ left: 50%;
+ bottom: 0px;
+ display: inline-block;
+ position: absolute;
+}
+
+$reactionCount: 20;
+
+@function random($min, $max) {
+ @return math.random() * ($max - $min) + $min;
+}
+
+.reaction-emoji {
+ position: absolute;
+ font-size: 1.5rem;
+ line-height: 2rem;
+ width: 32px;
+ height: 32px;
+ top: 0;
+ left: 20px;
+ opacity: 0;
+ z-index: 1;
+
+ &.reaction-0 {
+ animation: flowToRight 5s forwards ease-in-out;
+ }
+
+ @for $i from 1 through $reactionCount {
+ &.reaction-#{$i} {
+ animation: animation-#{$i} 5s forwards ease-in-out;
+ top: #{random(-40, 10)}px;
+ left: #{random(0, 30)}px;
+ }
+}
+}
+
+@keyframes flowToRight {
+ 0% {
+ transform: translate(0px, 0px) scale(0.6);
+ opacity: 1;
+ }
+
+ 70% {
+ transform: translate(40px, -70dvh) scale(1.5);
+ opacity: 1;
+ }
+
+ 75% {
+ transform: translate(40px, -70dvh) scale(1.5);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translate(140px, -50dvh) scale(1);
+ opacity: 0;
+ }
+}
+
+@mixin animation-list {
+ @for $i from 1 through $reactionCount {
+ $topX: random(-100, 100);
+ $topY: random(65, 75);
+ $bottomX: random(150, 200);
+ $bottomY: random(40, 50);
+
+ @if $topX < 0 {
+ $bottomX: -$bottomX;
+ }
+
+ @keyframes animation-#{$i} {
+ 0% {
+ transform: translate(0, 0) scale(0.6);
+ opacity: 1;
+ }
+
+ 70% {
+ transform: translate(#{$topX}px, -#{$topY}dvh) scale(1.5);
+ opacity: 1;
+ }
+
+ 75% {
+ transform: translate(#{$topX}px, -#{$topY}dvh) scale(1.5);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translate(#{$bottomX}px, -#{$bottomY}dvh) scale(1);
+ opacity: 0;
+ }
+ }
+ }
+}
+
+@include animation-list;
diff --git a/css/_recording.scss b/css/_recording.scss
new file mode 100644
index 0000000..681c407
--- /dev/null
+++ b/css/_recording.scss
@@ -0,0 +1,199 @@
+.recording-dialog {
+ flex: 0;
+ flex-direction: column;
+
+ .recording-header {
+ align-items: center;
+ display: flex;
+ flex: 0;
+ flex-direction: row;
+ justify-content: space-between;
+
+ .recording-title {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.875rem;
+ margin-left: 16px;
+ max-width: 70%;
+
+ &-no-space {
+ margin-left: 0;
+ }
+ }
+
+ &.space-top {
+ margin-top: 10px;
+ }
+ }
+
+ .recording-header-line {
+ border-top: 1px solid #5e6d7a;
+ padding-top: 16px;
+ margin-top: 16px;
+ }
+
+ .local-recording-warning {
+ margin-top: 8px;
+ display: block;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ padding: 8px 16px;
+
+ &.text {
+ color: #fff;
+ background-color: #3D3D3D;
+ }
+
+ &.notification {
+ color: #040404;
+ background-color: #F8AE1A;
+ }
+ }
+
+ .recording-icon-container {
+ display: inline-flex;
+ align-items: center;
+ }
+
+ .file-sharing-icon-container {
+ background-color: #525252;
+ border-radius: 4px;
+ height: 40px;
+ justify-content: center;
+ width: 42px;
+ }
+
+ .cloud-content-recording-icon-container {
+ background-color: #FFFFFF;
+ border-radius: 4px;
+ height: 40px;
+ justify-content: center;
+ width: 40px;
+ }
+
+ .jitsi-recording-header {
+ margin-bottom: 16px;
+ }
+
+ .jitsi-content-recording-icon-container-with-switch {
+ background-color: #FFFFFF;
+ border-radius: 4px;
+ height: 40px;
+ width: 40px;
+ }
+
+ .jitsi-content-recording-icon-container-without-switch {
+ background-color: #FFFFFF;
+ border-radius: 4px;
+ height: 40px;
+ width: 46px;
+ }
+
+ .recording-icon {
+ height: 40px;
+ object-fit: contain;
+ width: 40px;
+ }
+
+ .content-recording-icon {
+ height: 18px;
+ margin: 10px 0 0 10px;
+ object-fit: contain;
+ width: 18px;
+ }
+
+ .recording-file-sharing-icon {
+ height: 18px;
+ object-fit: contain;
+ width: 18px;
+ }
+
+ .recording-info{
+ background-color: #FFD740;
+ color: black;
+ display: inline-flex;
+ margin: 32px 0;
+ width: 100%;
+ }
+
+ .recording-info-icon {
+ align-self: center;
+ height: 14px;
+ margin: 0 24px 0 16px;
+ object-fit: contain;
+ width: 14px;
+ }
+
+ .recording-info-title {
+ display: inline-flex;
+ font-size: 0.875rem;
+ width: 290px
+ }
+
+ .recording-switch {
+ margin-left: auto;
+ }
+
+ .authorization-panel {
+ display: flex;
+ flex-direction: column;
+ margin: 0 40px 10px 40px;
+ padding-bottom: 10px;
+
+ .logged-in-panel {
+ padding: 10px;
+ }
+ }
+}
+
+.live-stream-dialog {
+ /**
+ * Set font-size to be consistent with Atlaskit FieldText.
+ */
+ font-size: 0.875rem;
+
+ .broadcast-dropdown {
+ text-align: left;
+ }
+
+ .form-footer {
+ display: flex;
+ margin-top: 5px;
+ text-align: right;
+ flex-direction: column;
+
+ .help-container {
+ display: flex;
+ }
+ }
+
+ .live-stream-cta {
+ a {
+ cursor: pointer;
+ }
+ }
+
+ .google-api {
+ margin-top: 10px;
+ min-height: 36px;
+ text-align: center;
+ width: 100%;
+ }
+
+ .google-error {
+ color: #c61600;
+ }
+
+ .google-panel {
+ align-items: center;
+ border-bottom: 2px solid rgba(0, 0, 0, 0.3);
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 10px;
+ }
+
+ .warning-text {
+ color:#FFD740;
+ font-size: 0.75rem;
+ }
+}
diff --git a/css/_redirect_page.scss b/css/_redirect_page.scss
new file mode 100644
index 0000000..1fbe474
--- /dev/null
+++ b/css/_redirect_page.scss
@@ -0,0 +1,40 @@
+.redirectPageMessage {
+ width: 30%;
+ margin: 20% auto;
+ text-align: center;
+ font-size: 1.5rem;
+
+ .thanks-msg {
+ border-bottom: 1px solid #FFFFFF;
+ padding-left: 30px;
+ padding-right: 30px;
+ p {
+ margin: 30px auto;
+ font-size: 1.5rem;
+ line-height: 1.5rem;
+ }
+ }
+ .hint-msg {
+ p {
+ margin: 26px auto;
+ font-weight: 600;
+ font-size: 1rem;
+ line-height: 1.125rem;
+ .hint-msg__holder{
+ font-weight: 200;
+ }
+ }
+ .happy-software{
+ width: 120px;
+ height: 86px;
+ margin: 0 auto;
+ background: transparent;
+ }
+ }
+ .forbidden-msg {
+ p {
+ font-size: 1rem;
+ margin-top: 15px;
+ }
+ }
+}
diff --git a/css/_reset.scss b/css/_reset.scss
new file mode 100644
index 0000000..ae1fe50
--- /dev/null
+++ b/css/_reset.scss
@@ -0,0 +1,231 @@
+/* Fonts and line heights */
+/**
+ * RESET
+ */
+html,
+body,
+p,
+div,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+img,
+pre,
+form,
+fieldset {
+ margin: 0;
+ padding: 0;
+}
+ul,
+ol,
+dl {
+ margin: 0;
+}
+img,
+fieldset {
+ border: 0;
+}
+@-moz-document url-prefix() {
+ img {
+ font-size: 0;
+ }
+ img:-moz-broken {
+ font-size: inherit;
+ }
+}
+/* https://github.com/necolas/normalize.css */
+/* Customised to remove styles for unsupported browsers */
+details,
+main,
+summary {
+ display: block;
+}
+audio,
+canvas,
+progress,
+video {
+ display: inline-block;
+ transition: object-position 0.5s ease 0s;
+ vertical-align: baseline;
+}
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+[hidden],
+template {
+ display: none;
+}
+input[type="button"],
+input[type="submit"],
+input[type="reset"] {
+ -webkit-appearance: button;
+}
+/**
+ * TYPOGRAPHY - 14px base font size, agnostic font stack
+ */
+body {
+ color: #333;
+ font-family: Arial, sans-serif;
+ font-size: 0.875rem;
+ line-height: 1.42857142857143;
+}
+/* International Font Stacks*/
+[lang|=en] {
+ font-family: Arial, sans-serif;
+}
+[lang|=ja] {
+ font-family: "Hiragino Kaku Gothic Pro", "ヒラギノ角ゴ Pro W3", "メイリオ", Meiryo, "MS Pゴシック", Verdana, Arial, sans-serif;
+}
+/* Default margins */
+p,
+ul,
+ol,
+dl,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+blockquote,
+pre {
+ margin: 10px 0 0 0;
+}
+/* No top margin to interfere with box padding */
+p:first-child,
+ul:first-child,
+ol:first-child,
+dl:first-child,
+h1:first-child,
+h2:first-child,
+h3:first-child,
+h4:first-child,
+h5:first-child,
+h6:first-child,
+blockquote:first-child,
+pre:first-child {
+ margin-top: 0;
+}
+/* Headings: desired line height in px / font size = unitless line height */
+h1 {
+ color: #333;
+ font-size: 2rem;
+ font-weight: normal;
+ line-height: 1.25;
+ text-transform: none;
+ margin: 30px 0 0 0;
+}
+h2 {
+ color: #333;
+ font-size: 1.5rem;
+ font-weight: normal;
+ line-height: 1.25;
+ text-transform: none;
+ margin: 30px 0 0 0;
+}
+h3 {
+ color: #333;
+ font-size: 1.25rem;
+ font-weight: normal;
+ line-height: 1.5;
+ text-transform: none;
+ margin: 30px 0 0 0;
+}
+h4 {
+ font-size: 1rem;
+ font-weight: bold;
+ line-height: 1.25;
+ text-transform: none;
+ margin: 20px 0 0 0;
+}
+h5 {
+ color: #333;
+ font-size: 0.875rem;
+ font-weight: bold;
+ line-height: 1.42857143;
+ text-transform: none;
+ margin: 20px 0 0 0;
+}
+h6 {
+ color: #707070;
+ font-size: 0.75rem;
+ font-weight: bold;
+ line-height: 1.66666667;
+ text-transform: uppercase;
+ margin: 20px 0 0 0;
+}
+h1:first-child,
+h2:first-child,
+h3:first-child,
+h4:first-child,
+h5:first-child,
+h6:first-child {
+ margin-top: 0;
+}
+/* Nice styles for using subheadings */
+h1 + h2,
+h2 + h3,
+h3 + h4,
+h4 + h5,
+h5 + h6 {
+ margin-top: 10px;
+}
+
+
+/* Other typographical elements */
+small {
+ color: #707070;
+ font-size: 0.75rem;
+ line-height: 1.33333333333333;
+}
+code,
+kbd {
+ font-family: monospace;
+}
+var,
+address,
+dfn,
+cite {
+ font-style: italic;
+}
+cite:before {
+ content: "\2014 \2009";
+}
+blockquote {
+ border-left: 1px solid #ccc;
+ color: #707070;
+ margin-left: 19px;
+ padding: 10px 20px;
+}
+blockquote > cite {
+ display: block;
+ margin-top: 10px;
+}
+q {
+ color: #707070;
+}
+q:before {
+ content: open-quote;
+}
+q:after {
+ content: close-quote;
+}
+abbr {
+ border-bottom: 1px #707070 dotted;
+ cursor: help;
+}
+
+a {
+ color: #44A5FF;
+ text-decoration: none;
+ font-weight: bold;
+}
+a:focus,
+a:hover,
+a:active {
+ text-decoration: underline;
+}
diff --git a/css/_responsive.scss b/css/_responsive.scss
new file mode 100644
index 0000000..626af69
--- /dev/null
+++ b/css/_responsive.scss
@@ -0,0 +1,62 @@
+@media only screen and (max-width: $verySmallScreen) {
+ .welcome {
+ display: block;
+
+ #enter_room {
+ .welcome-page-button {
+ font-size: 1rem;
+ left: 0;
+ text-align: center;
+ width: 100%;
+ }
+ }
+
+ .header {
+ background-color: #002637;
+
+ .insecure-room-name-warning {
+ width: 100%;
+ }
+
+ #enter_room {
+ width: 100%;
+
+ .join-meeting-container {
+ padding: 0;
+ flex-direction: column;
+ background: transparent;
+ }
+
+ .enter-room-input-container {
+ padding-right: 0;
+ margin-bottom: 10px;
+ }
+ }
+ }
+
+ .header-text-title {
+ text-align: center;
+ }
+
+ .welcome-cards-container {
+ padding: 0;
+ }
+
+ &.without-content {
+ .header {
+ height: 100%;
+ }
+ }
+
+ #moderated-meetings {
+ display: none;
+ }
+
+ .welcome-footer-row-block {
+ display: flex;
+ flex-direction: column;
+ gap:12px;
+ align-items: center;
+ }
+ }
+}
diff --git a/css/_settings-button.scss b/css/_settings-button.scss
new file mode 100644
index 0000000..aa68f8b
--- /dev/null
+++ b/css/_settings-button.scss
@@ -0,0 +1,74 @@
+.settings-button-container {
+ position: relative;
+
+ .toolbox-icon {
+ align-items: center;
+ border-radius: 3px;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+
+ &.disabled, .disabled & {
+ cursor: initial;
+ color: #929292;
+ background-color: #36383c;
+
+ &:hover {
+ background-color: #36383c;
+ }
+ }
+ }
+}
+
+.settings-button-small-icon {
+ background: #36383C;
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.1);
+ border-radius: 3px;
+ cursor: pointer;
+ padding: 1px;
+ position: absolute;
+ right: -4px;
+ top: -3px;
+
+ &:hover {
+ background: #F2F3F4;
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.1);
+
+ & svg {
+ fill: #040404;
+ }
+
+ &.settings-button-small-icon--disabled {
+ background: #36383C;
+
+ & svg {
+ fill: #929292;
+ }
+ }
+ }
+
+ & svg {
+ fill: #fff;
+ }
+
+ &--disabled {
+ background-color: #36383c;
+ cursor: default;
+
+ & svg {
+ fill: #929292;
+ }
+ }
+}
+
+.settings-button-small-icon-container {
+ position: absolute;
+ right: -4px;
+ top: -3px;
+
+ & .settings-button-small-icon {
+ position: relative;
+ top: 0;
+ right: 0;
+ }
+}
diff --git a/css/_subject.scss b/css/_subject.scss
new file mode 100644
index 0000000..5efb9b6
--- /dev/null
+++ b/css/_subject.scss
@@ -0,0 +1,64 @@
+.subject {
+ color: #fff;
+ transition: opacity .6s ease-in-out;
+ z-index: $toolbarZ + 2;
+ margin-top: 20px;
+ opacity: 0;
+
+ &.visible {
+ opacity: 1;
+ }
+
+ autoHide.with-always-on {
+ overflow: hidden;
+ animation: hideSubject forwards .6s ease-out;
+
+ & > .subject-info-container {
+ justify-content: flex-start;
+ }
+
+ &.visible {
+ animation: showSubject forwards .6s ease-out;
+ }
+ }
+}
+
+.subject-info-container {
+ display: flex;
+ justify-content: center;
+ margin: 0 auto;
+ height: 28px;
+
+ @media (max-width: 500px) {
+ flex-wrap: wrap;
+ }
+}
+
+.details-container {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ height: 48px;
+}
+
+@keyframes hideSubject {
+ 0% {
+ max-width: 100%;
+ }
+
+ 100% {
+ max-width: 0;
+ }
+}
+
+@keyframes showSubject {
+ 0% {
+ max-width: 0%;
+ }
+
+ 100% {
+ max-width: 100%;
+ }
+}
diff --git a/css/_toolbars.scss b/css/_toolbars.scss
new file mode 100644
index 0000000..d1327a9
--- /dev/null
+++ b/css/_toolbars.scss
@@ -0,0 +1,204 @@
+/**
+ * Round badge.
+ */
+.badge-round {
+ background-color: #165ECC;
+ border-radius: 50%;
+ box-sizing: border-box;
+ color: #FFFFFF;
+ // Do not inherit the font-family from the toolbar button, because it's an
+ // icon style.
+ font-family: $baseFontFamily;
+ font-size: 0.5rem;
+ font-weight: 700;
+ line-height: 0.75rem;
+ min-width: 13px;
+ overflow: hidden;
+ text-align: center;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+}
+
+/**
+ * TODO: when the old filmstrip has been removed, remove the "new-" prefix.
+ */
+.new-toolbox {
+ bottom: calc((#{$newToolbarSize} * 2) * -1);
+ left: 0;
+ position: absolute;
+ right: 0;
+ transition: bottom .3s ease-in;
+ width: 100%;
+ pointer-events: none;
+ z-index: $toolbarZ + 2;
+
+ &.shift-up {
+ bottom: calc(((#{$newToolbarSize} + 30px) * 2) * -1);
+
+ .toolbox-content {
+ margin-bottom: 46px;
+ }
+ }
+
+ &.visible {
+ bottom: 0;
+ }
+
+ &.no-buttons {
+ display: none;
+ }
+}
+
+.toolbox-content {
+ align-items: center;
+ box-sizing: border-box;
+ display: flex;
+ margin-bottom: 16px;
+ position: relative;
+ z-index: $toolbarZ;
+ pointer-events: none;
+
+ .toolbox-button-wth-dialog {
+ display: inline-block;
+ }
+}
+
+.toolbar-button-with-badge {
+ display: inline-block;
+ position: relative;
+
+ .badge-round {
+ bottom: -5px;
+ font-size: 0.75rem;
+ line-height: 1.25rem;
+ min-width: 20px;
+ pointer-events: none;
+ position: absolute;
+ right: -5px;
+ }
+}
+
+.toolbox-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ margin: 0 auto;
+ max-width: 100%;
+ pointer-events: all;
+ border-radius: 6px;
+
+ .toolbox-content-items {
+ @include ltr;
+ }
+}
+
+.toolbox-content-wrapper::after {
+ content: '';
+ background: $newToolbarBackgroundColor;
+ padding-bottom: env(safe-area-inset-bottom, 0);
+}
+
+.overflow-menu-hr {
+ border-top: 1px solid #4C4D50;
+ border-bottom: 0;
+ margin: 8px 0;
+}
+
+div.hangup-button {
+ background-color: #CB2233;
+
+ @media (hover: hover) and (pointer: fine) {
+ &:hover {
+ background-color: #E04757;
+ }
+
+ &:active {
+ background-color: #A21B29;
+ }
+ }
+
+ svg {
+ fill: #fff;
+ }
+}
+
+div.hangup-menu-button {
+ background-color: #CB2233;
+
+ @media (hover: hover) and (pointer: fine) {
+ &:hover {
+ background-color: #E04757;
+ }
+
+ &:active {
+ background-color: #A21B29;
+ }
+ }
+
+ svg {
+ fill: #fff;
+ }
+}
+
+.profile-button-avatar {
+ align-items: center;
+}
+
+/**
+ * START of fade in animation for main toolbar
+ */
+.fadeIn {
+ opacity: 1;
+
+ @include transition(all .3s ease-in);
+}
+
+.fadeOut {
+ opacity: 0;
+
+ @include transition(all .3s ease-out);
+}
+
+/**
+ * Audio and video buttons do not have toggled state.
+ */
+.audio-preview,
+.video-preview {
+ .toolbox-icon.toggled {
+ background: none;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.2);
+ }
+ }
+
+}
+
+/**
+ * On small mobile devices make the toolbar full width and pad the invite prompt.
+ */
+.toolbox-content-mobile {
+ @media (max-width: 500px) {
+ margin-bottom: 0;
+
+ .toolbox-content-wrapper {
+ width: 100%;
+ }
+
+ .toolbox-content-items {
+ @include ltr;
+ border-radius: 0;
+ display: flex;
+ justify-content: space-evenly;
+ padding: 8px 0;
+ width: 100%;
+ }
+
+ .invite-more-container {
+ margin: 0 16px 8px;
+ }
+
+ .invite-more-container.elevated {
+ margin-bottom: 52px;
+ }
+ }
+}
diff --git a/css/_utils.scss b/css/_utils.scss
new file mode 100644
index 0000000..ce26463
--- /dev/null
+++ b/css/_utils.scss
@@ -0,0 +1,58 @@
+.flip-x {
+ transform: scaleX(-1);
+}
+
+.hidden {
+ display: none;
+}
+
+/**
+ * Hides an element.
+ */
+.hide {
+ display: none !important;
+}
+
+.invisible {
+ visibility: hidden;
+}
+
+/**
+ * Shows an element.
+ */
+.show {
+ display: block !important;
+}
+
+/**
+ * resets default button styles,
+ * mostly intended to be used on interactive elements that
+ * differ from their default styles (e.g. ) or have custom styles
+ */
+.invisible-button {
+ background: none;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ padding: 0;
+}
+
+
+/**
+ * style an element the same as an
+ * useful on some cases where we visually have a link but it's actually a
+ */
+.as-link {
+ @extend .invisible-button;
+
+ display: inline;
+ color: #44A5FF;
+ text-decoration: none;
+ font-weight: bold;
+
+ &:focus,
+ &:hover,
+ &:active {
+ text-decoration: underline;
+ }
+}
diff --git a/css/_variables.scss b/css/_variables.scss
new file mode 100644
index 0000000..f3f2bc5
--- /dev/null
+++ b/css/_variables.scss
@@ -0,0 +1,161 @@
+/**
+ * Style variables
+ */
+$baseFontFamily: -apple-system, BlinkMacSystemFont, 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+
+/**
+ * Size variables.
+ */
+
+// Video layout.
+$thumbnailsBorder: 2px;
+
+/**
+ * Toolbar
+ */
+$newToolbarBackgroundColor: #131519;
+$newToolbarSize: 48px;
+$newToolbarSizeMobile: 60px;
+$newToolbarSizeWithPadding: calc(#{$newToolbarSize} + 24px);
+
+/**
+ * Chat
+ */
+$chatBackgroundColor: #131519;
+
+/**
+ * Misc.
+ */
+$borderRadius: 4px;
+$scrollHeight: 7px;
+
+/**
+ * Z-indexes. TODO: Replace this by a function.
+ */
+$zindex0: 0;
+$zindex1: 1;
+$zindex2: 2;
+$zindex3: 3;
+$toolbarZ: 250;
+$watermarkZ: 253;
+
+// Place filmstrip videos over toolbar in order
+// to make connection info visible.
+$filmstripVideosZ: $toolbarZ + 1;
+
+/**
+ * Unsupported browser
+ */
+$primaryUnsupportedBrowserButtonBgColor: #0052CC;
+$unsupportedBrowserButtonBgColor: rgba(9, 30, 66, 0.04);
+$unsupportedBrowserTextColor: #4a4a4a;
+$unsupportedBrowserTextSmallFontSize: 1rem;
+$unsupportedBrowserTitleColor: #fff;
+$unsupportedBrowserTitleFontSize: 1.5rem;
+$unsupportedDesktopBrowserTextColor: rgba(255, 255, 255, 0.7);
+$unsupportedDesktopBrowserTextFontSize: 1.25rem;
+
+/**
+ * The size of the default watermark.
+ */
+$watermarkWidth: 71px;
+$watermarkHeight: 32px;
+
+$welcomePageWatermarkWidth: 71px;
+$welcomePageWatermarkHeight: 32px;
+
+/**
+ * Welcome page variables.
+ */
+$welcomePageDescriptionColor: #fff;
+$welcomePageFontFamily: inherit;
+$welcomePageBackground: none;
+$welcomePageTitleColor: #fff;
+
+$welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('../images/welcome-background.png');
+$welcomePageHeaderBackgroundPosition: center;
+$welcomePageHeaderBackgroundRepeat: none;
+$welcomePageHeaderBackgroundSize: cover;
+$welcomePageHeaderPadding: 1rem;
+$welcomePageHeaderTitleMaxWidth: initial;
+$welcomePageHeaderTextAlign: center;
+$welcomePageButtonBg: #0074E0;
+$welcomePageButtonHoverBg: #4687ED;
+$welcomePageButtonFocusOutline: #00225A;
+
+$welcomePageHeaderContainerMarginTop: 104px;
+$welcomePageHeaderContainerDisplay: flex;
+$welcomePageHeaderContainerMargin: $welcomePageHeaderContainerMarginTop auto 0;
+
+$welcomePageHeaderTextTitleMarginBottom: 0;
+$welcomePageHeaderTextTitleFontSize: 2.625rem;
+$welcomePageHeaderTextTitleFontWeight: normal;
+$welcomePageHeaderTextTitleLineHeight: 3.125rem;
+$welcomePageHeaderTextTitleOpacity: 1;
+
+$welcomePageEnterRoomDisplay: flex;
+$welcomePageEnterRoomWidth: calc(100% - 32px);
+$welcomePageEnterRoomPadding: 4px;
+$welcomePageEnterRoomMargin: 0 auto;
+
+$welcomePageTabContainerDisplay: flex;
+$welcomePageTabContentDisplay: inherit;
+$welcomePageTabButtonsDisplay: flex;
+$welcomePageTabDisplay: block;
+
+/**
+ * Deep-linking page variables.
+ */
+$deepLinkingMobileLogoHeight: 40px;
+
+$deepLinkingMobileHeaderBackground: #f1f2f5;
+
+$deepLinkingMobileLinkColor: inherit;
+$deepLinkingMobileTextFontSize: inherit;
+$deepLinkingMobileTextLineHeight: inherit;
+
+$deepLinkingDialInConferenceIdMargin: 10px 0 10px 0;
+$deepLinkingDialInConferenceIdPadding: inherit;
+$deepLinkingDialInConferenceIdBackgroundColor: inherit;
+$deepLinkingDialInConferenceIdBorderRadius: inherit;
+
+$deepLinkingDialInConferenceDescriptionFontSize: 0.8em;
+$deepLinkingDialInConferenceDescriptionLineHeight: inherit;
+$deepLinkingDialInConferenceDescriptionMarginBottom: none;
+
+$deepLinkingDialInConferencePinFontSize: inherit;
+$deepLinkingDialInConferencePinLineHeight: inherit;
+
+$depLinkingMobileHrefLineHeight: 2.2857142857142856em;
+$deepLinkingMobileHrefFontWeight: bolder;
+$deepLinkingMobileHrefFontSize: inherit;
+
+$deepLinkingMobileButtonHeight: 2.2857142857142856em;
+$deepLinkingMobileButtonLineHeight: 2.2857142857142856em;
+$deepLinkingMobileButtonMargin: 18px auto 10px;
+$deepLinkingMobileButtonWidth: auto;
+$deepLinkingMobileButtonFontWeight: bold;
+$deepLinkingMobileButtonFontSize: inherit;
+
+$primaryDeepLinkingMobileButtonBorderRadius: inherit;
+
+/**
+* Chrome extension banner variables.
+*/
+$chromeExtensionBannerDontShowAgainDisplay: flex;
+$chromeExtensionBannerHeight: 128px;
+$chromeExtensionBannerTop: 80px;
+$chromeExtensionBannerRight: 16px;
+$chromeExtensionBannerTopInMeeting: 10px;
+$chromeExtensionBannerRightInMeeeting: 10px;
+
+/**
+* media type thresholds
+*/
+$verySmallScreen: 500px;
+
+/**
+* Prejoin / premeeting screen
+*/
+
+$prejoinDefaultContentWidth: 336px;
diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss
new file mode 100644
index 0000000..1c72cbf
--- /dev/null
+++ b/css/_videolayout_default.scss
@@ -0,0 +1,355 @@
+#videoconference_page {
+ min-height: 100%;
+ position: relative;
+ transform: translate3d(0, 0, 0);
+ width: 100%;
+}
+
+#layout_wrapper {
+ @include ltr;
+ display: flex;
+ height: 100%;
+}
+
+#videospace {
+ display: block;
+ height: 100%;
+ width: 100%;
+ min-height: 100%;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ overflow: hidden;
+}
+
+#largeVideoBackgroundContainer,
+.large-video-background {
+ height: 100%;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ top: 0;
+ width: 100%;
+
+ #largeVideoBackground {
+ min-height: 100%;
+ min-width: 100%;
+ }
+}
+#largeVideoBackgroundContainer {
+ filter: blur(40px);
+}
+
+.videocontainer {
+ position: relative;
+ text-align: center;
+ overflow: 'hidden';
+}
+
+#localVideoWrapper {
+ display:inline-block;
+}
+
+.flipVideoX {
+ transform: scale(-1, 1);
+ -moz-transform: scale(-1, 1);
+ -webkit-transform: scale(-1, 1);
+ -o-transform: scale(-1, 1);
+}
+
+#localVideoWrapper video,
+#localVideoWrapper object {
+ border-radius: $borderRadius !important;
+ cursor: hand;
+ object-fit: cover;
+}
+
+#largeVideo,
+#largeVideoWrapper,
+#largeVideoContainer {
+ overflow: hidden;
+ text-align: center;
+
+ &.transition {
+ transition: width 1s, height 1s, top 1s;
+ }
+}
+
+.animatedFadeIn {
+ opacity: 0;
+ animation: fadeInAnimation 0.3s ease forwards;
+}
+
+@keyframes fadeInAnimation {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.animatedFadeOut {
+ opacity: 1;
+ animation: fadeOutAnimation 0.3s ease forwards;
+}
+
+@keyframes fadeOutAnimation {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+#largeVideoContainer {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0 !important;
+}
+
+#largeVideoWrapper {
+ box-shadow: 0 0 20px -2px #444;
+}
+
+#largeVideo,
+#largeVideoWrapper
+{
+ object-fit: cover;
+}
+
+#sharedVideo video {
+ width: 100%;
+ height: 100%;
+}
+
+#sharedVideo.disable-pointer {
+ pointer-events: none;
+}
+
+#sharedVideo,
+#etherpad,
+#localVideoWrapper video,
+#localVideoWrapper object,
+#localVideoWrapper,
+#largeVideoWrapper,
+#largeVideoWrapper>video,
+#largeVideoWrapper>object,
+.videocontainer>video,
+.videocontainer>object {
+ position: absolute;
+ left: 0;
+ top: 0;
+ z-index: $zindex1;
+ width: 100%;
+ height: 100%;
+}
+
+#etherpad {
+ text-align: center;
+}
+
+#etherpad {
+ z-index: $zindex0;
+}
+
+#alwaysOnTop .displayname {
+ font-size: 0.875rem;
+ position: inherit;
+ width: 100%;
+ left: 0px;
+ top: 0px;
+ margin-top: 10px;
+}
+
+/**
+ * Audio indicator on video thumbnails.
+ */
+.videocontainer>span.audioindicator,
+.videocontainer>.audioindicator-container {
+ position: absolute;
+ display: inline-block;
+ left: 6px;
+ top: 50%;
+ margin-top: -17px;
+ width: 6px;
+ height: 35px;
+ z-index: $zindex2;
+ border: none;
+
+ .audiodot-top,
+ .audiodot-bottom,
+ .audiodot-middle {
+ opacity: 0;
+ display: inline-block;
+ @include circle(5px);
+ background: rgba(9, 36, 77, 0.9);
+ margin: 1px 0 1px 0;
+ transition: opacity .25s ease-in-out;
+ -moz-transition: opacity .25s ease-in-out;
+ }
+
+ span.audiodot-top::after,
+ span.audiodot-bottom::after,
+ span.audiodot-middle::after {
+ content: "";
+ display: inline-block;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ -webkit-filter: blur(0.5px);
+ filter: blur(0.5px);
+ background: #44A5FF;
+ }
+}
+
+#dominantSpeaker {
+ visibility: hidden;
+ width: 300px;
+ height: 300px;
+ margin: auto;
+ position: relative;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+#dominantSpeakerAvatarContainer,
+.dynamic-shadow {
+ width: 200px;
+ height: 200px;
+}
+
+#dominantSpeakerAvatarContainer {
+ top: 50px;
+ margin: auto;
+ position: relative;
+ overflow: hidden;
+ visibility: inherit;
+}
+
+.dynamic-shadow {
+ border-radius: 50%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin: -100px 0 0 -100px;
+ transition: box-shadow 0.3s ease;
+}
+
+.avatar-container {
+ @include maxSize(60px);
+ @include absoluteAligning();
+ display: flex;
+ justify-content: center;
+ height: 50%;
+ width: auto;
+ overflow: hidden;
+
+ .userAvatar {
+ height: 100%;
+ object-fit: cover;
+ width: 100%;
+ top: 0px;
+ left: 0px;
+ position: absolute;
+ }
+}
+
+#videoNotAvailableScreen {
+ text-align: center;
+ #avatarContainer {
+ border-radius: 50%;
+ display: inline-block;
+ height: 50dvh;
+ margin-top: 25dvh;
+ overflow: hidden;
+ width: 50dvh;
+
+ #avatar {
+ height: 100%;
+ object-fit: cover;
+ width: 100%;
+ }
+ }
+}
+
+.sharedVideoAvatar {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ height: 100%;
+ width: 100%;
+ object-fit: cover;
+}
+
+#remotePresenceMessage,
+#remoteConnectionMessage {
+ position: absolute;
+ width: auto;
+ z-index: $zindex2;
+ font-weight: 600;
+ font-size: 0.875rem;
+ text-align: center;
+ color: #FFF;
+ left: 50%;
+ transform: translate(-50%, 0);
+}
+#remotePresenceMessage .presence-label,
+#remoteConnectionMessage {
+ opacity: .80;
+ text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
+ 0px 1px 1px rgba(0,0,0,0.3),
+ 1px 0px 1px rgba(0,0,0,0.3),
+ 0px 0px 1px rgba(0,0,0,0.3);
+
+ background: rgba(0,0,0,.5);
+ border-radius: 5px;
+ padding: 5px;
+ padding-left: 10px;
+ padding-right: 10px;
+}
+#remoteConnectionMessage {
+ display: none;
+}
+
+.display-video {
+ .avatar-container {
+ visibility: hidden;
+ }
+
+ video {
+ visibility: visible;
+ }
+}
+
+.display-avatar-only {
+ .avatar-container {
+ visibility: visible;
+ }
+
+ video {
+ visibility: hidden;
+ }
+}
+
+.presence-label {
+ color: #fff;
+ font-size: 0.75rem;
+ font-weight: 100;
+ left: 0;
+ margin: 0 auto;
+ overflow: hidden;
+ pointer-events: none;
+ right: 0;
+ text-align: center;
+ text-overflow: ellipsis;
+ top: calc(50% + 30px);
+ white-space: nowrap;
+ width: 100%;
+}
diff --git a/css/_welcome_page.scss b/css/_welcome_page.scss
new file mode 100644
index 0000000..e6188e0
--- /dev/null
+++ b/css/_welcome_page.scss
@@ -0,0 +1,354 @@
+body.welcome-page {
+ background: inherit;
+ overflow: auto;
+}
+
+.welcome {
+ background-image: $welcomePageBackground;
+ background-color: #fff;
+ display: flex;
+ flex-direction: column;
+ font-family: $welcomePageFontFamily;
+ justify-content: space-between;
+ min-height: 100dvh;
+ position: relative;
+
+ .header {
+ background-image: $welcomePageHeaderBackground;
+ background-position: $welcomePageHeaderBackgroundPosition;
+ background-repeat: $welcomePageHeaderBackgroundRepeat;
+ background-size: $welcomePageHeaderBackgroundSize;
+ padding: $welcomePageHeaderPadding;
+ background-color: #131519;
+ overflow: hidden;
+ position: relative;
+
+ .header-container {
+ display: $welcomePageHeaderContainerDisplay;
+ flex-direction: column;
+ margin: $welcomePageHeaderContainerMargin;
+ z-index: $zindex2;
+ align-items: center;
+ position: relative;
+ max-width: 688px;
+ }
+
+ .header-watermark-container {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ margin-top: calc(20px - #{$welcomePageHeaderContainerMarginTop});
+ }
+
+ .header-text-title {
+ color: $welcomePageTitleColor;
+ font-size: $welcomePageHeaderTextTitleFontSize;
+ font-weight: $welcomePageHeaderTextTitleFontWeight;
+ line-height: $welcomePageHeaderTextTitleLineHeight;
+ margin-bottom: $welcomePageHeaderTextTitleMarginBottom;
+ max-width: $welcomePageHeaderTitleMaxWidth;
+ opacity: $welcomePageHeaderTextTitleOpacity;
+ text-align: $welcomePageHeaderTextAlign;
+ }
+
+ .header-text-subtitle {
+ color: #fff;
+ font-size: 1.25rem;
+ font-weight: 600;
+ line-height: 1.625rem;
+ margin: 16px 0 32px 0;
+ text-align: $welcomePageHeaderTextAlign;
+
+ }
+
+ .not-allow-title-character-div {
+ color: #f03e3e;
+ background-color: #fff;
+ font-size: 0.75rem;
+ font-weight: 600;
+ margin: 10px 0px 5px 0px;
+ text-align: $welcomePageHeaderTextAlign;
+ border-radius: 5px;
+ padding: 5px;
+ .not-allow-title-character-text {
+ float: right;
+ line-height: 1.9;
+ };
+ .jitsi-icon {
+ margin-right: 9px;
+ float: left;
+
+
+ svg {
+ fill:#f03e3e;
+
+ & > *:first-child {
+ fill: none !important;
+ }
+ }
+ }
+
+ }
+
+ .insecure-room-name-warning {
+ align-items: center;
+ color: rgb(215, 121, 118);
+ font-weight: 600;
+ display: flex;
+ flex-direction: row;
+ margin-top: 15px;
+ max-width: 480px;
+ width: $welcomePageEnterRoomWidth;
+
+ .jitsi-icon {
+ margin-right: 15px;
+
+ svg {
+ fill: rgb(215, 121, 118);
+
+ & > *:first-child {
+ fill: none !important;
+ }
+ }
+ }
+ }
+
+ ::placeholder {
+ color: #253858;
+ }
+
+ #enter_room {
+ display: $welcomePageEnterRoomDisplay;
+ align-items: center;
+ max-width: 480px;
+ width: $welcomePageEnterRoomWidth;
+ z-index: $zindex2;
+ height: fit-content;
+
+ .join-meeting-container {
+ margin: $welcomePageEnterRoomMargin;
+ padding: $welcomePageEnterRoomPadding;
+ border-radius: 4px;
+ background-color: #fff;
+ display: flex;
+ width: 100%;
+ text-align: left;
+ color: #253858;
+ }
+
+ .enter-room-input-container {
+ flex-grow: 1;
+ padding-right: 4px;
+
+ .enter-room-input {
+ border-radius: 4px;
+ border: 0;
+ background: #fff;
+ display: inline-block;
+ height: 50px;
+ width: 100%;
+ font-size: 0.875rem;
+ padding-left: 10px;
+
+ &.focus-visible {
+ outline: auto 2px #005fcc;
+ }
+ }
+ }
+
+ }
+
+ #moderated-meetings {
+ max-width: calc(100% - 40px);
+ padding: 16px 0 0;
+ width: $welcomePageEnterRoomWidth;
+ text-align: center;
+
+ a {
+ color: inherit;
+ font-weight: 600;
+ }
+ }
+ }
+
+ .tab-container {
+ font-size: 1rem;
+ position: relative;
+ text-align: left;
+ display: $welcomePageTabContainerDisplay;
+ flex-direction: column;
+
+ .tab-content{
+ display: $welcomePageTabContentDisplay;
+ height: 250px;
+ margin: 5px 0px;
+ overflow: hidden;
+ flex-grow: 1;
+ position: relative;
+ }
+
+ .tab-buttons {
+ background-color: #c7ddff;
+ border-radius: 6px;
+ color: #0163FF;
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ margin: 4px;
+ display: $welcomePageTabButtonsDisplay;
+
+ [role="tab"] {
+ background-color: #c7ddff;
+ border-radius: 7px;
+ cursor: pointer;
+ display: $welcomePageTabDisplay;
+ flex-grow: 1;
+ margin: 2px;
+ padding: 7px 0;
+ text-align: center;
+ color: inherit;
+ border: 0;
+
+ &[aria-selected="true"] {
+ background-color: #FFF;
+ }
+ }
+
+ }
+ }
+
+ .welcome-page-button {
+ border: 0;
+ font-size: 0.875rem;
+ background: $welcomePageButtonBg;
+ border-radius: 3px;
+ color: #FFFFFF;
+ cursor: pointer;
+ padding: 16px 20px;
+ transition: all 0.2s;
+ &:focus-within {
+ outline: auto 2px $welcomePageButtonFocusOutline;
+ }
+
+ &:hover {
+ background-color: $welcomePageButtonHoverBg;
+ }
+ }
+
+ .welcome-page-settings {
+ background: rgba(255, 255, 255, 0.38);
+ border-radius: 3px;
+ color: $welcomePageDescriptionColor;
+ padding: 4px;
+ position: absolute;
+ top: calc(35px - #{$welcomePageHeaderContainerMarginTop});
+ right: 0;
+ z-index: $zindex2;
+
+ * {
+ cursor: pointer;
+ font-size: 2rem;
+ }
+
+ .toolbox-icon {
+ height: 24px;
+ width: 24px;
+ }
+ }
+
+ .welcome-watermark {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+
+ .watermark.leftwatermark {
+ width: $welcomePageWatermarkWidth;
+ height: $welcomePageWatermarkHeight;
+ }
+ }
+
+ &.without-content {
+ .welcome-card {
+ max-width: 100dvw;
+ }
+ }
+
+ &.without-footer {
+ justify-content: start;
+ }
+
+ .welcome-cards-container {
+ color:#131519;
+ padding-top: 40px;
+ }
+
+ .welcome-card-column {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ max-width: 688px;
+ margin: auto;
+
+ > div {
+ margin-bottom: 16px;
+ }
+ }
+
+ .welcome-card-text {
+ padding: 32px;
+ }
+
+ .welcome-card {
+ width: 100%;
+ border-radius: 8px;
+
+ &--dark {
+ background: #444447;
+ color: #fff;
+ }
+
+ &--blue {
+ background: #D5E5FF;
+ }
+
+ &--grey {
+ background: #F2F3F4;
+ }
+ }
+
+ .welcome-footer {
+ background: #131519;
+ color: #fff;
+ margin-top: 40px;
+ position: relative;
+ }
+
+ .welcome-footer-centered {
+ max-width: 688px;
+ margin: 0 auto;
+ }
+
+ .welcome-footer-padded {
+ padding: 0px 16px;
+ }
+
+ .welcome-footer-row-block {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #424447;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ .welcome-footer--row-1 {
+ padding: 40px 0 24px 0;
+ }
+
+ .welcome-footer-row-1-text {
+ max-width: 200px;
+ text-align: center;
+ }
+}
diff --git a/css/_welcome_page_content.scss b/css/_welcome_page_content.scss
new file mode 100644
index 0000000..985b19a
--- /dev/null
+++ b/css/_welcome_page_content.scss
@@ -0,0 +1 @@
+/** Insert custom CSS for any additional content in the welcome page **/
diff --git a/css/_welcome_page_settings_toolbar.scss b/css/_welcome_page_settings_toolbar.scss
new file mode 100644
index 0000000..c0e8925
--- /dev/null
+++ b/css/_welcome_page_settings_toolbar.scss
@@ -0,0 +1 @@
+/** Insert custom CSS for any additional content in the welcome page settings toolbar **/
diff --git a/css/components/_input-slider.scss b/css/components/_input-slider.scss
new file mode 100644
index 0000000..689f108
--- /dev/null
+++ b/css/components/_input-slider.scss
@@ -0,0 +1,43 @@
+$rangeInputThumbSize: 14;
+
+/**
+ * Disable the default webkit styles for range inputs (sliders).
+ */
+input[type=range]{
+ -webkit-appearance: none;
+ background: none;
+}
+
+/**
+ * Show focus for keyboard accessibility.
+ */
+input[type=range]:focus {
+ outline: 1px solid white !important;
+}
+
+/**
+ * Include the mixin for a range input style.
+ */
+@include slider {
+ background: #474747;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
+ height: 6px;
+ width: 100%;
+}
+
+/**
+ * Include the mixin for a range input thumb style.
+ */
+@include slider-thumb {
+ -webkit-appearance: none;
+ background: white;
+ border: 1px solid #3572b0;
+ border-radius: 50%;
+ box-shadow: 0px 0px 1px #3572b0;
+ cursor: pointer;
+ height: 14px;
+ margin-top: -4px;
+ width: 14px;
+}
diff --git a/css/deep-linking/_desktop.scss b/css/deep-linking/_desktop.scss
new file mode 100644
index 0000000..aa3d719
--- /dev/null
+++ b/css/deep-linking/_desktop.scss
@@ -0,0 +1,81 @@
+.deep-linking-desktop {
+ background-color: #fff;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-flow: column;
+ .header {
+ width: 100%;
+ height: 55px;
+ background-color: #f1f2f5;
+ padding-top: 15px;
+ padding-left: 50px;
+ display: flex;
+ flex-flow: row;
+ flex: 0 0 55px;
+ .logo {
+ height: 40px;
+ }
+ }
+ .content {
+ padding-top: 40px;
+ padding-bottom: 40px;
+ left: 0px;
+ right: 0px;
+ display: flex;
+ width: 100%;
+ height: 100%;
+ flex-flow: row;
+ .leftColumn {
+ left: 0px;
+ width: 50%;
+ min-height: 156px;
+ display: flex;
+ flex-flow: column;
+ .leftColumnContent{
+ padding: 20px;
+ display: flex;
+ flex-flow: column;
+ height: 100%;
+ .image {
+ background-image: url('../images/deep-linking-image.png');
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ height: 100%;
+ width: 100%;
+ }
+ }
+
+ }
+ .rightColumn {
+ top: 0px;
+ width: 50%;
+ min-height: 156px;
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ .rightColumnContent {
+ display: flex;
+ flex-flow: column;
+ padding: 20px 20px 20px 60px;
+ .title {
+ color: #1c2946;
+ }
+ .description {
+ color: #606a80;
+ margin-top: 8px;
+ }
+ .buttons {
+ margin-top: 16px;
+ display: flex;
+ align-items: center;
+
+ &>button:first-child {
+ margin-right: 8px;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/css/deep-linking/_main.scss b/css/deep-linking/_main.scss
new file mode 100644
index 0000000..262088e
--- /dev/null
+++ b/css/deep-linking/_main.scss
@@ -0,0 +1,3 @@
+@import 'desktop';
+@import 'mobile';
+@import 'no-mobile-app';
diff --git a/css/deep-linking/_mobile.scss b/css/deep-linking/_mobile.scss
new file mode 100644
index 0000000..52e8e41
--- /dev/null
+++ b/css/deep-linking/_mobile.scss
@@ -0,0 +1,160 @@
+.deep-linking-mobile {
+ background-color: #fff;
+ height: 100dvh;
+ overflow: auto;
+ position: relative;
+ width: 100vw;
+
+ .header {
+ width: 100%;
+ height: 70px;
+ background-color: $deepLinkingMobileHeaderBackground;
+ text-align: center;
+ .logo {
+ margin-top: 15px;
+ margin-left: auto;
+ margin-right: auto;
+ height: $deepLinkingMobileLogoHeight;
+ }
+ }
+
+ a {
+ text-decoration: none;
+ color: $deepLinkingMobileLinkColor;
+ }
+
+ &__body {
+ color: $unsupportedBrowserTextColor;
+ margin: auto;
+ max-width: 40em;
+ padding: 35px 0 40px 0;
+ text-align: center;
+ width: 90%;
+
+ a:active {
+ text-decoration: none;
+ }
+
+ .image {
+ max-width: 80%;
+ }
+ }
+
+ &__text {
+ font-weight: bolder;
+ font-size: $deepLinkingMobileTextFontSize;
+ line-height: $deepLinkingMobileTextLineHeight;
+ padding: 10px 10px 0px 10px;
+ }
+
+ &__text,
+ .deep-linking-dial-in {
+ font-size: 1em;
+ line-height: em(29px, 21px);
+ margin-bottom: 0.65em;
+
+ &_small {
+ font-size: 1.5em;
+ margin-bottom: 1em;
+ margin-top: em(21, 18);
+
+ strong {
+ font-size: em(21, 18);
+ }
+ }
+
+ table {
+ font-size: 1em;
+ }
+
+
+ .dial-in-conference-id {
+ text-align: center;
+ min-width: 200px;
+ margin-top: 40px;
+ }
+
+ .dial-in-conference-id {
+ margin: $deepLinkingDialInConferenceIdMargin;
+ padding: $deepLinkingDialInConferenceIdPadding;
+ background-color: $deepLinkingDialInConferenceIdBackgroundColor;
+ border-radius: $deepLinkingDialInConferenceIdBorderRadius;
+ }
+
+ .dial-in-conference-description {
+ font-size: $deepLinkingDialInConferenceDescriptionFontSize;
+ line-height: $deepLinkingDialInConferenceDescriptionLineHeight;
+ margin-bottom: $deepLinkingDialInConferenceDescriptionMarginBottom;
+ }
+
+ .toll-free-list {
+ min-width: 80px;
+ }
+
+ .numbers-list {
+ min-width: 150px;
+ }
+
+ li.toll-free:empty:before {
+ content: '.';
+ visibility: hidden;
+ }
+ }
+
+ &__href {
+ height: 2.2857142857142856em;
+ line-height: $depLinkingMobileHrefLineHeight;
+ margin: 18px auto 20px;
+ max-width: 300px;
+ width: auto;
+ font-weight: $deepLinkingMobileHrefFontWeight;
+ font-size: $deepLinkingMobileHrefFontSize;
+ }
+
+ &__button {
+ border: 0;
+ height: $deepLinkingMobileButtonHeight;
+ line-height: $deepLinkingMobileButtonLineHeight;
+ margin: $deepLinkingMobileButtonMargin;
+ padding: 0px 10px 0px 10px;
+ max-width: 300px;
+ width: $deepLinkingMobileButtonWidth;
+ @include border-radius(3px);
+ background-color: $unsupportedBrowserButtonBgColor;
+ color: #505F79;
+ font-weight: $deepLinkingMobileButtonFontWeight;
+ font-size: $deepLinkingMobileButtonFontSize;
+
+ &:active {
+ background-color: $unsupportedBrowserButtonBgColor;
+ }
+
+ &_primary {
+ background-color: $primaryUnsupportedBrowserButtonBgColor;
+ color: #FFFFFF;
+ border-radius: $primaryDeepLinkingMobileButtonBorderRadius;
+ &:active {
+ background-color: $primaryUnsupportedBrowserButtonBgColor;
+ }
+ }
+ }
+
+ .deep-linking-dial-in {
+ display: none;
+
+ &.has-numbers {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .dial-in-numbers-list {
+ color: $unsupportedBrowserTextColor;
+ padding-left: 20px;
+ }
+
+ .dial-in-numbers-body {
+ vertical-align: top;
+ }
+ }
+}
diff --git a/css/deep-linking/_no-mobile-app.scss b/css/deep-linking/_no-mobile-app.scss
new file mode 100644
index 0000000..128ca3c
--- /dev/null
+++ b/css/deep-linking/_no-mobile-app.scss
@@ -0,0 +1,21 @@
+.no-mobile-app {
+ margin: 30% auto 0;
+ max-width: 25em;
+ text-align: center;
+ width: auto;
+
+ &__title {
+ border-bottom: 1px solid lighten(#FFFFFF, 10%);
+ color: $unsupportedBrowserTitleColor;
+ font-weight: 400;
+ letter-spacing: 0.5px;
+ padding-bottom: em(17, 24);
+ }
+
+ &__description {
+ font-size: $unsupportedBrowserTextSmallFontSize;
+ font-weight: 300;
+ letter-spacing: 1px;
+ margin-top: 1em;
+ }
+}
diff --git a/css/filmstrip/_horizontal_filmstrip.scss b/css/filmstrip/_horizontal_filmstrip.scss
new file mode 100644
index 0000000..9e7c1fc
--- /dev/null
+++ b/css/filmstrip/_horizontal_filmstrip.scss
@@ -0,0 +1,89 @@
+%align-right {
+ @include flex();
+ flex-direction: row-reverse;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+}
+
+.horizontal-filmstrip .filmstrip {
+ padding: 10px 5px;
+ @extend %align-right;
+ z-index: $filmstripVideosZ;
+ box-sizing: border-box;
+ width: 100%;
+ position: fixed;
+
+ /*
+ * Firefox sets flex items to min-height: auto and min-width: auto,
+ * preventing flex children from shrinking like they do on other browsers.
+ * Setting min-height and min-width 0 is a workaround for the issue so
+ * Firefox behaves like other browsers.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1043520
+ */
+ @mixin minHWAutoFix() {
+ min-height: 0;
+ min-width: 0;
+ }
+
+ &.reduce-height {
+ bottom: calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight});
+ }
+
+ &__videos {
+ position:relative;
+ padding: 0;
+ /* The filmstrip should not be covered by the left toolbar. */
+ bottom: 0;
+ width:auto;
+
+ remoteVideos {
+ border: $thumbnailsBorder solid transparent;
+ transition: bottom 2s;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: row-reverse;
+ @include minHWAutoFix()
+ }
+
+ /**
+ * The local video identifier.
+ */
+ filmstripLocalVideo,
+ filmstripLocalScreenShare {
+ align-self: flex-end;
+ display: block;
+ margin-bottom: 8px;
+ }
+
+ &.hidden {
+ bottom: calc(-196px - #{$newToolbarSizeWithPadding} + 50px);
+ }
+ }
+
+ .remote-videos {
+ overscroll-behavior: contain;
+
+ & > div {
+ transition: opacity 1s;
+ position: absolute;
+ }
+
+ &.is-not-overflowing > div {
+ right: 2px;
+ }
+ }
+
+ &.hide-videos {
+ .remote-videos {
+ & > div {
+ opacity: 0;
+ pointer-events: none;
+ }
+ }
+ }
+
+ .videocontainer {
+ margin-bottom: 10px;
+ }
+}
+
diff --git a/css/filmstrip/_small_video.scss b/css/filmstrip/_small_video.scss
new file mode 100644
index 0000000..4671a06
--- /dev/null
+++ b/css/filmstrip/_small_video.scss
@@ -0,0 +1,24 @@
+.filmstrip__videos .videocontainer {
+ display: inline-block;
+ position: relative;
+ background-size: contain;
+ border: 2px solid transparent;
+ border-radius: $borderRadius;
+ margin: 0 2px;
+
+ &:hover {
+ cursor: hand;
+ }
+
+ & > video {
+ cursor: hand;
+ border-radius: $borderRadius;
+ object-fit: cover;
+ overflow: hidden;
+ }
+
+ .presence-label {
+ position: absolute;
+ z-index: $zindex3;
+ }
+}
diff --git a/css/filmstrip/_tile_view.scss b/css/filmstrip/_tile_view.scss
new file mode 100644
index 0000000..d461737
--- /dev/null
+++ b/css/filmstrip/_tile_view.scss
@@ -0,0 +1,93 @@
+/**
+ * CSS styles that are specific to the filmstrip that shows the thumbnail tiles.
+ */
+.tile-view {
+ .remote-videos {
+ align-items: center;
+ box-sizing: border-box;
+ overscroll-behavior: contain;
+ }
+
+ .filmstrip__videos .videocontainer {
+ &:not(.active-speaker),
+ &:hover:not(.active-speaker) {
+ border: none;
+ box-shadow: none;
+ }
+ }
+
+ #remoteVideos {
+ /**
+ * Height is modified with an inline style in horizontal filmstrip mode
+ * so !important is used to override that.
+ */
+ height: 100% !important;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: margin-bottom .3s ease-in;
+ }
+
+ .filmstrip {
+ align-items: center;
+ display: flex;
+ height: 100%;
+ justify-content: center;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+
+ &.collapse {
+ #remoteVideos {
+ height: calc(100% - #{$newToolbarSizeMobile}) !important;
+ margin-bottom: $newToolbarSizeMobile;
+ }
+
+ .remote-videos {
+ // !important is needed here as overflow is set via element.style in a FixedSizeGrid.
+ overflow: hidden auto !important;
+ }
+ }
+ }
+
+ /**
+ * Regardless of the user setting, do not let the filmstrip be in a hidden
+ * state.
+ */
+ .filmstrip__videos.hidden {
+ display: block;
+ }
+
+ .filmstrip__videos.has-scroll {
+ padding-left: 7px;
+ }
+
+ .remote-videos {
+ box-sizing: border-box;
+
+
+ /**
+ * The size of the thumbnails should be set with javascript, based on
+ * desired column count and window width. The rows are created using flex
+ * and allowing the thumbnails to wrap.
+ */
+ & > div {
+ align-content: center;
+ align-items: center;
+ box-sizing: border-box;
+ display: flex;
+ margin-top: auto;
+ margin-bottom: auto;
+ justify-content: center;
+
+ .videocontainer {
+ border: 0;
+ box-sizing: border-box;
+ display: block;
+ margin: 2px;
+ }
+ }
+ }
+}
diff --git a/css/filmstrip/_tile_view_overrides.scss b/css/filmstrip/_tile_view_overrides.scss
new file mode 100644
index 0000000..e24376b
--- /dev/null
+++ b/css/filmstrip/_tile_view_overrides.scss
@@ -0,0 +1,38 @@
+/**
+ * Various overrides outside of the filmstrip to style the app to support a
+ * tiled thumbnail experience.
+ */
+.tile-view,
+.whiteboard-container,
+.stage-filmstrip {
+ /**
+ * Let the avatar grow with the tile.
+ */
+ .avatar-container {
+ max-height: initial;
+ max-width: initial;
+ }
+
+ /**
+ * Hide various features that should not be displayed while in tile view.
+ */
+ #dominantSpeaker,
+ #largeVideoElementsContainer,
+ #sharedVideo {
+ display: none;
+ }
+
+ /**
+ * The follow styling uses !important to override inline styles set with
+ * javascript.
+ *
+ * TODO: These overrides should be more easy to remove and should be removed
+ * when the components are in react so their rendering done declaratively,
+ * making conditional styling easier to apply.
+ */
+ #largeVideoElementsContainer,
+ #remoteConnectionMessage,
+ #remotePresenceMessage {
+ display: none !important;
+ }
+}
diff --git a/css/filmstrip/_vertical_filmstrip.scss b/css/filmstrip/_vertical_filmstrip.scss
new file mode 100644
index 0000000..f63afc8
--- /dev/null
+++ b/css/filmstrip/_vertical_filmstrip.scss
@@ -0,0 +1,177 @@
+.vertical-filmstrip, .stage-filmstrip {
+ span:not(.tile-view) .filmstrip {
+ &.hide-videos {
+ .remote-videos {
+ & > div {
+ opacity: 0;
+ pointer-events: none;
+ }
+ }
+ }
+
+ /*
+ * Firefox sets flex items to min-height: auto and min-width: auto,
+ * preventing flex children from shrinking like they do on other browsers.
+ * Setting min-height and min-width 0 is a workaround for the issue so
+ * Firefox behaves like other browsers.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1043520
+ */
+ @mixin minHWAutoFix() {
+ min-height: 0;
+ min-width: 0;
+ }
+
+ @extend %align-right;
+ align-items: flex-end;
+ bottom: 0;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column-reverse;
+ height: 100%;
+ width: 100%;
+ padding: 0;
+ /**
+ * fixed positioning is necessary for remote menus and tooltips to pop
+ * out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
+ * a library called popper which will position its elements fixed if
+ * any parent is also fixed.
+ */
+ position: fixed;
+ top: 0;
+ right: 0;
+ z-index: $filmstripVideosZ;
+
+ &.no-vertical-padding {
+ padding: 0;
+ }
+
+ /**
+ * Hide videos by making them slight to the right.
+ */
+ .filmstrip__videos {
+ @extend %align-right;
+ bottom: 0;
+ padding: 0;
+ position:relative;
+ right: 0;
+ width: auto;
+
+ /**
+ * An id selector is used to match id specificity with existing
+ * filmstrip styles.
+ */
+ remoteVideos {
+ border: $thumbnailsBorder solid transparent;
+ padding-left: 0;
+ border-left: 0;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ }
+ }
+
+ /**
+ * Re-styles the local Video to better fit vertical filmstrip layout.
+ */
+ #filmstripLocalVideo {
+ align-self: initial;
+ margin-bottom: 5px;
+ display: flex;
+ flex-direction: column-reverse;
+ height: auto;
+ justify-content: flex-start;
+ width: 100%;
+
+ #filmstripLocalVideoThumbnail {
+ width: calc(100% - 15px);
+
+ .videocontainer {
+ height: 0px;
+ width: 100%;
+ }
+ }
+ }
+
+ #filmstripLocalScreenShare {
+ align-self: initial;
+ margin-bottom: 5px;
+ display: flex;
+ flex-direction: column-reverse;
+ height: auto;
+ justify-content: flex-start;
+ width: 100%;
+
+ #filmstripLocalScreenShareThumbnail {
+ width: calc(100% - 15px);
+
+ .videocontainer {
+ height: 0px;
+ width: 100%;
+ }
+ }
+ }
+
+ /**
+ * Remove unnecessary padding that is normally used to prevent horizontal
+ * filmstrip from overlapping the left edge of the screen.
+ */
+ #filmstripLocalVideo,
+ #filmstripLocalScreenShare,
+ .remote-videos {
+ padding: 0;
+ }
+
+ #remoteVideos {
+ @include minHWAutoFix();
+
+ flex-direction: column;
+ flex-grow: 1;
+ }
+
+ .resizable-filmstrip #remoteVideos .videocontainer {
+ border-left: 0;
+ margin: 0;
+ }
+
+ &.reduce-height {
+ height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
+ }
+
+ .filmstrip__videos.vertical-view-grid#remoteVideos {
+ align-items: 'center';
+ border: 0px;
+ padding-right: 7px;
+
+ &.has-scroll {
+ padding-right: 0px;
+ }
+
+ .remote-videos > div {
+ left: 0px; // fixes an issue on FF - the div is aligned to the right by default for some reason
+ }
+
+ .videocontainer {
+ border: 0px;
+ margin: 2px;
+ }
+ }
+
+ .remote-videos {
+ display: flex;
+ overscroll-behavior: contain;
+
+ &.height-transition {
+ transition: height .3s ease-in;
+ }
+
+ & > div {
+ position: absolute;
+ transition: opacity 1s;
+ }
+
+ &.is-not-overflowing > div {
+ bottom: 0px;
+ }
+ }
+ }
+}
diff --git a/css/filmstrip/_vertical_filmstrip_overrides.scss b/css/filmstrip/_vertical_filmstrip_overrides.scss
new file mode 100644
index 0000000..6dbf469
--- /dev/null
+++ b/css/filmstrip/_vertical_filmstrip_overrides.scss
@@ -0,0 +1,57 @@
+/**
+ * Overrides for video containers that should not be centered aligned to avoid=
+ * clashing with the filmstrip.
+ */
+.vertical-filmstrip #etherpad,
+.stage-filmstrip #etherpad,
+.vertical-filmstrip #sharedvideo,
+.stage-filmstrip #sharedvideo {
+ text-align: left;
+}
+
+/**
+ * Overrides for small videos in vertical filmstrip mode.
+ */
+.vertical-filmstrip .filmstrip__videos .videocontainer,
+.stage-filmstrip .filmstrip__videos .videocontainer {
+ .self-view-mobile-portrait video {
+ object-fit: contain;
+ }
+}
+
+/**
+ * Overrides for quality labels in filmstrip only mode. The styles adjust the
+ * labels' positioning as the filmstrip itself or filmstrip's remote videos
+ * appear and disappear.
+ *
+ * The class with-filmstrip is for when the filmstrip is visible.
+ * The class without-filmstrip is for when the filmstrip has been toggled to
+ * be hidden.
+ * The class opening is for when the filmstrip is transitioning from hidden
+ * to visible.
+ */
+.vertical-filmstrip .large-video-labels,
+.stage-filmstrip .large-video-labels {
+ &.with-filmstrip {
+ right: 150px;
+ }
+
+ &.with-filmstrip.opening {
+ transition: 0.9s;
+ transition-timing-function: ease-in-out;
+ }
+
+ &.without-filmstrip {
+ transition: 1.2s ease-in-out;
+ transition-delay: 0.1s;
+ }
+}
+
+/**
+ * Overrides for self view when in portrait mode on mobile.
+ * This is done in order to keep the aspect ratio.
+ */
+.vertical-filmstrip .self-view-mobile-portrait #localVideo_container,
+.stage-filmstrip .self-view-mobile-portrait #localVideo_container {
+ object-fit: contain;
+}
diff --git a/css/main.scss b/css/main.scss
new file mode 100644
index 0000000..dbc4365
--- /dev/null
+++ b/css/main.scss
@@ -0,0 +1,80 @@
+/* Functions BEGIN */
+
+@import 'functions';
+
+/* Functions END */
+
+/* Variables BEGIN */
+
+@import 'variables';
+
+/* Variables END */
+
+/* Mixins BEGIN */
+
+@import "mixins";
+
+/* Mixins END */
+
+/* Animations END */
+
+/* Flags BEGIN */
+$flagsImagePath: "../images/";
+@import "../node_modules/bc-css-flags/dist/css/bc-css-flags.scss";
+/* Flags END */
+
+/* Modules BEGIN */
+@import 'reset';
+@import 'base';
+@import 'utils';
+@import 'overlay/overlay';
+@import 'inlay';
+@import 'reload_overlay/reload_overlay';
+@import 'mini_toolbox';
+@import 'modals/desktop-picker/desktop-picker';
+@import 'modals/dialog';
+@import 'modals/invite/info';
+@import 'modals/screen-share/share-audio';
+@import 'modals/screen-share/share-screen-warning';
+@import 'modals/whiteboard';
+@import 'videolayout_default';
+@import 'subject';
+@import 'popup_menu';
+@import 'recording';
+@import 'login_menu';
+@import 'chat';
+@import 'ringing/ringing';
+@import 'welcome_page';
+@import 'welcome_page_content';
+@import 'welcome_page_settings_toolbar';
+@import 'toolbars';
+@import 'redirect_page';
+@import 'components/input-slider';
+@import '404';
+@import 'policy';
+@import 'popover';
+@import 'filmstrip/horizontal_filmstrip';
+@import 'filmstrip/small_video';
+@import 'filmstrip/tile_view';
+@import 'filmstrip/tile_view_overrides';
+@import 'filmstrip/vertical_filmstrip';
+@import 'filmstrip/vertical_filmstrip_overrides';
+@import 'unsupported-browser/main';
+@import 'deep-linking/main';
+@import '_meetings_list.scss';
+@import 'navigate_section_list';
+@import 'third-party-branding/google';
+@import 'third-party-branding/microsoft';
+@import 'promotional-footer';
+@import 'chrome-extension-banner';
+@import 'settings-button';
+@import 'meter';
+@import 'premeeting/main';
+@import 'modals/invite/invite_more';
+@import 'modals/security/security';
+@import 'responsive';
+@import 'participants-pane';
+@import 'reactions-menu';
+@import 'plan-limit';
+
+/* Modules END */
diff --git a/css/modals/_dialog.scss b/css/modals/_dialog.scss
new file mode 100644
index 0000000..4432272
--- /dev/null
+++ b/css/modals/_dialog.scss
@@ -0,0 +1,26 @@
+.modal-dialog-form {
+ margin-top: 5px !important;
+
+ .input-control {
+ background: #fafbfc;
+ border: 1px solid #f4f5f7;
+ color: inherit;
+ }
+
+ &-error {
+ margin-bottom: 8px;
+ }
+}
+
+/**
+ * Styling shared video dialog errors.
+ */
+.shared-video-dialog-error {
+ color: #E04757;
+ margin-top: 2px;
+ display: block;
+}
+
+.dialog-bottom-margin {
+ margin-bottom: 5px;
+}
diff --git a/css/modals/_whiteboard.scss b/css/modals/_whiteboard.scss
new file mode 100644
index 0000000..5a133e6
--- /dev/null
+++ b/css/modals/_whiteboard.scss
@@ -0,0 +1,7 @@
+.whiteboard {
+
+ .excalidraw-wrapper {
+ height: 100vh;
+ width: 100vw;
+ }
+}
diff --git a/css/modals/desktop-picker/_desktop-picker.scss b/css/modals/desktop-picker/_desktop-picker.scss
new file mode 100644
index 0000000..2ad2db4
--- /dev/null
+++ b/css/modals/desktop-picker/_desktop-picker.scss
@@ -0,0 +1,70 @@
+.desktop-picker-pane {
+ height: 320px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ width: 100%;
+
+ &.source-type-screen {
+ .desktop-picker-source {
+ margin-left: auto;
+ margin-right: auto;
+ width: 50%;
+ }
+
+ .desktop-source-preview-thumbnail {
+ width: 100%;
+ }
+
+ .desktop-source-preview-label {
+ display: none;
+ }
+ }
+
+ &.source-type-window {
+ .desktop-picker-source {
+ display: inline-block;
+ width: 30%;
+ }
+ }
+
+ &-spinner {
+ justify-content: center;
+ display: flex;
+ height: 100%;
+ align-items: center;
+ }
+}
+
+.desktop-picker-source {
+ margin-top: 10px;
+ text-align: center;
+
+ &.is-selected {
+ .desktop-source-preview-image-container {
+ background: rgba(255,255,255,0.3);
+ border-radius: $borderRadius;
+ }
+ }
+}
+
+.desktop-source-preview-label {
+ margin-top: 3px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.desktop-source-preview-thumbnail {
+ box-shadow: 5px 5px 5px grey;
+ height: auto;
+ max-width: 100%;
+}
+
+.desktop-source-preview-image-container {
+ padding: 10px;
+}
+
+.desktop-picker-tabs-container {
+ width: 65%;
+ margin-top: 3px;
+}
diff --git a/css/modals/invite/_add-people.scss b/css/modals/invite/_add-people.scss
new file mode 100644
index 0000000..be9c10e
--- /dev/null
+++ b/css/modals/invite/_add-people.scss
@@ -0,0 +1,43 @@
+/**
+ * Styles errors and links in the AddPeopleDialog.
+ */
+.modal-dialog-form {
+ .add-people-form-wrap {
+ margin-top: 8px;
+
+ .error {
+ padding-left: 5px;
+
+ a {
+ padding-left: 5px;
+ }
+ }
+
+ .add-telephone-icon {
+ display: flex;
+ height: 28px;
+ transform: scaleX(-1);
+ width: 28px;
+
+ i {
+ line-height: 1.75rem;
+ margin: auto;
+ }
+ }
+
+ .footer-text-wrap {
+ display: flex;
+ }
+
+ .footer-telephone-icon {
+ display: flex;
+ transform: scaleX(-1);
+ padding-left: 10px;
+
+ i {
+ line-height: 1.25rem;
+ margin: auto;
+ }
+ }
+ }
+}
diff --git a/css/modals/invite/_info.scss b/css/modals/invite/_info.scss
new file mode 100644
index 0000000..bcec7de
--- /dev/null
+++ b/css/modals/invite/_info.scss
@@ -0,0 +1,137 @@
+.info-dialog {
+ cursor: default;
+ display: flex;
+ font-size: 0.875rem;
+
+ .info-dialog-column {
+ margin-right: 10px;
+ overflow: hidden;
+
+ a,
+ a:active,
+ a:focus,
+ a:hover {
+ text-decoration: none;
+ }
+ }
+
+ .info-dialog-password,
+ .info-password,
+ .info-password-form {
+ align-items: baseline;
+ display: flex;
+ }
+
+ .info-label {
+ font-weight: bold;
+ }
+
+ .info-password-field {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-right: 5px;
+ }
+
+ .info-password-none,
+ .info-password-remote {
+ color: #fff;
+ }
+
+ .info-password-local {
+ user-select: text;
+ }
+}
+
+.dial-in-number {
+ display: flex;
+ justify-content: space-between;
+ padding-right: 8px;
+}
+
+.dial-in-numbers-list {
+ max-width: 334px;
+ width: 100%;
+ margin-top: 20px;
+ font-size: 0.75rem;
+ line-height: 1.5rem;
+ border-collapse: collapse;
+
+ * {
+ user-select: text;
+ }
+
+ thead {
+ text-align: left;
+ }
+
+ .flag-cell {
+ vertical-align: top;
+ width: 30px;
+ }
+ .flag {
+ display: block;
+ margin: 5px 5px 0px 5px;
+ }
+
+ .country {
+ font-weight: bold;
+ vertical-align: top;
+ padding: 0 20px 0 0;
+ }
+
+ ul {
+ padding: 0px 0px 0px 0px;
+ }
+
+ .numbers-list {
+ list-style: none;
+ padding: 0 20px 0 0;
+ }
+
+ .toll-free-list {
+ font-weight: bold;
+ list-style: none;
+ vertical-align: top;
+ text-align: right;
+ }
+
+ li.toll-free:empty:before {
+ content: '.';
+ visibility: hidden;
+ }
+}
+
+.dial-in-page {
+ align-items: center;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ font-size: 0.75rem;
+ max-height: 100%;
+ overflow: auto;
+ padding: 15pt;
+ position: absolute;
+ transform: translateY(-50%);
+ top: 50%;
+ width: 100%;
+
+ .dial-in-conference-id {
+ text-align: center;
+ min-width: 200px;
+ margin-top: 40px;
+ }
+
+ .dial-in-conference-description {
+ margin: 12px;
+ }
+}
+
+.info-dialog,
+.dial-in-page {
+ * {
+ user-select: text;
+ -moz-user-select: text;
+ -webkit-user-select: text;
+ }
+}
diff --git a/css/modals/invite/_invite_more.scss b/css/modals/invite/_invite_more.scss
new file mode 100644
index 0000000..dd85bd6
--- /dev/null
+++ b/css/modals/invite/_invite_more.scss
@@ -0,0 +1,54 @@
+.invite-more {
+ &-dialog {
+ color: #fff;
+ font-size: 0.875rem;
+ line-height: 1.5rem;
+
+ &.separator {
+ margin: 24px 0 24px -20px;
+ padding: 0 20px;
+ width: 100%;
+ height: 1px;
+ background: #5E6D7A;
+ }
+
+ &.stream {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 8px 8px 16px;
+ margin-top: 8px;
+ width: calc(100% - 26px);
+ height: 22px;
+
+ background: #2A3A4B;
+ border: 1px solid #5E6D7A;
+ border-radius: 3px;
+ cursor: pointer;
+
+ &:hover {
+ font-weight: 600;
+ }
+
+ &-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 292px;
+
+ &.selected {
+ font-weight: 600;
+ }
+ }
+
+ &.clicked {
+ background: #31B76A;
+ border: 1px solid #31B76A;
+ }
+
+ & > div > svg > path {
+ fill: #fff;
+ }
+ }
+ }
+}
diff --git a/css/modals/screen-share/_share-audio.scss b/css/modals/screen-share/_share-audio.scss
new file mode 100644
index 0000000..071e5c0
--- /dev/null
+++ b/css/modals/screen-share/_share-audio.scss
@@ -0,0 +1,20 @@
+.share-audio-dialog {
+ .share-audio-animation {
+ width: 100%;
+ height: 90%;
+ object-fit: contain;
+ margin-bottom: 10px;
+ }
+
+ .separator-line {
+ margin: 24px 0 24px -20px;
+ padding: 0 20px;
+ width: 100%;
+ height: 1px;
+ background: #5E6D7A;
+
+ &:last-child {
+ display: none;
+ }
+ }
+}
\ No newline at end of file
diff --git a/css/modals/screen-share/_share-screen-warning.scss b/css/modals/screen-share/_share-screen-warning.scss
new file mode 100644
index 0000000..277432f
--- /dev/null
+++ b/css/modals/screen-share/_share-screen-warning.scss
@@ -0,0 +1,23 @@
+.share-screen-warn-dialog {
+ font-size: 0.875rem;
+
+ .separator-line {
+ margin: 24px 0 24px -20px;
+ padding: 0 20px;
+ width: 100%;
+ height: 1px;
+ background: #5E6D7A;
+
+ &:last-child {
+ display: none;
+ }
+ }
+
+ .header {
+ font-weight: 600;
+ }
+
+ .description {
+ margin-top: 16px;
+ }
+}
\ No newline at end of file
diff --git a/css/modals/security/_security.scss b/css/modals/security/_security.scss
new file mode 100644
index 0000000..78f6791
--- /dev/null
+++ b/css/modals/security/_security.scss
@@ -0,0 +1,58 @@
+.security {
+ &-dialog {
+ color: #fff;
+ font-size: 0.875rem;
+ line-height: 1.5rem;
+
+ &.password-section {
+ display: flex;
+ flex-direction: column;
+
+ .description {
+ font-size: 0.75rem;
+ }
+
+ .password {
+ align-items: flex-start;
+ display: flex;
+ justify-content: flex-start;
+ margin-top: 15px;
+ flex-direction: column;
+
+ &-actions {
+ margin-top: 10px;
+ button {
+ cursor: pointer;
+ text-decoration: none;
+ font-size: 0.875rem;
+ color: #6FB1EA;
+ }
+
+ & > :not(:last-child) {
+ margin-right: 24px;
+ }
+ }
+ }
+ }
+
+ .separator-line {
+ margin: 24px 0 24px -20px;
+ padding: 0 20px;
+ width: 100%;
+ height: 1px;
+ background: #5E6D7A;
+
+ &:last-child {
+ display: none;
+ }
+ }
+ }
+}
+
+.new-toolbox .toolbox-content .toolbox-icon.toggled.security-toolbar-button {
+ border-width: 0;
+
+ &:not(:hover) {
+ background: unset;
+ }
+}
\ No newline at end of file
diff --git a/css/overlay/_overlay.scss b/css/overlay/_overlay.scss
new file mode 100644
index 0000000..67eae8f
--- /dev/null
+++ b/css/overlay/_overlay.scss
@@ -0,0 +1,44 @@
+.overlay {
+ &__container,
+ &__container-light {
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ z-index: 1016;
+ background: #474747;
+ }
+
+ &__container-light {
+ @include transparentBg(#474747, 0.7);
+ }
+
+ &__content {
+ position: absolute;
+ margin: 0 auto;
+ height: 100%;
+ width: 56%;
+ left: 50%;
+ @include transform(translateX(-50%));
+
+ &_bottom {
+ position: absolute;
+ bottom: 0;
+ }
+ }
+
+ &__policy {
+ position: absolute;
+ bottom: 24px;
+ width: 100%;
+ }
+
+ &__spinner-container {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ align-items: center;
+ }
+}
diff --git a/css/premeeting/_lobby.scss b/css/premeeting/_lobby.scss
new file mode 100644
index 0000000..bfaf9b6
--- /dev/null
+++ b/css/premeeting/_lobby.scss
@@ -0,0 +1,233 @@
+.lobby-screen {
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.625rem;
+
+ &-content {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+
+ .spinner {
+ margin: 8px;
+ }
+
+ .lobby-chat-container {
+ background-color: $chatBackgroundColor;
+ width: 100%;
+ height: 314px;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ margin-bottom: 16px;
+ border-radius: 5px;
+ .lobby-chat-header {
+ display: none;
+ }
+ }
+
+ .joining-message {
+ color: white;
+ margin: 24px auto;
+ text-align: center;
+ }
+
+ .open-chat-button {
+ display: none;
+ }
+ }
+}
+
+#lobby-section {
+ display: flex;
+ flex-direction: column;
+
+ .description {
+ font-size: 0.75rem;
+ }
+
+ .control-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-top: 15px;
+
+ label {
+ font-size: 0.875rem;
+ font-weight: bold;
+ }
+ }
+}
+
+#notification-participant-list {
+ background-color: $newToolbarBackgroundColor;
+ border: 1px solid rgba(255, 255, 255, .4);
+ border-radius: 8px;
+ left: 0;
+ margin: 20px;
+ max-height: 600px;
+ overflow: hidden;
+ overflow-y: auto;
+ position: fixed;
+ top: 30px;
+ z-index: $toolbarZ + 1;
+
+ &:empty {
+ border: none;
+ }
+
+ &.toolbox-visible {
+ // Same as toolbox subject position
+ top: 120px;
+ }
+
+ &.avoid-chat {
+ left: 315px;
+ }
+
+ .title {
+ background-color: rgba(0, 0, 0, .2);
+ font-size: 1.2em;
+ padding: 15px
+ }
+
+ button {
+ align-self: stretch;
+ margin-bottom: 8px 0;
+ padding: 12px;
+ transition: .2s transform ease;
+
+ &:disabled {
+ opacity: .5;
+ }
+
+ &:hover {
+ transform: scale(1.05);
+
+ &:disabled {
+ transform: none;
+ }
+ }
+
+ &.borderLess {
+ background-color: transparent;
+ border-width: 0;
+ }
+
+ &.primary {
+ background-color: rgb(3, 118, 218);
+ border-width: 0;
+ }
+ }
+}
+
+.knocking-participants-container {
+ list-style-type: none;
+ padding: 0 15px 15px 15px;
+}
+
+.knocking-participant {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ margin: 8px 0;
+
+ .details {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ justify-content: space-evenly;
+ margin: 0 30px 0 10px;
+ }
+
+ button {
+ align-self: unset;
+ margin: 0 5px;
+ }
+}
+
+@media (max-width: 300px) {
+ #knocking-participant-list {
+ margin: 0;
+ text-align: center;
+ width: 100%;
+
+ .avatar {
+ display: none;
+ }
+ }
+
+ .knocking-participant {
+ flex-direction: column;
+
+ .details {
+ margin: 0;
+ }
+ }
+}
+
+@media (max-width: 1000px) {
+ .lobby-screen-content {
+
+ .lobby-chat-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 255;
+
+ &.hidden {
+ display: none;
+ }
+
+ .lobby-chat-header {
+ display: flex;
+ flex-direction: row;
+ padding-top: 20px;
+ padding-left: 16px;
+ padding-right: 16px;
+
+ .title {
+ flex: 1;
+ color: #fff;
+ font-size: 1.25rem;
+ font-weight: 600;
+ line-height: 1.75rem;
+ letter-spacing: -1.2%;
+ }
+ }
+ }
+
+ .open-chat-button {
+ display: block;
+ }
+ }
+}
+
+.lobby-button-margin {
+ margin-bottom: 16px;
+}
+
+.lobby-prejoin-error {
+ background-color: #E04757;
+ border-radius: 6px;
+ box-sizing: border-box;
+ color: white;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ margin-bottom: 16px;
+ margin-top: -8px;
+ padding: 4px;
+ text-align: center;
+ width: 100%;
+}
+
+.lobby-prejoin-input {
+ margin-bottom: 16px;
+ width: 100%;
+
+ & input {
+ text-align: center;
+ }
+}
diff --git a/css/premeeting/_main.scss b/css/premeeting/_main.scss
new file mode 100644
index 0000000..334b88c
--- /dev/null
+++ b/css/premeeting/_main.scss
@@ -0,0 +1,3 @@
+@import 'lobby';
+@import 'premeeting-screens';
+@import 'prejoin-third-party';
diff --git a/css/premeeting/_prejoin-third-party.scss b/css/premeeting/_prejoin-third-party.scss
new file mode 100644
index 0000000..dcdf84b
--- /dev/null
+++ b/css/premeeting/_prejoin-third-party.scss
@@ -0,0 +1,42 @@
+$sidePanelWidth: 300px;
+
+.prejoin-third-party {
+ flex-direction: column-reverse;
+ z-index: auto;
+ align-items: center;
+
+ .content {
+ height: auto;
+ margin: 0 auto;
+ width: auto;
+
+ .new-toolbox {
+ width: auto;
+ }
+ }
+
+ #preview {
+ background-color: transparent;
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ .avatar {
+ display: none;
+ }
+ }
+
+ &.splash {
+ .content {
+ margin-left: calc((100% - #{$prejoinDefaultContentWidth} + #{$sidePanelWidth}) / 2)
+ }
+ }
+
+ &.guest {
+ .content {
+ margin-bottom: auto;
+ }
+ }
+}
diff --git a/css/premeeting/_premeeting-screens.scss b/css/premeeting/_premeeting-screens.scss
new file mode 100644
index 0000000..bdf1972
--- /dev/null
+++ b/css/premeeting/_premeeting-screens.scss
@@ -0,0 +1,133 @@
+.premeeting-screen {
+ .action-btn {
+ border-radius: 6px;
+ box-sizing: border-box;
+ color: #fff;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 0.875rem;
+ font-weight: 600;
+ line-height: 1.5rem;
+ margin-bottom: 16px;
+ padding: 7px 16px;
+ position: relative;
+ text-align: center;
+ width: 100%;
+
+ &.primary {
+ background: #0376DA;
+ border: 1px solid #0376DA;
+ }
+
+ &.secondary {
+ background: #3D3D3D;
+ border: 1px solid transparent;
+ }
+
+ &.text {
+ width: auto;
+ font-size: 0.75rem;
+ margin: 0;
+ padding: 0;
+ }
+
+ &.disabled {
+ background: #5E6D7A;
+ border: 1px solid #5E6D7A;
+ color: #AFB6BC;
+ cursor: initial;
+
+ .icon {
+ & > svg {
+ fill: #AFB6BC;
+ }
+ }
+ }
+
+ .options {
+ border-radius: 3px;
+ align-items: center;
+ display: flex;
+ height: 100%;
+ justify-content: center;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 36px;
+
+ &:hover {
+ background-color: #0262B6;
+ }
+
+ svg {
+ pointer-events: none;
+ }
+ }
+ }
+
+ #new-toolbox {
+ bottom: 0;
+ position: relative;
+ transition: none;
+
+ .toolbox-content {
+ margin-bottom: 4px;
+ }
+
+ .toolbox-content-items {
+ @include ltr;
+ background: transparent;
+ box-shadow: none;
+ display: flex;
+ justify-content: space-between;
+ padding: 8px 0;
+ }
+
+ .toolbox-content,
+ .toolbox-content-wrapper,
+ .toolbox-content-items {
+ box-sizing: border-box;
+ width: auto;
+ }
+ }
+
+ @media (max-width: 400px) {
+ .device-status-error {
+ border-radius: 0;
+ margin: 0 -16px;
+ }
+
+ .action-btn {
+ font-size: 1rem;
+ margin-bottom: 8px;
+ padding: 11px 16px;
+ }
+ }
+}
+
+#preview {
+ background: #040404;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+
+ .avatar {
+ text {
+ fill: white;
+ }
+ }
+
+ video {
+ height: 100%;
+ object-fit: cover;
+ width: 100%;
+ }
+}
+
+@mixin flex-centered() {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+}
diff --git a/css/reload_overlay/_reload_overlay.scss b/css/reload_overlay/_reload_overlay.scss
new file mode 100644
index 0000000..89e68ad
--- /dev/null
+++ b/css/reload_overlay/_reload_overlay.scss
@@ -0,0 +1,26 @@
+.reload_overlay_title {
+ display: block;
+ font-size: 1rem;
+ line-height: 1.25rem;
+}
+
+.reload_overlay_text {
+ display: block;
+ font-size: 0.75rem;
+ line-height: 1.875rem;
+}
+
+#reloadProgressBar {
+ background: #e9e9e9;
+ border-radius: 3px;
+ height: 5px;
+ margin: 5px auto;
+ overflow: hidden;
+ width: 180px;
+
+ .progress-indicator-fill {
+ background: #0074E0;
+ height: 100%;
+ transition: width .5s;
+ }
+}
diff --git a/css/ringing/_ringing.scss b/css/ringing/_ringing.scss
new file mode 100644
index 0000000..0663c8c
--- /dev/null
+++ b/css/ringing/_ringing.scss
@@ -0,0 +1,45 @@
+.ringing {
+ display: block;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ z-index: 300;
+ @include transparentBg(#283447, 0.95);
+
+ &.solidBG {
+ background: #040404;
+ }
+
+ &__content {
+ position: absolute;
+ width: 400px;
+ height: 250px;
+ left: 50%;
+ top: 50%;
+ margin-left: -200px;
+ margin-top: -125px;
+ text-align: center;
+ font-weight: normal;
+ color: #FFFFFF;
+ }
+
+ &__avatar {
+ width: 128px;
+ height: 128px;
+ border-radius: 50%;
+ border: 2px solid #1B2638;
+ }
+
+ &__status{
+ margin-top: 15px;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ }
+
+ &__name {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ }
+}
diff --git a/css/third-party-branding/google.scss b/css/third-party-branding/google.scss
new file mode 100644
index 0000000..e8bc7a3
--- /dev/null
+++ b/css/third-party-branding/google.scss
@@ -0,0 +1,32 @@
+/**
+ * The Google sign in button must follow Google's design guidelines.
+ * See: https://developers.google.com/identity/branding-guidelines
+ */
+.google-sign-in {
+ background-color: #4285f4;
+ border-radius: 2px;
+ cursor: pointer;
+ display: inline-flex;
+ font-family: Roboto, arial, sans-serif;
+ font-size: 0.875rem;
+ padding: 1px;
+
+ .google-cta {
+ color: white;
+ display: inline-block;
+ /**
+ * Hack the line height for vertical centering of text.
+ */
+ line-height: 2rem;
+ margin: 0 15px;
+ }
+
+ .google-logo {
+ background-color: white;
+ border-radius: 2px;
+ display: inline-block;
+ padding: 8px;
+ height: 18px;
+ width: 18px;
+ }
+}
diff --git a/css/third-party-branding/microsoft.scss b/css/third-party-branding/microsoft.scss
new file mode 100644
index 0000000..ccaa1ff
--- /dev/null
+++ b/css/third-party-branding/microsoft.scss
@@ -0,0 +1,28 @@
+/**
+ * The Microsoft sign in button must follow Microsoft's brand guidelines.
+ * See: https://docs.microsoft.com/en-us/azure/active-directory/
+ * develop/active-directory-branding-guidelines
+ */
+.microsoft-sign-in {
+ align-items: center;
+ background: #FFFFFF;
+ border: 1px solid #8C8C8C;
+ box-sizing: border-box;
+ cursor: pointer;
+ display: inline-flex;
+ font-family: Segoe UI, Roboto, arial, sans-serif;
+ height: 41px;
+ padding: 12px;
+
+ .microsoft-cta {
+ display: inline-block;
+ color: #5E5E5E;
+ font-size: 0.875rem;
+ line-height: 2.5rem;
+ }
+
+ .microsoft-logo {
+ display: inline-block;
+ margin-right: 12px;
+ }
+}
diff --git a/css/unsupported-browser/_main.scss b/css/unsupported-browser/_main.scss
new file mode 100644
index 0000000..d40c011
--- /dev/null
+++ b/css/unsupported-browser/_main.scss
@@ -0,0 +1 @@
+@import 'unsupported-desktop-browser';
diff --git a/css/unsupported-browser/_unsupported-desktop-browser.scss b/css/unsupported-browser/_unsupported-desktop-browser.scss
new file mode 100644
index 0000000..ae6a328
--- /dev/null
+++ b/css/unsupported-browser/_unsupported-desktop-browser.scss
@@ -0,0 +1,39 @@
+.unsupported-desktop-browser {
+ @include absoluteAligning();
+
+ display: block;
+ text-align: center;
+
+ &__title {
+ color: $unsupportedBrowserTitleColor;
+ font-weight: 300;
+ font-size: $unsupportedBrowserTitleFontSize;
+ letter-spacing: 1px;
+ }
+
+ &__description {
+ color: $unsupportedDesktopBrowserTextColor;
+ font-size: $unsupportedDesktopBrowserTextFontSize;
+ font-weight: 300;
+ letter-spacing: 1px;
+ margin-top: 16px;
+
+ &_small {
+ @extend .unsupported-desktop-browser__description;
+ font-size: $unsupportedBrowserTextSmallFontSize;
+ }
+ }
+
+ &__link {
+ color: #489afe;
+ @include transition(color .1s ease-out);
+
+ &:hover {
+ color: #287ade;
+ cursor: pointer;
+ text-decoration: none;
+
+ @include transition(color .1s ease-in);
+ }
+ }
+}
diff --git a/custom.d.ts b/custom.d.ts
new file mode 100644
index 0000000..60bd434
--- /dev/null
+++ b/custom.d.ts
@@ -0,0 +1,4 @@
+declare module '*.svg' {
+ const content: any;
+ export default content;
+}
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..da40ffb
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+jitsi-meet-web (1.0.1-1) unstable; urgency=low
+
+ * Initial release. (Closes: #760485)
+
+ -- Damian Minkov Wed, 22 Oct 2014 10:30:00 +0200
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..48082f7
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+12
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..b4569a4
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,58 @@
+Source: jitsi-meet-web
+Section: net
+Priority: extra
+Maintainer: Jitsi Team
+Uploaders: Emil Ivov , Damian Minkov
+Build-Depends: debhelper (>= 8.0.0)
+Standards-Version: 3.9.6
+Homepage: https://jitsi.org/meet
+
+Package: jitsi-meet-web
+Replaces: jitsi-meet (<= 1.0.1525-1)
+Architecture: all
+Depends: ${misc:Depends}
+Description: WebRTC JavaScript video conferences
+ Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
+ Videobridge to provide high quality, scalable video conferences.
+ .
+ It is a web interface to Jitsi Videobridge for audio and video
+ forwarding and relaying.
+
+Package: jitsi-meet-web-config
+Architecture: all
+Pre-Depends: nginx | nginx-full | nginx-extras | openresty | apache2
+Depends: openssl, curl
+Description: Configuration for web serving of Jitsi Meet
+ Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
+ Videobridge to provide high quality, scalable video conferences.
+ .
+ It is a web interface to Jitsi Videobridge for audio and video
+ forwarding and relaying, using a webserver Nginx or Apache2.
+ .
+ This package contains configuration for Nginx to be used with
+ Jitsi Meet.
+
+Package: jitsi-meet-prosody
+Architecture: all
+Depends: openssl, prosody (>= 0.12.0) | prosody-trunk | prosody-0.12 | prosody-13.0, lua-sec, lua-basexx, lua-luaossl, lua-cjson, lua-inspect
+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.
+ .
+ It is a web interface to Jitsi Videobridge for audio and video
+ forwarding and relaying.
+ .
+ This package contains configuration for Prosody to be used with
+ Jitsi Meet.
+
+Package: jitsi-meet-tokens
+Architecture: all
+Depends: ${misc:Depends}, prosody-trunk | prosody-0.12 | prosody-13.0 | prosody (>= 0.12.0), jitsi-meet-prosody
+Description: Prosody token authentication plugin for Jitsi Meet
+
+Package: jitsi-meet-turnserver
+Architecture: all
+Pre-Depends: jitsi-meet-web-config
+Depends: ${misc:Depends}, jitsi-meet-prosody, coturn, dnsutils
+Description: Configures coturn to be used with Jitsi Meet
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..782cfaa
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,38 @@
+Format: http://dep.debian.net/deps/dep5
+Upstream-Name: Jitsi Meet
+Upstream-Contact: Emil Ivov
+Source: https://github.com/jitsi/jitsi-meet
+
+Files: *
+Copyright: 2015 Atlassian Pty Ltd
+License: Apache-2.0
+
+License: Apache-2.0
+On Debian systems, the full text of the Apache
+ License version 2 can be found in the file
+ '/usr/share/common-licenses/Apache-2.0'.
+ Note:
+ This project was originally contributed to the community under the MIT license
+ and with the following notice:
+ .
+ The MIT License (MIT)
+ .
+ Copyright (c) 2013 ESTOS GmbH
+ Copyright (c) 2013 BlueJimp SARL
+ .
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
+ this software and associated documentation files (the "Software"), to deal in
+ the Software without restriction, including without limitation the rights to
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ the Software, and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/debian/jitsi-meet-prosody.README.Debian b/debian/jitsi-meet-prosody.README.Debian
new file mode 100644
index 0000000..c0127d6
--- /dev/null
+++ b/debian/jitsi-meet-prosody.README.Debian
@@ -0,0 +1,7 @@
+Prosody configuration for Jitsi Meet for Debian
+----------------------------
+
+Jitsi Meet is a WebRTC video conferencing application. This package contains
+configuration of prosody which are needed for Jitsi Meet to work.
+
+ -- Yasen Pramatarov Mon, 30 Jun 2014 23:05:18 +0100
diff --git a/debian/jitsi-meet-prosody.docs b/debian/jitsi-meet-prosody.docs
new file mode 100644
index 0000000..63f794a
--- /dev/null
+++ b/debian/jitsi-meet-prosody.docs
@@ -0,0 +1 @@
+doc/debian/jitsi-meet-prosody/README
diff --git a/debian/jitsi-meet-prosody.install b/debian/jitsi-meet-prosody.install
new file mode 100644
index 0000000..6661443
--- /dev/null
+++ b/debian/jitsi-meet-prosody.install
@@ -0,0 +1,3 @@
+doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example /usr/share/jitsi-meet-prosody/
+doc/debian/jitsi-meet-prosody/jaas.cfg.lua /usr/share/jitsi-meet-prosody/
+resources/prosody-plugins/ /usr/share/jitsi-meet/
diff --git a/debian/jitsi-meet-prosody.postinst b/debian/jitsi-meet-prosody.postinst
new file mode 100644
index 0000000..a2ff18e
--- /dev/null
+++ b/debian/jitsi-meet-prosody.postinst
@@ -0,0 +1,293 @@
+#!/bin/bash
+# postinst script for jitsi-meet-prosody
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `configure'
+# * `abort-upgrade'
+# * `abort-remove' `in-favour'
+#
+# * `abort-remove'
+# * `abort-deconfigure' `in-favour'
+# `removing'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+function generateRandomPassword() {
+ cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16
+}
+
+case "$1" in
+ configure)
+
+ # loading debconf
+ . /usr/share/debconf/confmodule
+
+ # try to get host from jitsi-videobridge
+ db_get jitsi-videobridge/jvb-hostname
+ if [ -z "$RET" ] ; then
+ # server hostname
+ db_set jitsi-videobridge/jvb-hostname "localhost"
+ db_input critical jitsi-videobridge/jvb-hostname || true
+ db_go
+ fi
+ JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
+
+ db_get jitsi-videobridge/jvbsecret
+ if [ -z "$RET" ] ; then
+ db_input critical jitsi-videobridge/jvbsecret || true
+ db_go
+ fi
+ JVB_SECRET="$RET"
+
+ JICOFO_AUTH_USER="focus"
+
+ db_get jicofo/jicofo-authpassword
+ if [ -z "$RET" ] ; then
+ # if password is missing generate it, and store it
+ JICOFO_AUTH_PASSWORD=`generateRandomPassword`
+ db_set jicofo/jicofo-authpassword "$JICOFO_AUTH_PASSWORD"
+ else
+ JICOFO_AUTH_PASSWORD="$RET"
+ fi
+
+ JICOFO_AUTH_DOMAIN="auth.$JVB_HOSTNAME"
+
+ # detect dpkg-reconfigure, just delete old links
+ db_get jitsi-meet-prosody/jvb-hostname
+ JVB_HOSTNAME_OLD=$(echo "$RET" | xargs echo -n)
+ if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
+ rm -f /etc/prosody/conf.d/$JVB_HOSTNAME_OLD.cfg.lua
+ rm -f /etc/prosody/certs/$JVB_HOSTNAME_OLD.key
+ rm -f /etc/prosody/certs/$JVB_HOSTNAME_OLD.crt
+ fi
+
+ # stores the hostname so we will reuse it later, like in purge
+ db_set jitsi-meet-prosody/jvb-hostname "$JVB_HOSTNAME"
+
+ db_get jitsi-meet-prosody/turn-secret
+ if [ -z "$RET" ] ; then
+ # 8-chars random secret used for the turnserver
+ TURN_SECRET=`generateRandomPassword`
+ db_set jitsi-meet-prosody/turn-secret "$TURN_SECRET"
+ else
+ TURN_SECRET="$RET"
+ fi
+
+ SELF_SIGNED_CHOICE="Generate a new self-signed certificate"
+ # In the case of updating from an older version the configure of -prosody package may happen before the -config
+ # one, so if JAAS_INPUT is empty (the question is not asked), let's ask it now.
+ # If db_get returns an error (workaround for strange Debian failure) continue without stopping the config
+ db_get jitsi-meet/cert-choice || CERT_CHOICE=$SELF_SIGNED_CHOICE
+ CERT_CHOICE="$RET"
+ if [ -z "$CERT_CHOICE" ] ; then
+ db_input critical jitsi-meet/cert-choice || true
+ db_go
+ db_get jitsi-meet/cert-choice
+ CERT_CHOICE="$RET"
+ fi
+ if [ "$CERT_CHOICE" != "$SELF_SIGNED_CHOICE" ]; then
+ db_get jitsi-meet/jaas-choice
+ JAAS_INPUT="$RET"
+ if [ -z "$JAAS_INPUT" ] ; then
+ db_subst jitsi-meet/jaas-choice domain "${JVB_HOSTNAME}"
+ db_set jitsi-meet/jaas-choice false
+ db_input critical jitsi-meet/jaas-choice || true
+ db_go
+ db_get jitsi-meet/jaas-choice
+ JAAS_INPUT="$RET"
+ fi
+ fi
+
+ # and we're done with debconf
+ db_stop
+
+ PROSODY_CONFIG_PRESENT="true"
+ PROSODY_CREATE_JICOFO_USER="false"
+ PROSODY_HOST_CONFIG="/etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua"
+ PROSODY_CONFIG_OLD="/etc/prosody/prosody.cfg.lua"
+ # if there is no prosody config extract our template
+ # check for config in conf.avail or check whether it wasn't already configured in main config
+ if [ ! -f $PROSODY_HOST_CONFIG ] && ! grep -q "VirtualHost \"$JVB_HOSTNAME\"" $PROSODY_CONFIG_OLD; then
+ PROSODY_CONFIG_PRESENT="false"
+ mkdir -p /etc/prosody/conf.avail/
+ mkdir -p /etc/prosody/conf.d/
+ cp /usr/share/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
+ sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
+ sed -i "s/focusUser/$JICOFO_AUTH_USER/g" $PROSODY_HOST_CONFIG
+ sed -i "s/__turnSecret__/$TURN_SECRET/g" $PROSODY_HOST_CONFIG
+ if [ ! -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua ]; then
+ ln -s $PROSODY_HOST_CONFIG /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
+ fi
+ PROSODY_CREATE_JICOFO_USER="true"
+ # on some distributions main prosody config doesn't include configs
+ # from conf.d folder enable it as this where we put our config by default
+ if ! grep -q "Include \"conf\.d\/\*\.cfg.lua\"" $PROSODY_CONFIG_OLD; then
+ echo -e "\nInclude \"conf.d/*.cfg.lua\"" >> $PROSODY_CONFIG_OLD
+ fi
+ fi
+
+ # Check whether prosody config has the internal muc, if not add it,
+ # as we are migrating configs
+ if [ -f $PROSODY_HOST_CONFIG ] && ! grep -q "internal.$JICOFO_AUTH_DOMAIN" $PROSODY_HOST_CONFIG; then
+ echo -e "\nComponent \"internal.$JICOFO_AUTH_DOMAIN\" \"muc\"" >> $PROSODY_HOST_CONFIG
+ echo -e " storage = \"memory\"" >> $PROSODY_HOST_CONFIG
+ echo -e " modules_enabled = { \"ping\"; }" >> $PROSODY_HOST_CONFIG
+ echo -e " admins = { \"$JICOFO_AUTH_USER@$JICOFO_AUTH_DOMAIN\", \"jvb@$JICOFO_AUTH_DOMAIN\" }" >> $PROSODY_HOST_CONFIG
+ echo -e " muc_room_locking = false" >> $PROSODY_HOST_CONFIG
+ echo -e " muc_room_default_public_jids = true" >> $PROSODY_HOST_CONFIG
+ fi
+
+ # Convert the old focus component config to the new one.
+ # Old:
+ # Component "focus.jitmeet.example.com"
+ # component_secret = "focusSecret"
+ # New:
+ # Component "focus.jitmeet.example.com" "client_proxy"
+ # target_address = "focus@auth.jitmeet.example.com"
+ if grep -q "Component \"focus.$JVB_HOSTNAME\"" $PROSODY_HOST_CONFIG && ! grep -q "Component \"focus.$JVB_HOSTNAME\" \"client_proxy\"" $PROSODY_HOST_CONFIG ;then
+ sed -i "s/Component \"focus.$JVB_HOSTNAME\"/Component \"focus.$JVB_HOSTNAME\" \"client_proxy\"\n target_address = \"$JICOFO_AUTH_USER@$JICOFO_AUTH_DOMAIN\"/g" $PROSODY_HOST_CONFIG
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+
+ # Old versions of jitsi-meet-prosody come with the extra plugin path commented out (https://github.com/jitsi/jitsi-meet/commit/e11d4d3101e5228bf956a69a9e8da73d0aee7949)
+ # Make sure it is uncommented, as it contains required modules.
+ if grep -q -- '--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }' $PROSODY_HOST_CONFIG ;then
+ sed -i 's#--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }#plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }#g' $PROSODY_HOST_CONFIG
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+
+ # Updates main muc component
+ MAIN_MUC_PATTERN="Component \"conference.$JVB_HOSTNAME\" \"muc\""
+ if ! grep -A 2 -- "${MAIN_MUC_PATTERN}" $PROSODY_HOST_CONFIG | grep -q "restrict_room_creation" ;then
+ sed -i "s/${MAIN_MUC_PATTERN}/${MAIN_MUC_PATTERN}\n restrict_room_creation = true/g" $PROSODY_HOST_CONFIG
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+
+ if ! grep -q -- 'unlimited_jids' $PROSODY_HOST_CONFIG ;then
+ sed -i "1s/^/unlimited_jids = { \"$JICOFO_AUTH_USER@$JICOFO_AUTH_DOMAIN\", \"jvb@$JICOFO_AUTH_DOMAIN\" }\n/" $PROSODY_HOST_CONFIG
+ sed -i "s/VirtualHost \"$JICOFO_AUTH_DOMAIN\"/VirtualHost \"$JICOFO_AUTH_DOMAIN\"\n modules_enabled = { \"limits_exception\"; }/g" $PROSODY_HOST_CONFIG
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+
+ # Since prosody 13 admins are not automatically room owners and we expect that for jicofo
+ if ! grep -q -- 'component_admins_as_room_owners = ' $PROSODY_HOST_CONFIG ;then
+ sed -i "1s/^/component_admins_as_room_owners = true\n/" $PROSODY_HOST_CONFIG
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+
+ JAAS_HOST_CONFIG="/etc/prosody/conf.avail/jaas.cfg.lua"
+ if [ "${JAAS_INPUT}" = "true" ] && [ ! -f $JAAS_HOST_CONFIG ]; then
+ sed -i "s/enabled = false -- Jitsi meet components/enabled = true -- Jitsi meet components/g" $PROSODY_HOST_CONFIG
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+
+ # For those deployments that don't have the config in the jitsi-meet prosody config add the new jaas file
+ if [ ! -f $JAAS_HOST_CONFIG ] && ! grep -q "VirtualHost \"jigasi.meet.jitsi\"" $PROSODY_HOST_CONFIG; then
+ PROSODY_CONFIG_PRESENT="false"
+ cp /usr/share/jitsi-meet-prosody/jaas.cfg.lua $JAAS_HOST_CONFIG
+ sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $JAAS_HOST_CONFIG
+ fi
+
+ if [ "${JAAS_INPUT}" = "true" ]; then
+ JAAS_HOST_CONFIG_ENABLED="/etc/prosody/conf.d/jaas.cfg.lua "
+ if [ ! -f $JAAS_HOST_CONFIG_ENABLED ] && ! grep -q "VirtualHost \"jigasi.meet.jitsi\"" $PROSODY_HOST_CONFIG; then
+ if [ -f $JAAS_HOST_CONFIG ]; then
+ ln -s $JAAS_HOST_CONFIG $JAAS_HOST_CONFIG_ENABLED
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+ fi
+ fi
+
+ if [ ! -f /var/lib/prosody/$JVB_HOSTNAME.crt ]; then
+ # prosodyctl takes care for the permissions
+ # echo for using all default values
+ echo | prosodyctl cert generate $JVB_HOSTNAME
+
+ ln -sf /var/lib/prosody/$JVB_HOSTNAME.key /etc/prosody/certs/$JVB_HOSTNAME.key
+ ln -sf /var/lib/prosody/$JVB_HOSTNAME.crt /etc/prosody/certs/$JVB_HOSTNAME.crt
+ fi
+
+ CERT_ADDED_TO_TRUST="false"
+
+ if [ ! -f /var/lib/prosody/$JICOFO_AUTH_DOMAIN.crt ]; then
+ # prosodyctl takes care for the permissions
+ # echo for using all default values
+ echo | prosodyctl cert generate $JICOFO_AUTH_DOMAIN
+
+ AUTH_KEY_FILE="/etc/prosody/certs/$JICOFO_AUTH_DOMAIN.key"
+ AUTH_CRT_FILE="/etc/prosody/certs/$JICOFO_AUTH_DOMAIN.crt"
+
+ ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.key $AUTH_KEY_FILE
+ ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.crt $AUTH_CRT_FILE
+ ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.crt /usr/local/share/ca-certificates/$JICOFO_AUTH_DOMAIN.crt
+
+ # we need to force updating certificates, in some cases java trust
+ # store not get re-generated with latest changes
+ update-ca-certificates -f
+
+ CERT_ADDED_TO_TRUST="true"
+
+ # don't fail on systems with custom config ($PROSODY_HOST_CONFIG is missing)
+ if [ -f $PROSODY_HOST_CONFIG ]; then
+ # now let's add the ssl cert for the auth. domain (we use # as a sed delimiter cause filepaths are confused with default / delimiter)
+ sed -i "s#VirtualHost \"$JICOFO_AUTH_DOMAIN\"#VirtualHost \"$JICOFO_AUTH_DOMAIN\"\n ssl = {\n key = \"$AUTH_KEY_FILE\";\n certificate = \"$AUTH_CRT_FILE\";\n \}#g" $PROSODY_HOST_CONFIG
+ fi
+
+ # trigger a restart
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+
+ if [ "$PROSODY_CONFIG_PRESENT" = "false" ]; then
+ invoke-rc.d prosody restart || true
+
+ # give it some time to warm up
+ sleep 10
+
+ if [ "$PROSODY_CREATE_JICOFO_USER" = "true" ]; then
+ # create 'focus@auth.domain' prosody user
+ echo -e "$JICOFO_AUTH_PASSWORD\n$JICOFO_AUTH_PASSWORD" | prosodyctl adduser $JICOFO_AUTH_USER@$JICOFO_AUTH_DOMAIN > /dev/null || true
+
+ # trigger a restart
+ PROSODY_CONFIG_PRESENT="false"
+ fi
+
+ # creates the user if it does not exist
+ echo -e "$JVB_SECRET\n$JVB_SECRET" | prosodyctl adduser jvb@$JICOFO_AUTH_DOMAIN > /dev/null || true
+
+ # Make sure the focus@auth user's roster includes the proxy component (this is idempotent)
+ prosodyctl mod_roster_command subscribe focus.$JVB_HOSTNAME $JICOFO_AUTH_USER@$JICOFO_AUTH_DOMAIN
+
+ # To make sure the roster command is loaded
+ # Once we have https://issues.prosody.im/1908 we can start using prosodyctl shell roster subscribe
+ # and drop the wait and the prosody restart
+ sleep 1
+ invoke-rc.d prosody restart || true
+
+ # In case we had updated the certificates and restarted prosody, let's restart and the bridge and jicofo if possible
+ if [ -d /run/systemd/system ] && [ "$CERT_ADDED_TO_TRUST" = "true" ]; then
+ systemctl restart jitsi-videobridge2.service >/dev/null || true
+ systemctl restart jicofo.service >/dev/null || true
+ fi
+ fi
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/jitsi-meet-prosody.postrm b/debian/jitsi-meet-prosody.postrm
new file mode 100644
index 0000000..33d676c
--- /dev/null
+++ b/debian/jitsi-meet-prosody.postrm
@@ -0,0 +1,78 @@
+#!/bin/sh
+# postrm script for jitsi-meet-prosody
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `remove'
+# * `purge'
+# * `upgrade'
+# * `failed-upgrade'
+# * `abort-install'
+# * `abort-install'
+# * `abort-upgrade'
+# * `disappear'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+# Load debconf
+. /usr/share/debconf/confmodule
+
+
+case "$1" in
+ remove)
+ if [ -x "/etc/init.d/prosody" ]; then
+ invoke-rc.d prosody reload || true
+ fi
+ ;;
+
+ purge)
+ db_get jitsi-meet-prosody/jvb-hostname
+ JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
+ if [ -n "$RET" ]; then
+ rm -f /etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua
+ rm -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
+ rm -f /etc/prosody/conf.avail/jaas.cfg.lua
+ rm -f /etc/prosody/conf.d/jaas.cfg.lua
+
+ JICOFO_AUTH_DOMAIN="auth.$JVB_HOSTNAME"
+ # clean up generated certificates
+ rm -f /etc/prosody/certs/$JVB_HOSTNAME.crt
+ rm -f /etc/prosody/certs/$JVB_HOSTNAME.key
+ rm -f /etc/prosody/certs/$JICOFO_AUTH_DOMAIN.crt
+ rm -f /etc/prosody/certs/$JICOFO_AUTH_DOMAIN.key
+ rm -rf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.*
+ rm -rf /var/lib/prosody/$JVB_HOSTNAME.*
+
+ # clean created users, replace '.' with '%2e', replace '-' with '%2d'
+ rm -rf /var/lib/prosody/`echo $JICOFO_AUTH_DOMAIN | sed -e "s/\./%2e/g"| sed -e "s/-/%2d/g"`
+
+ # clean the prosody cert from the trust store
+ rm -rf /usr/local/share/ca-certificates/$JICOFO_AUTH_DOMAIN.*
+ update-ca-certificates -f
+ fi
+
+ # Clear the debconf variable
+ db_purge
+ ;;
+
+ upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+ ;;
+
+ *)
+ echo "postrm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+db_stop
+
+exit 0
diff --git a/debian/jitsi-meet-prosody.templates b/debian/jitsi-meet-prosody.templates
new file mode 100644
index 0000000..a2225a0
--- /dev/null
+++ b/debian/jitsi-meet-prosody.templates
@@ -0,0 +1,24 @@
+Template: jitsi-meet-prosody/jvb-hostname
+Type: string
+_Description: The domain of the current installation (e.g. meet.jitsi.com):
+ The value of the domain that is set in the Jitsi Videobridge installation.
+
+Template: jitsi-videobridge/jvb-hostname
+Type: string
+_Description: The domain of the current installation (e.g. meet.jitsi.com):
+ The value of the domain that is set in the Jitsi Videobridge installation.
+
+Template: jitsi-videobridge/jvbsecret
+Type: password
+_Description: Jitsi Videobridge Component secret:
+ The secret used by Jitsi Videobridge to connect to xmpp server as component.
+
+Template: jicofo/jicofo-authpassword
+Type: password
+_Description: Jicofo user password:
+ The secret used to connect to xmpp server as jicofo user.
+
+Template: jitsi-meet-prosody/turn-secret
+Type: string
+_Description: The turn server secret
+ The secret used to connect to turnserver server.
diff --git a/debian/jitsi-meet-tokens.README.Debian b/debian/jitsi-meet-tokens.README.Debian
new file mode 100644
index 0000000..0a54726
--- /dev/null
+++ b/debian/jitsi-meet-tokens.README.Debian
@@ -0,0 +1,7 @@
+Token authentication plugin for Jitsi Meet
+----------------------------
+
+Jitsi Meet is a WebRTC video conferencing application. This package contains
+Prosody plugin which enables token authentication in Jitsi Meet installation.
+
+ -- Pawel Domas Mon, 2 Nov 2015 14:45:00 -0600
diff --git a/debian/jitsi-meet-tokens.config b/debian/jitsi-meet-tokens.config
new file mode 100644
index 0000000..db965ce
--- /dev/null
+++ b/debian/jitsi-meet-tokens.config
@@ -0,0 +1,10 @@
+#!/bin/sh -e
+
+# Source debconf library.
+. /usr/share/debconf/confmodule
+
+db_input critical jitsi-meet-tokens/appid || true
+db_go
+
+db_input critical jitsi-meet-tokens/appsecret || true
+db_go
\ No newline at end of file
diff --git a/debian/jitsi-meet-tokens.docs b/debian/jitsi-meet-tokens.docs
new file mode 100644
index 0000000..e69de29
diff --git a/debian/jitsi-meet-tokens.postinst b/debian/jitsi-meet-tokens.postinst
new file mode 100644
index 0000000..3df2f9f
--- /dev/null
+++ b/debian/jitsi-meet-tokens.postinst
@@ -0,0 +1,91 @@
+#!/bin/bash
+# postinst script for jitsi-meet-tokens
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `configure'
+# * `abort-upgrade'
+# * `abort-remove' `in-favour'
+#
+# * `abort-remove'
+# * `abort-deconfigure' `in-favour'
+# `removing'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ configure)
+
+ # loading debconf
+ . /usr/share/debconf/confmodule
+
+ db_get jitsi-meet-prosody/jvb-hostname
+ JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
+
+ db_get jitsi-meet-tokens/appid
+ if [ "$RET" = "false" ] ; then
+ echo "Application ID is mandatory"
+ exit 1
+ fi
+ APP_ID=$RET
+
+ db_get jitsi-meet-tokens/appsecret
+ if [ "$RET" = "false" ] ; then
+ echo "Application secret is mandatory"
+ fi
+ # Not allowed unix special characters in secret: /, \, ", ', `
+ if echo "$RET" | grep -q "[/\\\"\`\']" ; then
+ echo "Application secret contains invalid characters: /, \\, \", ', \`"
+ exit 1
+ fi
+ APP_SECRET=$RET
+
+ PROSODY_HOST_CONFIG="/etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua"
+
+ # Store config filename for purge
+ db_set jitsi-meet-prosody/prosody_config "$PROSODY_HOST_CONFIG"
+
+ db_stop
+
+ if [ -f "$PROSODY_HOST_CONFIG" ] ; then
+ # search for the token auth, if this is not enabled this is the
+ # first time we install tokens package and needs a config change
+ if ! egrep -q '^\s*authentication\s*=\s*"token" -- do not delete me' "$PROSODY_HOST_CONFIG"; then
+ # enable tokens in prosody host config
+ sed -i 's/--plugin_paths/plugin_paths/g' $PROSODY_HOST_CONFIG
+ sed -i 's/authentication = "jitsi-anonymous" -- do not delete me/authentication = "token" -- do not delete me/g' $PROSODY_HOST_CONFIG
+ sed -i "s/ --app_id=\"example_app_id\"/ app_id=\"$APP_ID\"/g" $PROSODY_HOST_CONFIG
+ sed -i "s/ --app_secret=\"example_app_secret\"/ app_secret=\"$APP_SECRET\"/g" $PROSODY_HOST_CONFIG
+ sed -i 's/ --modules_enabled = { "token_verification" }/ modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
+ sed -i '/^\s*--\s*"token_verification"/ s/--\s*//' $PROSODY_HOST_CONFIG
+
+ if [ -x "/etc/init.d/prosody" ]; then
+ invoke-rc.d prosody restart || true
+ fi
+ fi
+ else
+ echo "Prosody config not found at $PROSODY_HOST_CONFIG - unable to auto-configure token authentication"
+ fi
+
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/jitsi-meet-tokens.postrm b/debian/jitsi-meet-tokens.postrm
new file mode 100644
index 0000000..e0f3030
--- /dev/null
+++ b/debian/jitsi-meet-tokens.postrm
@@ -0,0 +1,74 @@
+#!/bin/sh
+# postrm script for jitsi-meet-tokens
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `remove'
+# * `purge'
+# * `upgrade'
+# * `failed-upgrade'
+# * `abort-install'
+# * `abort-install'
+# * `abort-upgrade'
+# * `disappear'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+# Load debconf
+. /usr/share/debconf/confmodule
+
+
+case "$1" in
+ remove)
+
+ db_get jitsi-meet-prosody/prosody_config
+ PROSODY_HOST_CONFIG=$RET
+
+ if [ -f "$PROSODY_HOST_CONFIG" ] ; then
+
+ db_get jitsi-meet-tokens/appid
+ APP_ID=$RET
+
+ db_get jitsi-meet-tokens/appsecret
+ APP_SECRET=$RET
+
+ # Revert prosody config
+ sed -i 's/authentication = "token" -- do not delete me/authentication = "jitsi-anonymous" -- do not delete me/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"/ s/"token_verification"/-- "token_verification"/' $PROSODY_HOST_CONFIG
+
+ if [ -x "/etc/init.d/prosody" ]; then
+ invoke-rc.d prosody restart || true
+ fi
+ fi
+
+ db_stop
+ ;;
+
+ purge)
+ # Clear the debconf variable
+ db_purge
+ ;;
+
+ upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+ ;;
+
+ *)
+ echo "postrm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+db_stop
+
+exit 0
diff --git a/debian/jitsi-meet-tokens.templates b/debian/jitsi-meet-tokens.templates
new file mode 100644
index 0000000..de31185
--- /dev/null
+++ b/debian/jitsi-meet-tokens.templates
@@ -0,0 +1,14 @@
+Template: jitsi-meet-tokens/appid
+Type: string
+_Description: The application ID to be used by token authentication plugin:
+ Application ID:
+
+Template: jitsi-meet-tokens/appsecret
+Type: password
+_Description: The application secret to be used by token authentication plugin:
+ Application secret:
+
+Template: jitsi-meet-prosody/prosody_config
+Type: string
+_Description: The location of Jitsi Meet Prosody config file
+ Jitsi-meet Prosody config file location:
\ No newline at end of file
diff --git a/debian/jitsi-meet-turnserver.install b/debian/jitsi-meet-turnserver.install
new file mode 100644
index 0000000..6bf8fb6
--- /dev/null
+++ b/debian/jitsi-meet-turnserver.install
@@ -0,0 +1,2 @@
+doc/debian/jitsi-meet-turn/turnserver.conf /usr/share/jitsi-meet-turnserver/
+doc/debian/jitsi-meet/jitsi-meet.conf /usr/share/jitsi-meet-turnserver/
diff --git a/debian/jitsi-meet-turnserver.postinst b/debian/jitsi-meet-turnserver.postinst
new file mode 100644
index 0000000..57f9b01
--- /dev/null
+++ b/debian/jitsi-meet-turnserver.postinst
@@ -0,0 +1,150 @@
+#!/bin/bash
+# postinst script for jitsi-meet-turnserver
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `configure'
+# * `abort-upgrade'
+# * `abort-remove' `in-favour'
+#
+# * `abort-remove'
+# * `abort-deconfigure' `in-favour'
+# `removing'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+case "$1" in
+ configure)
+ # loading debconf
+ . /usr/share/debconf/confmodule
+
+ # try to get host from jitsi-videobridge
+ db_get jitsi-videobridge/jvb-hostname
+ if [ -z "$RET" ] ; then
+ # server hostname
+ db_set jitsi-videobridge/jvb-hostname "localhost"
+ db_input critical jitsi-videobridge/jvb-hostname || true
+ db_go
+ fi
+ JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
+
+ TURN_CONFIG="/etc/turnserver.conf"
+ JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
+
+ # if there was a turn config backup it so we can configure
+ # we cannot recognize at the moment is this a user config or default config when installing coturn
+ if [[ -f $TURN_CONFIG ]] && ! grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
+ mv $TURN_CONFIG $TURN_CONFIG.bak
+ fi
+
+ # detect dpkg-reconfigure, just delete old links
+ db_get jitsi-meet-turnserver/jvb-hostname
+ JVB_HOSTNAME_OLD=$(echo "$RET" | xargs echo -n)
+ if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
+ if [[ -f $TURN_CONFIG ]] && grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
+ rm -f $TURN_CONFIG
+ fi
+ fi
+
+ if [[ -f $TURN_CONFIG ]] ; then
+ echo "------------------------------------------------"
+ echo ""
+ echo "turnserver is already configured on this machine."
+ echo ""
+ echo "------------------------------------------------"
+
+ if grep -q "jitsi-meet coturn config" "$TURN_CONFIG" && ! grep -q "jitsi-meet coturn relay disable config" "$TURN_CONFIG" ; then
+ echo "Updating coturn config"
+ echo "# jitsi-meet coturn relay disable config. Do not modify this line
+no-multicast-peers
+no-cli
+no-loopback-peers
+no-tcp-relay
+denied-peer-ip=0.0.0.0-0.255.255.255
+denied-peer-ip=10.0.0.0-10.255.255.255
+denied-peer-ip=100.64.0.0-100.127.255.255
+denied-peer-ip=127.0.0.0-127.255.255.255
+denied-peer-ip=169.254.0.0-169.254.255.255
+denied-peer-ip=127.0.0.0-127.255.255.255
+denied-peer-ip=172.16.0.0-172.31.255.255
+denied-peer-ip=192.0.0.0-192.0.0.255
+denied-peer-ip=192.0.2.0-192.0.2.255
+denied-peer-ip=192.88.99.0-192.88.99.255
+denied-peer-ip=192.168.0.0-192.168.255.255
+denied-peer-ip=198.18.0.0-198.19.255.255
+denied-peer-ip=198.51.100.0-198.51.100.255
+denied-peer-ip=203.0.113.0-203.0.113.255
+denied-peer-ip=240.0.0.0-255.255.255.255" >> $TURN_CONFIG
+
+ invoke-rc.d coturn restart || true
+ fi
+
+ db_stop
+ exit 0
+ fi
+
+ # stores the hostname so we will reuse it later, like in purge
+ db_set jitsi-meet-turnserver/jvb-hostname "$JVB_HOSTNAME"
+
+ # try to get turnserver password
+ db_get jitsi-meet-prosody/turn-secret
+ if [ -z "$RET" ] ; then
+ db_input critical jitsi-meet-prosody/turn-secret || true
+ db_go
+ fi
+ TURN_SECRET="$RET"
+
+ # no turn config exists, lt's copy template and fill it in
+ cp /usr/share/jitsi-meet-turnserver/turnserver.conf $TURN_CONFIG
+ sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $TURN_CONFIG
+ sed -i "s/__turnSecret__/$TURN_SECRET/g" $TURN_CONFIG
+
+ # SSL settings
+ db_get jitsi-meet/cert-choice
+ CERT_CHOICE="$RET"
+
+ UPLOADED_CERT_CHOICE="I want to use my own certificate"
+ LE_CERT_CHOICE="Let's Encrypt certificates"
+ if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ]; then
+ db_get jitsi-meet/cert-path-key
+ CERT_KEY="$RET"
+ db_get jitsi-meet/cert-path-crt
+ CERT_CRT="$RET"
+
+ # replace self-signed certificate paths with user provided ones
+ CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
+ CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
+ sed -i "s/pkey=\/etc\/jitsi\/meet\/.*key/pkey=$CERT_KEY_ESC/g" $TURN_CONFIG
+ CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
+ CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
+ sed -i "s/cert=\/etc\/jitsi\/meet\/.*crt/cert=$CERT_CRT_ESC/g" $TURN_CONFIG
+ elif [ "$CERT_CHOICE" = "$LE_CERT_CHOICE" ]; then
+ /usr/share/jitsi-meet/scripts/coturn-le-update.sh ${JVB_HOSTNAME}
+ fi
+
+ sed -i "s/#TURNSERVER_ENABLED/TURNSERVER_ENABLED/g" /etc/default/coturn
+ invoke-rc.d coturn restart || true
+
+ # and we're done with debconf
+ db_stop
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/jitsi-meet-turnserver.postrm b/debian/jitsi-meet-turnserver.postrm
new file mode 100644
index 0000000..6e1308e
--- /dev/null
+++ b/debian/jitsi-meet-turnserver.postrm
@@ -0,0 +1,47 @@
+#!/bin/sh
+# postrm script for jitsi-meet-turnserver
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `remove'
+# * `purge'
+# * `upgrade'
+# * `failed-upgrade'
+# * `abort-install'
+# * `abort-install'
+# * `abort-upgrade'
+# * `disappear'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+# Load debconf
+. /usr/share/debconf/confmodule
+
+
+case "$1" in
+ purge)
+ rm -rf /etc/turnserver.conf
+ # Clear the debconf variable
+ db_purge
+ ;;
+ remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+ ;;
+
+ *)
+ echo "postrm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+db_stop
+
+exit 0
diff --git a/debian/jitsi-meet-turnserver.templates b/debian/jitsi-meet-turnserver.templates
new file mode 100644
index 0000000..8947ea3
--- /dev/null
+++ b/debian/jitsi-meet-turnserver.templates
@@ -0,0 +1,9 @@
+Template: jitsi-meet-turnserver/jvb-hostname
+Type: string
+_Description: The domain of the current installation (e.g. meet.jitsi.com):
+ The value of the domain that is set in the Jitsi Videobridge installation.
+
+Template: jitsi-videobridge/jvb-hostname
+Type: string
+_Description: The domain of the current installation (e.g. meet.jitsi.com):
+ The value of the domain that is set in the Jitsi Videobridge installation.
diff --git a/debian/jitsi-meet-web-config.dirs b/debian/jitsi-meet-web-config.dirs
new file mode 100644
index 0000000..0bb2ab3
--- /dev/null
+++ b/debian/jitsi-meet-web-config.dirs
@@ -0,0 +1 @@
+etc/jitsi/meet/
diff --git a/debian/jitsi-meet-web-config.docs b/debian/jitsi-meet-web-config.docs
new file mode 100644
index 0000000..690648e
--- /dev/null
+++ b/debian/jitsi-meet-web-config.docs
@@ -0,0 +1 @@
+doc/debian/jitsi-meet/README
diff --git a/debian/jitsi-meet-web-config.install b/debian/jitsi-meet-web-config.install
new file mode 100644
index 0000000..75c8210
--- /dev/null
+++ b/debian/jitsi-meet-web-config.install
@@ -0,0 +1,6 @@
+doc/debian/jitsi-meet/jitsi-meet.example /usr/share/jitsi-meet-web-config/
+doc/debian/jitsi-meet/jitsi-meet.example-apache /usr/share/jitsi-meet-web-config/
+config.js /usr/share/jitsi-meet-web-config/
+doc/jaas/nginx-jaas.conf /usr/share/jitsi-meet-web-config/
+doc/jaas/index-jaas.html /usr/share/jitsi-meet-web-config/
+doc/jaas/8x8.vc-config.js /usr/share/jitsi-meet-web-config/
diff --git a/debian/jitsi-meet-web-config.postinst b/debian/jitsi-meet-web-config.postinst
new file mode 100644
index 0000000..a24a71e
--- /dev/null
+++ b/debian/jitsi-meet-web-config.postinst
@@ -0,0 +1,318 @@
+#!/bin/bash
+# postinst script for jitsi-meet-web-config
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `configure'
+# * `abort-upgrade'
+# * `abort-remove' `in-favour'
+#
+# * `abort-remove'
+# * `abort-deconfigure' `in-favour'
+# `removing'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+case "$1" in
+ configure)
+
+ # loading debconf
+ . /usr/share/debconf/confmodule
+
+ # try to get host from jitsi-videobridge
+ db_get jitsi-videobridge/jvb-hostname
+ if [ -z "$RET" ] ; then
+ # server hostname
+ db_set jitsi-videobridge/jvb-hostname "localhost"
+ db_input critical jitsi-videobridge/jvb-hostname || true
+ db_go
+ db_get jitsi-videobridge/jvb-hostname
+ fi
+ JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
+
+ # detect dpkg-reconfigure
+ RECONFIGURING="false"
+ db_get jitsi-meet/jvb-hostname
+ JVB_HOSTNAME_OLD=$(echo "$RET" | xargs echo -n)
+ if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
+ RECONFIGURING="true"
+ rm -f /etc/jitsi/meet/$JVB_HOSTNAME_OLD-config.js
+ fi
+
+ # stores the hostname so we will reuse it later, like in purge
+ db_set jitsi-meet/jvb-hostname $JVB_HOSTNAME
+
+ NGINX_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx' 2>/dev/null | awk '{print $3}' || true)"
+ NGINX_FULL_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx-full' 2>/dev/null | awk '{print $3}' || true)"
+ NGINX_EXTRAS_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx-extras' 2>/dev/null | awk '{print $3}' || true)"
+ if [ "$NGINX_INSTALL_CHECK" = "installed" ] \
+ || [ "$NGINX_INSTALL_CHECK" = "unpacked" ] \
+ || [ "$NGINX_FULL_INSTALL_CHECK" = "installed" ] \
+ || [ "$NGINX_FULL_INSTALL_CHECK" = "unpacked" ] \
+ || [ "$NGINX_EXTRAS_INSTALL_CHECK" = "installed" ] \
+ || [ "$NGINX_EXTRAS_INSTALL_CHECK" = "unpacked" ] ; then
+ FORCE_NGINX="true"
+ fi
+ OPENRESTY_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'openresty' 2>/dev/null | awk '{print $3}' || true)"
+ if [ "$OPENRESTY_INSTALL_CHECK" = "installed" ] || [ "$OPENRESTY_INSTALL_CHECK" = "unpacked" ] ; then
+ FORCE_OPENRESTY="true"
+ fi
+ APACHE_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'apache2' 2>/dev/null | awk '{print $3}' || true)"
+ if [ "$APACHE_INSTALL_CHECK" = "installed" ] || [ "$APACHE_INSTALL_CHECK" = "unpacked" ] ; then
+ FORCE_APACHE="true"
+ fi
+ # In case user enforces apache and if apache is available, unset nginx.
+ RET=""
+ db_get jitsi-meet/enforce_apache || RET="false"
+ if [ "$RET" = "true" ] && [ "$FORCE_APACHE" = "true" ]; then
+ FORCE_NGINX="false"
+ fi
+
+ UPLOADED_CERT_CHOICE="I want to use my own certificate"
+ LE_CERT_CHOICE="Let's Encrypt certificates"
+ # if first time config ask for certs, or if we are reconfiguring
+ if [ -z "$JVB_HOSTNAME_OLD" ] || [ "$RECONFIGURING" = "true" ] ; then
+ RET=""
+ # ask the question only if there is nothing stored, option to pre-set it on install in automations
+ db_get jitsi-meet/cert-choice
+ CERT_CHOICE="$RET"
+ if [ -z "$CERT_CHOICE" ] ; then
+ db_input critical jitsi-meet/cert-choice || true
+ db_go
+ db_get jitsi-meet/cert-choice
+ CERT_CHOICE="$RET"
+ fi
+
+ if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ]; then
+ RET=""
+ db_get jitsi-meet/cert-path-key
+ if [ -z "$RET" ] ; then
+ db_set jitsi-meet/cert-path-key "/etc/ssl/$JVB_HOSTNAME.key"
+ db_input critical jitsi-meet/cert-path-key || true
+ db_go
+ db_get jitsi-meet/cert-path-key
+ fi
+ CERT_KEY="$RET"
+ RET=""
+ db_get jitsi-meet/cert-path-crt
+ if [ -z "$RET" ] ; then
+ db_set jitsi-meet/cert-path-crt "/etc/ssl/$JVB_HOSTNAME.crt"
+ db_input critical jitsi-meet/cert-path-crt || true
+ db_go
+ db_get jitsi-meet/cert-path-crt
+ fi
+ CERT_CRT="$RET"
+ else
+ # create self-signed certs (we also need them for the case of LE so we can start nginx)
+ CERT_KEY="/etc/jitsi/meet/$JVB_HOSTNAME.key"
+ CERT_CRT="/etc/jitsi/meet/$JVB_HOSTNAME.crt"
+ HOST="$( (hostname -s; echo localhost) | head -n 1)"
+ DOMAIN="$( (hostname -d; echo localdomain) | head -n 1)"
+ openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj \
+ "/O=$DOMAIN/OU=$HOST/CN=$JVB_HOSTNAME/emailAddress=webmaster@$HOST.$DOMAIN" \
+ -keyout $CERT_KEY \
+ -out $CERT_CRT \
+ -reqexts SAN \
+ -extensions SAN \
+ -config <(cat /etc/ssl/openssl.cnf \
+ <(printf "[SAN]\nsubjectAltName=DNS:localhost,DNS:$JVB_HOSTNAME"))
+
+ if [ "$CERT_CHOICE" = "$LE_CERT_CHOICE" ]; then
+ db_subst jitsi-meet/email domain "${JVB_HOSTNAME}"
+ db_input critical jitsi-meet/email || true
+ db_go
+ db_get jitsi-meet/email
+ EMAIL="$RET"
+ if [ ! -z "$EMAIL" ] ; then
+ ISSUE_LE_CERT="true"
+ fi
+ fi
+ fi
+ fi
+
+ # jitsi meet
+ JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
+ if [ ! -f $JITSI_MEET_CONFIG ] ; then
+ cp /usr/share/jitsi-meet-web-config/config.js $JITSI_MEET_CONFIG
+ # replaces needed config for multidomain as it works only with nginx
+ if [[ "$FORCE_NGINX" = "true" ]] ; then
+ sed -i "s/conference.jitsi-meet.example.com/conference.<\!--# echo var=\"subdomain\" default=\"\" -->jitsi-meet.example.com/g" $JITSI_MEET_CONFIG
+ fi
+ sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $JITSI_MEET_CONFIG
+ fi
+
+ if [ "$CERT_CHOICE" = "$LE_CERT_CHOICE" ] || [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ]; then
+ # Make sure jaas-choice is not answered already
+ db_get jitsi-meet/jaas-choice
+ JAAS_INPUT="$RET"
+ if [ -z "$JAAS_INPUT" ] ; then
+ db_subst jitsi-meet/jaas-choice domain "${JVB_HOSTNAME}"
+ db_set jitsi-meet/jaas-choice false
+ db_input critical jitsi-meet/jaas-choice || true
+ db_go
+ db_get jitsi-meet/jaas-choice
+ JAAS_INPUT="$RET"
+ fi
+ fi
+
+ if [ "${JAAS_INPUT}" = "true" ] && ! grep -q "^var enableJaaS = true;$" $JITSI_MEET_CONFIG; then
+ if grep -q "^var enableJaaS = false;$" $JITSI_MEET_CONFIG; then
+ sed -i "s/^var enableJaaS = false;$/var enableJaaS = true;/g" $JITSI_MEET_CONFIG
+ else
+ # old config, let's add the lines at the end. Adding var enableJaaS to avoid adding it on update again
+ echo "var enableJaaS = true;" >> $JITSI_MEET_CONFIG
+ echo "config.dialInNumbersUrl = 'https://conference-mapper.jitsi.net/v1/access/dids';" >> $JITSI_MEET_CONFIG
+ echo "config.dialInConfCodeUrl = 'https://conference-mapper.jitsi.net/v1/access';" >> $JITSI_MEET_CONFIG
+
+ # Sets roomPasswordNumberOfDigits only if there was not already set
+ if ! cat $JITSI_MEET_CONFIG | grep roomPasswordNumberOfDigits | grep -qv //; then
+ echo "config.roomPasswordNumberOfDigits = 10; // skip re-adding it (do not remove comment)" >> $JITSI_MEET_CONFIG
+ fi
+ fi
+ fi
+
+ if [[ "$FORCE_OPENRESTY" = "true" ]]; then
+ NGX_COMMON_CONF_PATH="/usr/local/openresty/nginx/conf/$JVB_HOSTNAME.conf"
+ NGX_SVC_NAME=openresty
+ OPENRESTY_NGX_CONF="/usr/local/openresty/nginx/conf/nginx.conf"
+ else
+ NGX_COMMON_CONF_PATH="/etc/nginx/sites-available/$JVB_HOSTNAME.conf"
+ NGX_SVC_NAME=nginx
+ fi
+
+ if [[ ( "$FORCE_NGINX" = "true" || "$FORCE_OPENRESTY" = "true" ) && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
+
+ # this is a reconfigure, lets just delete old links
+ if [ "$RECONFIGURING" = "true" ] ; then
+ rm -f /etc/nginx/sites-enabled/$JVB_HOSTNAME_OLD.conf
+ rm -f /etc/jitsi/meet/$JVB_HOSTNAME_OLD-config.js
+ if [[ "$FORCE_OPENRESTY" = "true" ]]; then
+ sed -i "/include.*$JVB_HOSTNAME_OLD/d" "$OPENRESTY_NGX_CONF"
+ fi
+ fi
+
+ # nginx conf
+ if [ ! -f "$NGX_COMMON_CONF_PATH" ] ; then
+ cp /usr/share/jitsi-meet-web-config/jitsi-meet.example "$NGX_COMMON_CONF_PATH"
+ if [ ! -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf ] && ! [[ "$FORCE_OPENRESTY" = "true" ]] ; then
+ ln -s "$NGX_COMMON_CONF_PATH" /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf
+ fi
+ sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" "$NGX_COMMON_CONF_PATH"
+
+ if [[ "$FORCE_OPENRESTY" = "true" ]]; then
+ OPENRESTY_NGX_CONF_MD5_ORIG=$(dpkg-query -s openresty | sed -n '/\/nginx\.conf /{s@.* @@;p}')
+ OPENRESTY_NGX_CONF_MD5_USERS=$(md5sum "$OPENRESTY_NGX_CONF" | sed 's@ .*@@')
+ if [[ "$OPENRESTY_NGX_CONF_MD5_USERS" = "$OPENRESTY_NGX_CONF_MD5_ORIG" ]]; then
+ sed -i "/^http \x7b/,/^\x7d/s@^\x7d@\tinclude $NGX_COMMON_CONF_PATH;\n\x7d@" "$OPENRESTY_NGX_CONF"
+ fi
+ fi
+ fi
+
+ if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
+ # replace self-signed certificate paths with user provided ones
+ CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
+ CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
+ sed -i "s/ssl_certificate_key\ \/etc\/jitsi\/meet\/.*key/ssl_certificate_key\ $CERT_KEY_ESC/g" \
+ "$NGX_COMMON_CONF_PATH"
+ CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
+ CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
+ sed -i "s/ssl_certificate\ \/etc\/jitsi\/meet\/.*crt/ssl_certificate\ $CERT_CRT_ESC/g" \
+ "$NGX_COMMON_CONF_PATH"
+ fi
+
+ invoke-rc.d $NGX_SVC_NAME reload || true
+ elif [[ "$FORCE_APACHE" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
+
+ # this is a reconfigure, lets just delete old links
+ if [ "$RECONFIGURING" = "true" ] ; then
+ a2dissite $JVB_HOSTNAME_OLD.conf
+ rm -f /etc/jitsi/meet/$JVB_HOSTNAME_OLD-config.js
+ fi
+
+ # apache2 config
+ if [ ! -f /etc/apache2/sites-available/$JVB_HOSTNAME.conf ] ; then
+ # when creating new config, make sure all needed modules are enabled
+ a2enmod rewrite ssl headers proxy_http proxy_wstunnel include
+ cp /usr/share/jitsi-meet-web-config/jitsi-meet.example-apache /etc/apache2/sites-available/$JVB_HOSTNAME.conf
+ a2ensite $JVB_HOSTNAME.conf
+ sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/apache2/sites-available/$JVB_HOSTNAME.conf
+ fi
+
+ if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
+ # replace self-signed certificate paths with user provided ones
+ CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
+ CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
+ sed -i "s/SSLCertificateKeyFile\ \/etc\/jitsi\/meet\/.*key/SSLCertificateKeyFile\ $CERT_KEY_ESC/g" \
+ /etc/apache2/sites-available/$JVB_HOSTNAME.conf
+ CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
+ CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
+ sed -i "s/SSLCertificateFile\ \/etc\/jitsi\/meet\/.*crt/SSLCertificateFile\ $CERT_CRT_ESC/g" \
+ /etc/apache2/sites-available/$JVB_HOSTNAME.conf
+ fi
+
+ invoke-rc.d apache2 reload || true
+ fi
+
+ # If scripts fail they will print suggestions for next steps, do not fail install
+ # those can be re-run later
+ # run the scripts only on new install or when re-configuring
+ if [[ "$ISSUE_LE_CERT" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
+ /usr/share/jitsi-meet/scripts/install-letsencrypt-cert.sh $EMAIL $JVB_HOSTNAME || true
+ fi
+ JAAS_REG_ERROR=0
+ if [[ "${JAAS_INPUT}" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
+ /usr/share/jitsi-meet/scripts/register-jaas-account.sh $EMAIL $JVB_HOSTNAME || JAAS_REG_ERROR=$?
+ fi
+
+ echo ""
+ echo ""
+ echo " ;dOocd;"
+ echo " .dNMM0dKO."
+ echo " lNMMMKd0K,"
+ echo " .xMMMMNxkNc"
+ echo " dMMMMMkxXc"
+ echo " cNMMMNl.."
+ if [ "${JAAS_INPUT}" != "true" ] || [ ${JAAS_REG_ERROR} -ne 0 ]; then
+ echo " .kMMMX; Interested in adding telephony to your Jitsi meetings?"
+ echo " ;XMMMO'"
+ echo " lNMMWO' Sign up on https://jaas.8x8.vc/components?host=${JVB_HOSTNAME}"
+ echo " lNMMM0, and follow the guide in the dev console."
+ else
+ echo " .kMMMX;"
+ echo " ;XMMMO' Congratulations! Now you can use telephony in your Jitsi meetings!"
+ echo " lNMMWO' We have created a free JaaS (Jitsi as a Service) account for you. "
+ echo " lNMMM0, You can login to https://jaas.8x8.vc/components to check our developer console and your account details."
+ fi
+ echo " lXMMMK:."
+ echo " ;KMMMNKd. 'oo,"
+ echo " 'xNMMMMXkkkkOKOl'"
+ echo " :0WMMMMMMNOkk0Kk,"
+ echo " .cdOWMMMMMWXOkOl"
+ echo " .;dKWMMMMMXc."
+ echo " .,:cll:'"
+ echo ""
+ echo ""
+
+ # and we're done with debconf
+ db_stop
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/jitsi-meet-web-config.postrm b/debian/jitsi-meet-web-config.postrm
new file mode 100644
index 0000000..2877b65
--- /dev/null
+++ b/debian/jitsi-meet-web-config.postrm
@@ -0,0 +1,69 @@
+#!/bin/sh
+# postrm script for jitsi-meet-web-config
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `remove'
+# * `purge'
+# * `upgrade'
+# * `failed-upgrade'
+# * `abort-install'
+# * `abort-install'
+# * `abort-upgrade'
+# * `disappear'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+# Load debconf
+. /usr/share/debconf/confmodule
+
+
+case "$1" in
+ remove)
+ if [ -x "/etc/init.d/openresty" ]; then
+ invoke-rc.d openresty reload || true
+ fi
+ if [ -x "/etc/init.d/nginx" ]; then
+ invoke-rc.d nginx reload || true
+ fi
+ if [ -x "/etc/init.d/apache2" ]; then
+ invoke-rc.d apache2 reload || true
+ fi
+ ;;
+ purge)
+ db_get jitsi-meet/jvb-hostname
+ JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
+ if [ -n "$RET" ]; then
+ rm -f /etc/jitsi/meet/$JVB_HOSTNAME-config.js
+ rm -f /etc/nginx/sites-available/$JVB_HOSTNAME.conf
+ rm -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf
+ rm -f /usr/local/openresty/nginx/conf/$JVB_HOSTNAME.conf
+ rm -f /etc/apache2/sites-available/$JVB_HOSTNAME.conf
+ rm -f /etc/apache2/sites-enabled/$JVB_HOSTNAME.conf
+ rm -f /etc/jitsi/meet/$JVB_HOSTNAME.key
+ rm -f /etc/jitsi/meet/$JVB_HOSTNAME.crt
+ fi
+ # Clear the debconf variable
+ db_purge
+ ;;
+ upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+ ;;
+
+ *)
+ echo "postrm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+db_stop
+
+exit 0
diff --git a/debian/jitsi-meet-web-config.templates b/debian/jitsi-meet-web-config.templates
new file mode 100644
index 0000000..eb74a32
--- /dev/null
+++ b/debian/jitsi-meet-web-config.templates
@@ -0,0 +1,49 @@
+Template: jitsi-meet/cert-choice
+Type: select
+__Choices: Let's Encrypt certificates, I want to use my own certificate, Generate a new self-signed certificate
+_Description: SSL certificate
+ .
+ Jitsi Meet requires an SSL certificate. This installer can generate one automatically for your using "Let’s Encrypt". This is the recommended and simplest option for most installations.
+ .
+ In the event you need to use a certificate of your own, you can configure its location which defaults to /etc/ssl/--domain.name--.key for the key and /etc/ssl/--domain.name--.crt for the certificate.
+ .
+ If you are a developer and are only looking for a quick way to test basic Jitsi Meet functionality then this installer can also generate a self-signed certificate.
+
+Template: jitsi-meet/cert-path-key
+Type: string
+_Description: Full local server path to the SSL key file:
+ The full path to the SSL key file on the server.
+ If it has not been uploaded, now is a good time to do so.
+
+Template: jitsi-meet/cert-path-crt
+Type: string
+_Description: Full local server path to the SSL certificate file:
+ The full path to the SSL certificate file on the server.
+ If you haven't uploaded it, now is a good time to upload it in another console.
+
+Template: jitsi-meet/jvb-hostname
+Type: string
+_Description: The domain of the current installation (e.g. meet.jitsi.com):
+ The value of the domain that is set in the Jitsi Videobridge installation.
+
+Template: jitsi-videobridge/jvb-hostname
+Type: string
+_Description: Hostname:
+ The Jitsi Meet web config package needs the DNS hostname of your instance.
+
+Template: jitsi-meet/jaas-choice
+Type: boolean
+_Description: Add telephony to your Jitsi meetings?
+ You can easily add dial-in support to your meetings. To allow this we would need your permission to create a free JaaS (Jitsi as a Service) account for you.
+
+Template: jitsi-meet/email
+Type: string
+_Description: Enter your email:
+ To successfully issue Let's Encrypt certificates:
+ .
+ You need a working DNS record pointing to this machine(for hostname ${domain})"
+ .
+ You need to agree to the ACME server's Subscriber Agreement (https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf)
+ by providing an email address for important account notifications.
+ .
+ We will use the email for creating your JaaS (Jitsi as a Service) account if that option was selected.
diff --git a/debian/jitsi-meet-web.README.Debian b/debian/jitsi-meet-web.README.Debian
new file mode 100644
index 0000000..e79c133
--- /dev/null
+++ b/debian/jitsi-meet-web.README.Debian
@@ -0,0 +1,8 @@
+Jitsi Meet for Debian
+----------------------------
+
+This is a WebRTC frontend of the video conferencing tool Jitsi Meet. It depends on the
+jitsi-videobridge package, which is a SFU (Selective Forwarding Unit) and both packages
+are designed to work together.
+
+ -- Yasen Pramatarov Mon, 30 Jun 2014 23:05:18 +0100
diff --git a/debian/jitsi-meet-web.docs b/debian/jitsi-meet-web.docs
new file mode 100644
index 0000000..b43bf86
--- /dev/null
+++ b/debian/jitsi-meet-web.docs
@@ -0,0 +1 @@
+README.md
diff --git a/debian/jitsi-meet-web.install b/debian/jitsi-meet-web.install
new file mode 100644
index 0000000..c09a355
--- /dev/null
+++ b/debian/jitsi-meet-web.install
@@ -0,0 +1,15 @@
+interface_config.js /usr/share/jitsi-meet/
+*.html /usr/share/jitsi-meet/
+libs /usr/share/jitsi-meet/
+static /usr/share/jitsi-meet/
+css/all.css /usr/share/jitsi-meet/css/
+sounds /usr/share/jitsi-meet/
+fonts /usr/share/jitsi-meet/
+images /usr/share/jitsi-meet/
+lang /usr/share/jitsi-meet/
+resources/robots.txt /usr/share/jitsi-meet/
+resources/*.sh /usr/share/jitsi-meet/scripts/
+pwa-worker.js /usr/share/jitsi-meet/
+manifest.json /usr/share/jitsi-meet/
+doc/jaas/move-to-jaas.sh /usr/share/jitsi-meet/scripts/
+doc/jaas/update-asap-daily.sh /usr/share/jitsi-meet/scripts/
diff --git a/debian/po/POTFILES.in b/debian/po/POTFILES.in
new file mode 100644
index 0000000..6ddfac3
--- /dev/null
+++ b/debian/po/POTFILES.in
@@ -0,0 +1 @@
+[type: gettext/rfc822deb] jitsi-meet-web-config.templates
diff --git a/debian/po/templates.pot b/debian/po/templates.pot
new file mode 100644
index 0000000..35d837a
--- /dev/null
+++ b/debian/po/templates.pot
@@ -0,0 +1,113 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the jitsi-meet-web package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: jitsi-meet-web\n"
+"Report-Msgid-Bugs-To: jitsi-meet-web@packages.debian.org\n"
+"POT-Creation-Date: 2016-11-15 22:39+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Type: select
+#. Choices
+#: ../jitsi-meet-web-config.templates:1001
+msgid "Generate a new self-signed certificate"
+msgstr ""
+
+#. Type: select
+#. Choices
+#: ../jitsi-meet-web-config.templates:1001
+msgid "I want to use my own certificate"
+msgstr ""
+
+#. Type: select
+#. Description
+#: ../jitsi-meet-web-config.templates:1002
+msgid "SSL certificate for the Jitsi Meet instance"
+msgstr ""
+
+#. Type: select
+#. Description
+#: ../jitsi-meet-web-config.templates:1002
+msgid ""
+"Jitsi Meet is best to be set up with an SSL certificate. Having no "
+"certificate, a self-signed one will be generated. Having a certificate "
+"signed by a recognised CA, it can be uploaded on the server and point its "
+"location. The default filenames will be /etc/ssl/--domain.name--.key for the "
+"key and /etc/ssl/--domain.name--.crt for the certificate."
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../jitsi-meet-web-config.templates:2001
+msgid "Full local server path to the SSL key file:"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../jitsi-meet-web-config.templates:2001
+msgid ""
+"The full path to the SSL key file on the server. If it has not been "
+"uploaded, now is a good time to do so."
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../jitsi-meet-web-config.templates:3001
+msgid "Full local server path to the SSL certificate file:"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../jitsi-meet-web-config.templates:3001
+msgid ""
+"The full path to the SSL certificate file on the server. If you haven't "
+"uploaded it, now is a good time to upload it in another console."
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../jitsi-meet-web-config.templates:4001
+msgid "The hostname of the current installation:"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../jitsi-meet-web-config.templates:4001
+msgid ""
+"The value of the domain that is set in the Jitsi Videobridge installation."
+msgstr ""
+
+#. Type: boolean
+#. Description
+#: ../jitsi-meet-web-config.templates:5001
+msgid "for internal use"
+msgstr ""
+
+#. Type: boolean
+#. Description
+#: ../jitsi-meet-web-config.templates:5001
+msgid "for internal use."
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../jitsi-meet-web-config.templates:6001
+msgid "Hostname:"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../jitsi-meet-web-config.templates:6001
+msgid ""
+"The Jitsi Meet web config package needs the DNS hostname of your instance."
+msgstr ""
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..60f3ae4
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,31 @@
+#!/usr/bin/make -f
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+LANGUAGES := $(shell node -p "Object.keys(require('./lang/languages.json')).join(' ')")
+COUNTRIES_DIR := node_modules/i18n-iso-countries/langs
+
+%:
+ dh $@
+
+# we skip making Makefile exists for updating browserify modules when developing
+override_dh_auto_build:
+
+override_dh_install: $(LANGUAGES)
+ dh_installdirs
+ dh_install
+
+$(LANGUAGES):
+ LOCALE=$$(echo $@ | cut -c1-2) ; \
+ if [ -f $(COUNTRIES_DIR)/$@.json ] ; \
+ then \
+ dh_install -pjitsi-meet-web $(COUNTRIES_DIR)/$@.json usr/share/jitsi-meet/lang/; \
+ mv debian/jitsi-meet-web/usr/share/jitsi-meet/lang/$@.json debian/jitsi-meet-web/usr/share/jitsi-meet/lang/countries-$@.json; \
+ else \
+ if [ -f $(COUNTRIES_DIR)/$$LOCALE.json ] ; \
+ then \
+ dh_install -pjitsi-meet-web $(COUNTRIES_DIR)/$$LOCALE.json usr/share/jitsi-meet/lang/; \
+ mv debian/jitsi-meet-web/usr/share/jitsi-meet/lang/$$LOCALE.json debian/jitsi-meet-web/usr/share/jitsi-meet/lang/countries-$@.json; \
+ fi; \
+ fi;
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/watch b/debian/watch
new file mode 100644
index 0000000..e33c057
--- /dev/null
+++ b/debian/watch
@@ -0,0 +1,2 @@
+version=3
+opts="uversionmangle=s/^/1.0./" https://github.com/jitsi/jitsi-meet/releases/ /jitsi/jitsi-meet/archive/(\S+)\.tar\.gz
\ No newline at end of file
diff --git a/doc/README.md b/doc/README.md
new file mode 100644
index 0000000..3d943e0
--- /dev/null
+++ b/doc/README.md
@@ -0,0 +1,3 @@
+# Documentation
+
+The Jitsi documentation has been moved to [The Handbook](https://jitsi.github.io/handbook/). The repo is https://github.com/jitsi/handbook.
diff --git a/doc/api.md b/doc/api.md
new file mode 100644
index 0000000..8091b84
--- /dev/null
+++ b/doc/api.md
@@ -0,0 +1,3 @@
+# Jitsi Meet API
+
+This document has been moved [here](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-iframe).
diff --git a/doc/debian/jitsi-meet-prosody/README b/doc/debian/jitsi-meet-prosody/README
new file mode 100644
index 0000000..d141f6b
--- /dev/null
+++ b/doc/debian/jitsi-meet-prosody/README
@@ -0,0 +1 @@
+Prosody configuration for Jitsi Meet
diff --git a/doc/debian/jitsi-meet-prosody/jaas.cfg.lua b/doc/debian/jitsi-meet-prosody/jaas.cfg.lua
new file mode 100644
index 0000000..20df64a
--- /dev/null
+++ b/doc/debian/jitsi-meet-prosody/jaas.cfg.lua
@@ -0,0 +1,12 @@
+-- Enables dial-in for Jitsi meet components customers
+VirtualHost "jigasi.meet.jitsi"
+ modules_enabled = {
+ "ping";
+ "bosh";
+ "muc_password_check";
+ }
+ authentication = "token"
+ app_id = "jitsi";
+ asap_key_server = "https://jaas-public-keys.jitsi.net/jitsi-components/prod-8x8"
+ asap_accepted_issuers = { "jaas-components" }
+ asap_accepted_audiences = { "jigasi.jitmeet.example.com" }
diff --git a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example
new file mode 100644
index 0000000..a98abdf
--- /dev/null
+++ b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example
@@ -0,0 +1,167 @@
+-- We need this for prosody 13.0
+component_admins_as_room_owners = true
+
+plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
+
+-- domain mapper options, must at least have domain base set to use the mapper
+muc_mapper_domain_base = "jitmeet.example.com";
+
+external_service_secret = "__turnSecret__";
+external_services = {
+ { type = "stun", host = "jitmeet.example.com", port = 3478 },
+ { type = "turn", host = "jitmeet.example.com", port = 3478, transport = "udp", secret = true, ttl = 86400, algorithm = "turn" },
+ { type = "turns", host = "jitmeet.example.com", port = 5349, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" }
+};
+
+cross_domain_bosh = false;
+consider_bosh_secure = true;
+consider_websocket_secure = true;
+-- https_ports = { }; -- Remove this line to prevent listening on port 5284
+
+-- by default prosody 0.12 sends cors headers, if you want to disable it uncomment the following (the config is available on 0.12.1)
+--http_cors_override = {
+-- bosh = {
+-- enabled = false;
+-- };
+-- websocket = {
+-- enabled = false;
+-- };
+--}
+
+-- https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4
+ssl = {
+ protocol = "tlsv1_2+";
+ ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"
+}
+
+unlimited_jids = {
+ "focusUser@auth.jitmeet.example.com",
+ "jvb@auth.jitmeet.example.com"
+}
+
+-- https://prosody.im/doc/modules/mod_smacks
+smacks_max_unacked_stanzas = 5;
+smacks_hibernation_time = 60;
+smacks_max_old_sessions = 1;
+
+VirtualHost "jitmeet.example.com"
+ authentication = "jitsi-anonymous" -- do not delete me
+ -- Properties below are modified by jitsi-meet-tokens package config
+ -- and authentication above is switched to "token"
+ --app_id="example_app_id"
+ --app_secret="example_app_secret"
+ -- Assign this host a certificate for TLS, otherwise it would use the one
+ -- set in the global section (if any).
+ -- Note that old-style SSL on port 5223 only supports one certificate, and will always
+ -- use the global one.
+ ssl = {
+ key = "/etc/prosody/certs/jitmeet.example.com.key";
+ certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
+ }
+ -- we need bosh
+ modules_enabled = {
+ "bosh";
+ "websocket";
+ "smacks";
+ "ping"; -- Enable mod_ping
+ "external_services";
+ "features_identity";
+ "conference_duration";
+ "muc_lobby_rooms";
+ "muc_breakout_rooms";
+ }
+ c2s_require_encryption = false
+ lobby_muc = "lobby.jitmeet.example.com"
+ breakout_rooms_muc = "breakout.jitmeet.example.com"
+ main_muc = "conference.jitmeet.example.com"
+ -- muc_lobby_whitelist = { "recorder.jitmeet.example.com" } -- Here we can whitelist jibri to enter lobby enabled rooms
+
+Component "conference.jitmeet.example.com" "muc"
+ restrict_room_creation = true
+ storage = "memory"
+ modules_enabled = {
+ "muc_hide_all";
+ "muc_meeting_id";
+ "muc_domain_mapper";
+ "polls";
+ --"token_verification";
+ "muc_rate_limit";
+ "muc_password_whitelist";
+ }
+ admins = { "focusUser@auth.jitmeet.example.com" }
+ muc_password_whitelist = {
+ "focusUser@auth.jitmeet.example.com"
+ }
+ muc_room_locking = false
+ muc_room_default_public_jids = true
+
+Component "breakout.jitmeet.example.com" "muc"
+ restrict_room_creation = true
+ storage = "memory"
+ modules_enabled = {
+ "muc_hide_all";
+ "muc_meeting_id";
+ "muc_domain_mapper";
+ "muc_rate_limit";
+ "polls";
+ }
+ admins = { "focusUser@auth.jitmeet.example.com" }
+ muc_room_locking = false
+ muc_room_default_public_jids = true
+
+-- internal muc component
+Component "internal.auth.jitmeet.example.com" "muc"
+ storage = "memory"
+ modules_enabled = {
+ "muc_hide_all";
+ "ping";
+ }
+ admins = { "focusUser@auth.jitmeet.example.com", "jvb@auth.jitmeet.example.com" }
+ muc_room_locking = false
+ muc_room_default_public_jids = true
+
+VirtualHost "auth.jitmeet.example.com"
+ modules_enabled = {
+ "limits_exception";
+ "smacks";
+ }
+ authentication = "internal_hashed"
+ smacks_hibernation_time = 15;
+
+VirtualHost "recorder.jitmeet.example.com"
+ modules_enabled = {
+ "smacks";
+ }
+ authentication = "internal_hashed"
+ smacks_max_old_sessions = 2000;
+
+-- Proxy to jicofo's user JID, so that it doesn't have to register as a component.
+Component "focus.jitmeet.example.com" "client_proxy"
+ target_address = "focusUser@auth.jitmeet.example.com"
+
+Component "speakerstats.jitmeet.example.com" "speakerstats_component"
+ muc_component = "conference.jitmeet.example.com"
+
+Component "endconference.jitmeet.example.com" "end_conference"
+ muc_component = "conference.jitmeet.example.com"
+
+Component "avmoderation.jitmeet.example.com" "av_moderation_component"
+ muc_component = "conference.jitmeet.example.com"
+
+Component "filesharing.jitmeet.example.com" "filesharing_component"
+ muc_component = "conference.jitmeet.example.com"
+
+Component "lobby.jitmeet.example.com" "muc"
+ storage = "memory"
+ restrict_room_creation = true
+ muc_room_locking = false
+ muc_room_default_public_jids = true
+ modules_enabled = {
+ "muc_hide_all";
+ "muc_rate_limit";
+ "polls";
+ }
+
+Component "metadata.jitmeet.example.com" "room_metadata_component"
+ muc_component = "conference.jitmeet.example.com"
+ breakout_rooms_component = "breakout.jitmeet.example.com"
diff --git a/doc/debian/jitsi-meet-turn/README b/doc/debian/jitsi-meet-turn/README
new file mode 100644
index 0000000..c64c0f4
--- /dev/null
+++ b/doc/debian/jitsi-meet-turn/README
@@ -0,0 +1 @@
+Coturn configuration for Jitsi Meet
diff --git a/doc/debian/jitsi-meet-turn/turnserver.conf b/doc/debian/jitsi-meet-turn/turnserver.conf
new file mode 100644
index 0000000..843f320
--- /dev/null
+++ b/doc/debian/jitsi-meet-turn/turnserver.conf
@@ -0,0 +1,45 @@
+# jitsi-meet coturn config. Do not modify this line
+use-auth-secret
+keep-address-family
+static-auth-secret=__turnSecret__
+realm=jitsi-meet.example.com
+cert=/etc/jitsi/meet/jitsi-meet.example.com.crt
+pkey=/etc/jitsi/meet/jitsi-meet.example.com.key
+no-multicast-peers
+no-cli
+no-loopback-peers
+no-tcp-relay
+no-tcp
+listening-port=3478
+tls-listening-port=5349
+no-tlsv1
+no-tlsv1_1
+# https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4
+cipher-list=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
+# without it there are errors when running on Ubuntu 20.04
+dh2066
+# jitsi-meet coturn relay disable config. Do not modify this line
+denied-peer-ip=0.0.0.0-0.255.255.255
+denied-peer-ip=10.0.0.0-10.255.255.255
+denied-peer-ip=100.64.0.0-100.127.255.255
+denied-peer-ip=127.0.0.0-127.255.255.255
+denied-peer-ip=169.254.0.0-169.254.255.255
+denied-peer-ip=127.0.0.0-127.255.255.255
+denied-peer-ip=172.16.0.0-172.31.255.255
+denied-peer-ip=192.0.0.0-192.0.0.255
+denied-peer-ip=192.0.2.0-192.0.2.255
+denied-peer-ip=192.88.99.0-192.88.99.255
+denied-peer-ip=192.168.0.0-192.168.255.255
+denied-peer-ip=198.18.0.0-198.19.255.255
+denied-peer-ip=198.51.100.0-198.51.100.255
+denied-peer-ip=203.0.113.0-203.0.113.255
+denied-peer-ip=240.0.0.0-255.255.255.255
+denied-peer-ip=::1
+denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
+denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
+denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
+denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
+denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
+denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
+denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
+syslog
diff --git a/doc/debian/jitsi-meet/README b/doc/debian/jitsi-meet/README
new file mode 100644
index 0000000..a198dca
--- /dev/null
+++ b/doc/debian/jitsi-meet/README
@@ -0,0 +1,13 @@
+Jitsi Meet
+
+====
+
+A WebRTC-powered multi-user videochat. For a live demo, check out either
+https://meet.estos.de/ or https://meet.jit.si/.
+
+Built using colibri.js[0] and strophe.jingle[1], powered by the jitsi-videobridge[2]
+
+
+[0] https://github.com/ESTOS/colibri.js
+[1] https://github.com/ESTOS/strophe.jingle
+[3] https://github.com/jitsi/jitsi-videobridge
diff --git a/doc/debian/jitsi-meet/jitsi-meet.conf b/doc/debian/jitsi-meet/jitsi-meet.conf
new file mode 100644
index 0000000..59e9a53
--- /dev/null
+++ b/doc/debian/jitsi-meet/jitsi-meet.conf
@@ -0,0 +1,34 @@
+# this is jitsi-meet nginx module configuration
+# this forward all http traffic to the nginx virtual host port
+# and the rest to the turn server
+#
+# Multiplexing based on ALPN is DEPRECATED. ALPN does not play well with websockets on some browsers and reverse proxies.
+# To migrate away from using it read: https://jitsi.org/multiplexing-to-bridge-ws-howto
+# This file will be removed at some point and if deployment is still using it, will break.
+#
+stream {
+ upstream web {
+ server 127.0.0.1:4444;
+ }
+ upstream turn {
+ server 127.0.0.1:5349;
+ }
+ # since 1.13.10
+ map $ssl_preread_alpn_protocols $upstream {
+ ~\bh2\b web;
+ ~\bhttp/1\. web;
+ default turn;
+ }
+
+ server {
+ listen 443;
+ listen [::]:443;
+
+ # since 1.11.5
+ ssl_preread on;
+ proxy_pass $upstream;
+
+ # Increase buffer to serve video
+ proxy_buffer_size 10m;
+ }
+}
diff --git a/doc/debian/jitsi-meet/jitsi-meet.example b/doc/debian/jitsi-meet/jitsi-meet.example
new file mode 100644
index 0000000..0ea213d
--- /dev/null
+++ b/doc/debian/jitsi-meet/jitsi-meet.example
@@ -0,0 +1,226 @@
+server_names_hash_bucket_size 64;
+
+types {
+# nginx's default mime.types doesn't include a mapping for wasm or wav.
+ application/wasm wasm;
+ audio/wav wav;
+}
+upstream prosody {
+ zone upstreams 64K;
+ server 127.0.0.1:5280;
+ keepalive 2;
+}
+upstream jvb1 {
+ zone upstreams 64K;
+ server 127.0.0.1:9090;
+ keepalive 2;
+}
+map $arg_vnode $prosody_node {
+ default prosody;
+ v1 v1;
+ v2 v2;
+ v3 v3;
+ v4 v4;
+ v5 v5;
+ v6 v6;
+ v7 v7;
+ v8 v8;
+}
+server {
+ listen 80;
+ listen [::]:80;
+ server_name jitsi-meet.example.com;
+
+ location ^~ /.well-known/acme-challenge/ {
+ default_type "text/plain";
+ root /usr/share/jitsi-meet;
+ }
+ location = /.well-known/acme-challenge/ {
+ return 404;
+ }
+ location / {
+ return 301 https://$host$request_uri;
+ }
+}
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name jitsi-meet.example.com;
+
+ # Mozilla Guideline v5.4, nginx 1.17.7, OpenSSL 1.1.1d, intermediate configuration
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+ ssl_prefer_server_ciphers off;
+
+ ssl_session_timeout 1d;
+ ssl_session_cache shared:SSL:10m; # about 40000 sessions
+ ssl_session_tickets off;
+
+ add_header Strict-Transport-Security "max-age=63072000" always;
+ set $prefix "";
+ set $custom_index "";
+ set $config_js_location /etc/jitsi/meet/jitsi-meet.example.com-config.js;
+
+ ssl_certificate /etc/jitsi/meet/jitsi-meet.example.com.crt;
+ ssl_certificate_key /etc/jitsi/meet/jitsi-meet.example.com.key;
+
+ root /usr/share/jitsi-meet;
+
+ # ssi on with javascript for multidomain variables in config.js
+ ssi on;
+ ssi_types application/x-javascript application/javascript;
+
+ index index.html index.htm;
+ error_page 404 /static/404.html;
+
+ gzip on;
+ gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
+ gzip_vary on;
+ gzip_proxied no-cache no-store private expired auth;
+ gzip_min_length 512;
+
+ include /etc/jitsi/meet/jaas/*.conf;
+
+ location = /config.js {
+ alias $config_js_location;
+ }
+
+ location = /external_api.js {
+ alias /usr/share/jitsi-meet/libs/external_api.min.js;
+ }
+
+ location = /_api/room-info {
+ proxy_pass http://prosody/room-info?prefix=$prefix&$args;
+ proxy_http_version 1.1;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header Host $http_host;
+ }
+
+ location ~ ^/_api/public/(.*)$ {
+ autoindex off;
+ alias /etc/jitsi/meet/public/$1;
+ }
+
+ # ensure all static content can always be found first
+ location ~ ^/(libs|css|static|images|fonts|lang|sounds|.well-known)/(.*)$
+ {
+ add_header 'Access-Control-Allow-Origin' '*';
+ alias /usr/share/jitsi-meet/$1/$2;
+
+ # cache all versioned files
+ if ($arg_v) {
+ expires 1y;
+ }
+ }
+
+ # BOSH
+ location = /http-bind {
+ proxy_pass http://$prosody_node/http-bind?prefix=$prefix&$args;
+ proxy_http_version 1.1;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header Host $http_host;
+ proxy_set_header Connection "";
+ }
+
+ # xmpp websockets
+ location = /xmpp-websocket {
+ proxy_pass http://$prosody_node/xmpp-websocket?prefix=$prefix&$args;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $http_host;
+ tcp_nodelay on;
+ }
+
+ # colibri (JVB) websockets for jvb1
+ location ~ ^/colibri-ws/default-id/(.*) {
+ proxy_pass http://jvb1/colibri-ws/default-id/$1$is_args$args;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ tcp_nodelay on;
+ }
+
+ # load test minimal client, uncomment when used
+ #location ~ ^/_load-test/([^/?&:'"]+)$ {
+ # rewrite ^/_load-test/(.*)$ /load-test/index.html break;
+ #}
+ #location ~ ^/_load-test/libs/(.*)$ {
+ # add_header 'Access-Control-Allow-Origin' '*';
+ # alias /usr/share/jitsi-meet/load-test/libs/$1;
+ #}
+
+ location = /_unlock {
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains';
+ add_header "Cache-Control" "no-cache, no-store";
+ }
+
+ location ~ ^/conference-request/v1(\/.*)?$ {
+ proxy_pass http://127.0.0.1:8888/conference-request/v1$1;
+ add_header "Cache-Control" "no-cache, no-store";
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Content-Type';
+ }
+ location ~ ^/([^/?&:'"]+)/conference-request/v1(\/.*)?$ {
+ rewrite ^/([^/?&:'"]+)/conference-request/v1(\/.*)?$ /conference-request/v1$2;
+ }
+
+ location ~ ^/([^/?&:'"]+)$ {
+ set $roomname "$1";
+ try_files $uri @root_path;
+ }
+
+ location @root_path {
+ rewrite ^/(.*)$ /$custom_index break;
+ }
+
+ location ~ ^/([^/?&:'"]+)/config.js$
+ {
+ set $subdomain "$1.";
+ set $subdir "$1/";
+
+ alias $config_js_location;
+ }
+
+ # Matches /(TENANT)/pwa-worker.js or /(TENANT)/manifest.json to rewrite to / and look for file
+ location ~ ^/([^/?&:'"]+)/(pwa-worker.js|manifest.json)$ {
+ set $subdomain "$1.";
+ set $subdir "$1/";
+ rewrite ^/([^/?&:'"]+)/(pwa-worker.js|manifest.json)$ /$2;
+ }
+
+ # BOSH for subdomains
+ location ~ ^/([^/?&:'"]+)/http-bind {
+ set $subdomain "$1.";
+ set $subdir "$1/";
+ set $prefix "$1";
+
+ rewrite ^/(.*)$ /http-bind;
+ }
+
+ # websockets for subdomains
+ location ~ ^/([^/?&:'"]+)/xmpp-websocket {
+ set $subdomain "$1.";
+ set $subdir "$1/";
+ set $prefix "$1";
+
+ rewrite ^/(.*)$ /xmpp-websocket;
+ }
+
+ location ~ ^/([^/?&:'"]+)/_api/room-info {
+ set $subdomain "$1.";
+ set $subdir "$1/";
+ set $prefix "$1";
+
+ rewrite ^/(.*)$ /_api/room-info;
+ }
+
+ # Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
+ location ~ ^/([^/?&:'"]+)/(.*)$ {
+ set $subdomain "$1.";
+ set $subdir "$1/";
+ rewrite ^/([^/?&:'"]+)/(.*)$ /$2;
+ }
+}
diff --git a/doc/debian/jitsi-meet/jitsi-meet.example-apache b/doc/debian/jitsi-meet/jitsi-meet.example-apache
new file mode 100644
index 0000000..86027db
--- /dev/null
+++ b/doc/debian/jitsi-meet/jitsi-meet.example-apache
@@ -0,0 +1,57 @@
+
+
+ ServerName jitsi-meet.example.com
+ Redirect permanent / https://jitsi-meet.example.com/
+
+
+
+ ServerName jitsi-meet.example.com
+
+ # enable HTTP/2, if available
+ Protocols h2 http/1.1
+
+ SSLEngine on
+ SSLProxyEngine on
+ SSLCertificateFile /etc/jitsi/meet/jitsi-meet.example.com.crt
+ SSLCertificateKeyFile /etc/jitsi/meet/jitsi-meet.example.com.key
+
+ Header always set Strict-Transport-Security "max-age=63072000"
+
+ DocumentRoot "/usr/share/jitsi-meet"
+
+ Options Indexes MultiViews Includes FollowSymLinks
+ AddOutputFilter Includes html
+ AllowOverride All
+ Order allow,deny
+ Allow from all
+
+
+ ErrorDocument 404 /static/404.html
+
+ Alias "/config.js" "/etc/jitsi/meet/jitsi-meet.example.com-config.js"
+
+ Require all granted
+
+
+ Alias "/external_api.js" "/usr/share/jitsi-meet/libs/external_api.min.js"
+
+ Require all granted
+
+
+ ProxyPreserveHost on
+ ProxyPass /http-bind http://localhost:5280/http-bind
+ ProxyPassReverse /http-bind http://localhost:5280/http-bind
+ ProxyPass /xmpp-websocket ws://localhost:5280/xmpp-websocket
+ ProxyPassReverse /xmpp-websocket ws://localhost:5280/xmpp-websocket
+ ProxyPass /colibri-ws/default-id ws://localhost:9090/colibri-ws/default-id
+ ProxyPassReverse /colibri-ws/default-id ws://localhost:9090/colibri-ws/default-id
+
+ RewriteEngine on
+ RewriteRule ^/([a-zA-Z0-9]+)$ /index.html
+
+
+# Mozilla Guideline v5.4, Apache 2.4.41, OpenSSL 1.1.1d, intermediate configuration, no OCSP
+SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
+SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
+SSLHonorCipherOrder off
+SSLSessionTickets off
diff --git a/doc/examples/api.html b/doc/examples/api.html
new file mode 100644
index 0000000..8871d3f
--- /dev/null
+++ b/doc/examples/api.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/jaas/8x8.vc-config.js b/doc/jaas/8x8.vc-config.js
new file mode 100644
index 0000000..47e8a8f
--- /dev/null
+++ b/doc/jaas/8x8.vc-config.js
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
+
+