mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-10 11:08:42 +00:00
Compare commits
86 Commits
4667
...
react-film
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
295b3d9afc | ||
|
|
c11a94f7d7 | ||
|
|
bfd093b0ba | ||
|
|
861935c9d7 | ||
|
|
8fcaea9e3d | ||
|
|
e0aab11f98 | ||
|
|
946339a52e | ||
|
|
f71e8a9982 | ||
|
|
af6080f173 | ||
|
|
b1080340ec | ||
|
|
684d121159 | ||
|
|
6740b0861e | ||
|
|
65c56669c4 | ||
|
|
2cd43ba2e4 | ||
|
|
fec2641730 | ||
|
|
04ee423257 | ||
|
|
460e137ee4 | ||
|
|
1a789130a3 | ||
|
|
16b00dc2af | ||
|
|
c3a41b8cf3 | ||
|
|
f4d0ec1bb4 | ||
|
|
ef6b641802 | ||
|
|
579acbc570 | ||
|
|
bca9a12df1 | ||
|
|
a57b967f2e | ||
|
|
299927fcad | ||
|
|
7dc899ace1 | ||
|
|
a6c6cd6c56 | ||
|
|
7dc45c28a2 | ||
|
|
a215f9706a | ||
|
|
4beca0d5dd | ||
|
|
bfc4b2ac6f | ||
|
|
53bdaa7928 | ||
|
|
ba18b12024 | ||
|
|
a1438f1f21 | ||
|
|
c856c20513 | ||
|
|
cf92c964b4 | ||
|
|
e69529867e | ||
|
|
fd313c1af7 | ||
|
|
73c3feb8fa | ||
|
|
67a01364d3 | ||
|
|
7a64bf006e | ||
|
|
e5ea96fd4c | ||
|
|
c8ad04d0ff | ||
|
|
c56afde00c | ||
|
|
9ed1969f7e | ||
|
|
12680c35ca | ||
|
|
0138f23755 | ||
|
|
16d88a288f | ||
|
|
210c4857fd | ||
|
|
dca96f25f3 | ||
|
|
b69e93a900 | ||
|
|
d2568b874b | ||
|
|
20c6115c38 | ||
|
|
68b8ee5961 | ||
|
|
9895a04609 | ||
|
|
87f688dc8f | ||
|
|
c58657c759 | ||
|
|
687106818a | ||
|
|
f228b4ecc1 | ||
|
|
8582b25d28 | ||
|
|
9418dbc2b1 | ||
|
|
19bf027b8b | ||
|
|
c370c05701 | ||
|
|
f034f179ff | ||
|
|
b94b18770c | ||
|
|
3f93726c41 | ||
|
|
af8072d9d2 | ||
|
|
45f4643469 | ||
|
|
745879c447 | ||
|
|
4aab5e2054 | ||
|
|
69971a0e90 | ||
|
|
7c90f75ec9 | ||
|
|
bf714c1c8b | ||
|
|
8414e9d99f | ||
|
|
dcaad41e69 | ||
|
|
63f0166f75 | ||
|
|
79f3756d33 | ||
|
|
5ac30262a5 | ||
|
|
09315fa653 | ||
|
|
5f0dd903f6 | ||
|
|
ef7d425859 | ||
|
|
a9bb8e5e81 | ||
|
|
8cf4e15b23 | ||
|
|
db84889143 | ||
|
|
6ca3c6e43a |
@@ -6,6 +6,8 @@ build/*
|
||||
flow-typed/*
|
||||
libs/*
|
||||
|
||||
react/features/stream-effects/blur/vendor/*
|
||||
|
||||
# ESLint will by default ignore its own configuration file. However, there does
|
||||
# not seem to be a reason why we will want to risk being inconsistent with our
|
||||
# remaining JavaScript source code.
|
||||
|
||||
23
Makefile
23
Makefile
@@ -5,6 +5,8 @@ LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
|
||||
LIBFLAC_DIR = node_modules/libflacjs/dist/min/
|
||||
OLM_DIR = node_modules/olm
|
||||
RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist/
|
||||
TFLITE_WASM = react/features/stream-effects/blur/vendor/tflite
|
||||
MEET_MODELS_DIR = react/features/stream-effects/blur/vendor/models/
|
||||
NODE_SASS = ./node_modules/.bin/sass
|
||||
NPM = npm
|
||||
OUTPUT_DIR = .
|
||||
@@ -16,14 +18,17 @@ WEBPACK_DEV_SERVER = ./node_modules/.bin/webpack-dev-server
|
||||
|
||||
all: compile deploy clean
|
||||
|
||||
compile:
|
||||
compile: compile-load-test
|
||||
$(WEBPACK) -p
|
||||
|
||||
compile-load-test:
|
||||
${NPM} install --prefix resources/load-test && ${NPM} run build --prefix resources/load-test
|
||||
|
||||
clean:
|
||||
rm -fr $(BUILD_DIR)
|
||||
|
||||
.NOTPARALLEL:
|
||||
deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-olm deploy-css deploy-local
|
||||
deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-tflite deploy-meet-models deploy-lib-jitsi-meet deploy-libflac deploy-olm deploy-css deploy-local
|
||||
|
||||
deploy-init:
|
||||
rm -fr $(DEPLOY_DIR)
|
||||
@@ -39,8 +44,6 @@ deploy-appbundle:
|
||||
$(BUILD_DIR)/external_api.min.map \
|
||||
$(BUILD_DIR)/flacEncodeWorker.min.js \
|
||||
$(BUILD_DIR)/flacEncodeWorker.min.map \
|
||||
$(BUILD_DIR)/device_selection_popup_bundle.min.js \
|
||||
$(BUILD_DIR)/device_selection_popup_bundle.min.map \
|
||||
$(BUILD_DIR)/dial_in_info_bundle.min.js \
|
||||
$(BUILD_DIR)/dial_in_info_bundle.min.map \
|
||||
$(BUILD_DIR)/alwaysontop.min.js \
|
||||
@@ -81,6 +84,16 @@ deploy-rnnoise-binary:
|
||||
$(RNNOISE_WASM_DIR)/rnnoise.wasm \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-tflite:
|
||||
cp \
|
||||
$(TFLITE_WASM)/*.wasm \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-meet-models:
|
||||
cp \
|
||||
$(MEET_MODELS_DIR)/*.tflite \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-css:
|
||||
$(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \
|
||||
$(CLEANCSS) --skip-rebase $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
|
||||
@@ -90,7 +103,7 @@ deploy-local:
|
||||
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
|
||||
|
||||
.NOTPARALLEL:
|
||||
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-olm
|
||||
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-tflite deploy-meet-models deploy-lib-jitsi-meet deploy-libflac deploy-olm
|
||||
$(WEBPACK_DEV_SERVER) --detect-circular-deps
|
||||
|
||||
source-package:
|
||||
|
||||
@@ -6,6 +6,8 @@ The Jitsi Meet client runs in your browser, without installing anything else on
|
||||
|
||||
Jitsi Meet allows very efficient collaboration. Users can stream their desktop or only some windows. It also supports shared document editing with Etherpad.
|
||||
|
||||
**NOTE:** If you are looking for Jitsi as a Service (JaaS) please start [here](https://jaas.8x8.vc).
|
||||
|
||||
## Installation
|
||||
|
||||
On the client side, no installation is necessary. You just point your browser to the URL of your deployment. This section is about installing a Jitsi Meet suite on your server and hosting your own conferencing service.
|
||||
|
||||
@@ -26,4 +26,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
appVersion=21.0.0
|
||||
sdkVersion=3.0.0
|
||||
sdkVersion=3.1.0
|
||||
|
||||
@@ -47,6 +47,7 @@ dependencies {
|
||||
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
|
||||
if (rootProject.ext.libreBuild) {
|
||||
implementation(project(':react-native-device-info')) {
|
||||
|
||||
@@ -61,7 +61,12 @@ public class BroadcastAction {
|
||||
enum Type {
|
||||
SET_AUDIO_MUTED("org.jitsi.meet.SET_AUDIO_MUTED"),
|
||||
HANG_UP("org.jitsi.meet.HANG_UP"),
|
||||
SEND_ENDPOINT_TEXT_MESSAGE("org.jitsi.meet.SEND_ENDPOINT_TEXT_MESSAGE");
|
||||
SEND_ENDPOINT_TEXT_MESSAGE("org.jitsi.meet.SEND_ENDPOINT_TEXT_MESSAGE"),
|
||||
TOGGLE_SCREEN_SHARE("org.jitsi.meet.TOGGLE_SCREEN_SHARE"),
|
||||
RETRIEVE_PARTICIPANTS_INFO("org.jitsi.meet.RETRIEVE_PARTICIPANTS_INFO"),
|
||||
OPEN_CHAT("org.jitsi.meet.OPEN_CHAT"),
|
||||
CLOSE_CHAT("org.jitsi.meet.CLOSE_CHAT"),
|
||||
SEND_CHAT_MESSAGE("org.jitsi.meet.SEND_CHAT_MESSAGE");
|
||||
|
||||
private final String action;
|
||||
|
||||
|
||||
@@ -81,7 +81,11 @@ public class BroadcastEvent {
|
||||
AUDIO_MUTED_CHANGED("org.jitsi.meet.AUDIO_MUTED_CHANGED"),
|
||||
PARTICIPANT_JOINED("org.jitsi.meet.PARTICIPANT_JOINED"),
|
||||
PARTICIPANT_LEFT("org.jitsi.meet.PARTICIPANT_LEFT"),
|
||||
ENDPOINT_TEXT_MESSAGE_RECEIVED("org.jitsi.meet.ENDPOINT_TEXT_MESSAGE_RECEIVED");
|
||||
ENDPOINT_TEXT_MESSAGE_RECEIVED("org.jitsi.meet.ENDPOINT_TEXT_MESSAGE_RECEIVED"),
|
||||
SCREEN_SHARE_TOGGLED("org.jitsi.meet.SCREEN_SHARE_TOGGLED"),
|
||||
PARTICIPANTS_INFO_RETRIEVED("org.jitsi.meet.PARTICIPANTS_INFO_RETRIEVED"),
|
||||
CHAT_MESSAGE_RECEIVED("org.jitsi.meet.CHAT_MESSAGE_RECEIVED"),
|
||||
CHAT_TOGGLED("org.jitsi.meet.CHAT_TOGGLED");
|
||||
|
||||
private static final String CONFERENCE_WILL_JOIN_NAME = "CONFERENCE_WILL_JOIN";
|
||||
private static final String CONFERENCE_JOINED_NAME = "CONFERENCE_JOINED";
|
||||
@@ -90,6 +94,10 @@ public class BroadcastEvent {
|
||||
private static final String PARTICIPANT_JOINED_NAME = "PARTICIPANT_JOINED";
|
||||
private static final String PARTICIPANT_LEFT_NAME = "PARTICIPANT_LEFT";
|
||||
private static final String ENDPOINT_TEXT_MESSAGE_RECEIVED_NAME = "ENDPOINT_TEXT_MESSAGE_RECEIVED";
|
||||
private static final String SCREEN_SHARE_TOGGLED_NAME = "SCREEN_SHARE_TOGGLED";
|
||||
private static final String PARTICIPANTS_INFO_RETRIEVED_NAME = "PARTICIPANTS_INFO_RETRIEVED";
|
||||
private static final String CHAT_MESSAGE_RECEIVED_NAME = "CHAT_MESSAGE_RECEIVED";
|
||||
private static final String CHAT_TOGGLED_NAME = "CHAT_TOGGLED";
|
||||
|
||||
private final String action;
|
||||
|
||||
@@ -126,6 +134,14 @@ public class BroadcastEvent {
|
||||
return PARTICIPANT_LEFT;
|
||||
case ENDPOINT_TEXT_MESSAGE_RECEIVED_NAME:
|
||||
return ENDPOINT_TEXT_MESSAGE_RECEIVED;
|
||||
case SCREEN_SHARE_TOGGLED_NAME:
|
||||
return SCREEN_SHARE_TOGGLED;
|
||||
case PARTICIPANTS_INFO_RETRIEVED_NAME:
|
||||
return PARTICIPANTS_INFO_RETRIEVED;
|
||||
case CHAT_MESSAGE_RECEIVED_NAME:
|
||||
return CHAT_MESSAGE_RECEIVED;
|
||||
case CHAT_TOGGLED_NAME:
|
||||
return CHAT_TOGGLED;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -19,4 +19,25 @@ public class BroadcastIntentHelper {
|
||||
intent.putExtra("message", message);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildToggleScreenShareIntent() {
|
||||
return new Intent(BroadcastAction.Type.TOGGLE_SCREEN_SHARE.getAction());
|
||||
}
|
||||
|
||||
public static Intent buildOpenChatIntent(String participantId) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.OPEN_CHAT.getAction());
|
||||
intent.putExtra("to", participantId);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildCloseChatIntent() {
|
||||
return new Intent(BroadcastAction.Type.CLOSE_CHAT.getAction());
|
||||
}
|
||||
|
||||
public static Intent buildSendChatMessageIntent(String participantId, String message) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.SEND_CHAT_MESSAGE.getAction());
|
||||
intent.putExtra("to", participantId);
|
||||
intent.putExtra("message", message);
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@ public class BroadcastReceiver extends android.content.BroadcastReceiver {
|
||||
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
|
||||
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
|
||||
intentFilter.addAction(BroadcastAction.Type.HANG_UP.getAction());
|
||||
intentFilter.addAction(BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
|
||||
|
||||
for (BroadcastAction.Type type : BroadcastAction.Type.values()) {
|
||||
intentFilter.addAction(type.getAction());
|
||||
}
|
||||
|
||||
localBroadcastManager.registerReceiver(this, intentFilter);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ class ExternalAPIModule
|
||||
|
||||
broadcastEmitter = new BroadcastEmitter(reactContext);
|
||||
broadcastReceiver = new BroadcastReceiver(reactContext);
|
||||
|
||||
ParticipantsService.init(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +80,11 @@ class ExternalAPIModule
|
||||
constants.put("SET_AUDIO_MUTED", BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
|
||||
constants.put("HANG_UP", BroadcastAction.Type.HANG_UP.getAction());
|
||||
constants.put("SEND_ENDPOINT_TEXT_MESSAGE", BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
|
||||
constants.put("TOGGLE_SCREEN_SHARE", BroadcastAction.Type.TOGGLE_SCREEN_SHARE.getAction());
|
||||
constants.put("RETRIEVE_PARTICIPANTS_INFO", BroadcastAction.Type.RETRIEVE_PARTICIPANTS_INFO.getAction());
|
||||
constants.put("OPEN_CHAT", BroadcastAction.Type.OPEN_CHAT.getAction());
|
||||
constants.put("CLOSE_CHAT", BroadcastAction.Type.CLOSE_CHAT.getAction());
|
||||
constants.put("SEND_CHAT_MESSAGE", BroadcastAction.Type.SEND_CHAT_MESSAGE.getAction());
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
@@ -266,12 +266,10 @@ public class JitsiMeetActivity extends FragmentActivity
|
||||
|
||||
private void registerForBroadcastMessages() {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(BroadcastEvent.Type.CONFERENCE_JOINED.getAction());
|
||||
intentFilter.addAction(BroadcastEvent.Type.CONFERENCE_WILL_JOIN.getAction());
|
||||
intentFilter.addAction(BroadcastEvent.Type.CONFERENCE_TERMINATED.getAction());
|
||||
intentFilter.addAction(BroadcastEvent.Type.PARTICIPANT_JOINED.getAction());
|
||||
intentFilter.addAction(BroadcastEvent.Type.PARTICIPANT_LEFT.getAction());
|
||||
intentFilter.addAction(BroadcastEvent.Type.ENDPOINT_TEXT_MESSAGE_RECEIVED.getAction());
|
||||
|
||||
for (BroadcastEvent.Type type : BroadcastEvent.Type.values()) {
|
||||
intentFilter.addAction(type.getAction());
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
@@ -96,11 +97,11 @@ class OngoingNotification {
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
|
||||
|
||||
NotificationCompat.Action hangupAction = createAction(context, JitsiMeetOngoingConferenceService.Action.HANGUP, "Hang up");
|
||||
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;
|
||||
String toggleAudioTitle = isMuted ? "Unmute" : "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);
|
||||
@@ -109,11 +110,12 @@ class OngoingNotification {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static NotificationCompat.Action createAction(Context context, JitsiMeetOngoingConferenceService.Action action, String title) {
|
||||
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_UPDATE_CURRENT);
|
||||
String title = context.getString(titleId);
|
||||
return new NotificationCompat.Action(0, title, pendingIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String, WeakReference<ParticipantsInfoCallback>> 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<ParticipantInfo> participantInfoList = new Gson().fromJson(
|
||||
event.getData().get("participantsInfo").toString(),
|
||||
new TypeToken<ArrayList<ParticipantInfo>>() {
|
||||
}.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<ParticipantInfo> participantInfoList);
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,7 @@
|
||||
<string name="dropbox_app_key"></string>
|
||||
<string name="ongoing_notification_title">Ongoing meeting</string>
|
||||
<string name="ongoing_notification_text">You are currently in a meeting. Tap to return to it.</string>
|
||||
<string name="ongoing_notification_action_hang_up">Hang up</string>
|
||||
<string name="ongoing_notification_action_mute">Mute</string>
|
||||
<string name="ongoing_notification_action_unmute">Unmute</string>
|
||||
</resources>
|
||||
|
||||
1
app.js
1
app.js
@@ -1,7 +1,6 @@
|
||||
/* application specific logic */
|
||||
|
||||
import 'jquery';
|
||||
import 'jquery-contextmenu';
|
||||
import 'jQuery-Impromptu';
|
||||
|
||||
import 'olm';
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
conferenceLeft,
|
||||
conferenceSubjectChanged,
|
||||
conferenceTimestampChanged,
|
||||
conferenceUniqueIdSet,
|
||||
conferenceWillJoin,
|
||||
conferenceWillLeave,
|
||||
dataChannelOpened,
|
||||
@@ -503,6 +504,11 @@ export default {
|
||||
|
||||
let tryCreateLocalTracks;
|
||||
|
||||
// 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 withing 15 seconds it will
|
||||
// probably never resolve.
|
||||
const timeout = browser.isElectron() ? 15000 : 60000;
|
||||
|
||||
// FIXME is there any simpler way to rewrite this spaghetti below ?
|
||||
if (options.startScreenSharing) {
|
||||
tryCreateLocalTracks = this._createDesktopTrack()
|
||||
@@ -511,7 +517,10 @@ export default {
|
||||
return [ desktopStream ];
|
||||
}
|
||||
|
||||
return createLocalTracksF({ devices: [ 'audio' ] }, true)
|
||||
return createLocalTracksF({
|
||||
devices: [ 'audio' ],
|
||||
timeout
|
||||
}, true)
|
||||
.then(([ audioStream ]) =>
|
||||
[ desktopStream, audioStream ])
|
||||
.catch(error => {
|
||||
@@ -525,7 +534,10 @@ export default {
|
||||
errors.screenSharingError = error;
|
||||
|
||||
return requestedAudio
|
||||
? createLocalTracksF({ devices: [ 'audio' ] }, true)
|
||||
? createLocalTracksF({
|
||||
devices: [ 'audio' ],
|
||||
timeout
|
||||
}, true)
|
||||
: [];
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -537,15 +549,33 @@ export default {
|
||||
// Resolve with no tracks
|
||||
tryCreateLocalTracks = Promise.resolve([]);
|
||||
} else {
|
||||
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
|
||||
tryCreateLocalTracks = createLocalTracksF({
|
||||
devices: initialDevices,
|
||||
timeout
|
||||
}, true)
|
||||
.catch(err => {
|
||||
if (requestedAudio && requestedVideo) {
|
||||
|
||||
// Try audio only...
|
||||
errors.audioAndVideoError = err;
|
||||
|
||||
if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
|
||||
// In this case we expect that the permission prompt is still visible. There is no point of
|
||||
// executing GUM with different source. Also at the time of writting the following
|
||||
// inconsistency have been noticed in some browsers - if the permissions prompt is visible
|
||||
// and another GUM is executed the prompt does not change its content but if the user
|
||||
// clicks allow the user action isassociated with the latest GUM call.
|
||||
errors.audioOnlyError = err;
|
||||
errors.videoOnlyError = err;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
createLocalTracksF({ devices: [ 'audio' ] }, true));
|
||||
createLocalTracksF({
|
||||
devices: [ 'audio' ],
|
||||
timeout
|
||||
}, true));
|
||||
} else if (requestedAudio && !requestedVideo) {
|
||||
errors.audioOnlyError = err;
|
||||
|
||||
@@ -566,7 +596,10 @@ export default {
|
||||
|
||||
// Try video only...
|
||||
return requestedVideo
|
||||
? createLocalTracksF({ devices: [ 'video' ] }, true)
|
||||
? createLocalTracksF({
|
||||
devices: [ 'video' ],
|
||||
timeout
|
||||
}, true)
|
||||
: [];
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -1332,9 +1365,6 @@ export default {
|
||||
.then(() => {
|
||||
this.localVideo = newTrack;
|
||||
this._setSharingScreen(newTrack);
|
||||
if (newTrack) {
|
||||
APP.UI.addLocalVideoStream(newTrack);
|
||||
}
|
||||
this.setVideoMuteStatus(this.isLocalVideoMuted());
|
||||
})
|
||||
.then(resolve)
|
||||
@@ -1865,6 +1895,10 @@ export default {
|
||||
APP.store.dispatch(conferenceLeft(room, ...args));
|
||||
});
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.CONFERENCE_UNIQUE_ID_SET,
|
||||
(...args) => APP.store.dispatch(conferenceUniqueIdSet(room, ...args)));
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
|
||||
(authEnabled, authLogin) =>
|
||||
@@ -2248,7 +2282,7 @@ export default {
|
||||
return this.useAudioStream(stream);
|
||||
})
|
||||
.then(() => {
|
||||
if (hasDefaultMicChanged) {
|
||||
if (this.localAudio && hasDefaultMicChanged) {
|
||||
// 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.
|
||||
@@ -2290,7 +2324,11 @@ export default {
|
||||
// There is no guarantee another event will trigger the update
|
||||
// immediately and in all situations, for example because a remote
|
||||
// participant is having connection trouble so no status changes.
|
||||
APP.UI.updateAllVideos();
|
||||
const displayedUserId = APP.UI.getLargeVideoID();
|
||||
|
||||
if (displayedUserId) {
|
||||
APP.UI.updateLargeVideo(displayedUserId, true);
|
||||
}
|
||||
});
|
||||
|
||||
APP.UI.addListener(
|
||||
@@ -2588,7 +2626,7 @@ export default {
|
||||
// Use the new stream or null if we failed to obtain it.
|
||||
return useStream(tracks.find(track => track.getType() === mediaType) || null)
|
||||
.then(() => {
|
||||
if (hasDefaultMicChanged) {
|
||||
if (this.localAudio && hasDefaultMicChanged) {
|
||||
// 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.
|
||||
|
||||
30
config.js
30
config.js
@@ -240,6 +240,12 @@ var config = {
|
||||
// 90: 2
|
||||
// },
|
||||
|
||||
// Provides a way to translate the legacy bridge signaling messages, 'LastNChangedEvent',
|
||||
// 'SelectedEndpointsChangedEvent' and 'ReceiverVideoConstraint' into the new 'ReceiverVideoConstraints' message
|
||||
// that invokes the new bandwidth allocation algorithm in the bridge which is described here
|
||||
// - https://github.com/jitsi/jitsi-videobridge/blob/master/doc/allocation.md.
|
||||
// useNewBandwidthAllocationStrategy: false,
|
||||
|
||||
// Specify the settings for video quality optimizations on the client.
|
||||
// videoQuality: {
|
||||
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified
|
||||
@@ -261,9 +267,16 @@ var config = {
|
||||
// // the available bandwidth calculated by the browser, but it will be capped by the values specified here.
|
||||
// // This is currently not implemented on app based clients on mobile.
|
||||
// maxBitratesVideo: {
|
||||
// low: 200000,
|
||||
// standard: 500000,
|
||||
// high: 1500000
|
||||
// VP8 : {
|
||||
// low: 200000,
|
||||
// standard: 500000,
|
||||
// high: 1500000
|
||||
// },
|
||||
// VP9: {
|
||||
// low: 100000,
|
||||
// standard: 300000,
|
||||
// high: 1200000
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// // The options can be used to override default thresholds of video thumbnail heights corresponding to
|
||||
@@ -318,6 +331,11 @@ var config = {
|
||||
// TCC sequence numbers starting from 0.
|
||||
// enableIceRestart: false,
|
||||
|
||||
// Enables forced reload of the client when the call is migrated as a result of
|
||||
// the bridge going down. Currently enabled by default as call migration through
|
||||
// session-terminate is causing siganling issues when Octo is enabled.
|
||||
// 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)
|
||||
@@ -326,6 +344,9 @@ var config = {
|
||||
// UI
|
||||
//
|
||||
|
||||
// Disables responsive tiles.
|
||||
// disableResponsiveTiles: false,
|
||||
|
||||
// Hides lobby button
|
||||
// hideLobbyButton: false,
|
||||
|
||||
@@ -696,6 +717,8 @@ var config = {
|
||||
forceTurnRelay
|
||||
hiddenDomain
|
||||
ignoreStartMuted
|
||||
websocketKeepAlive
|
||||
websocketKeepAliveUrl
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -720,6 +743,7 @@ var config = {
|
||||
// 'dialog.reservationError',
|
||||
// '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.transcribing', // transcribing notifications (pending, off)
|
||||
// 'dialOut.statusMessage', // shown when dial out status is updated.
|
||||
|
||||
@@ -94,6 +94,10 @@ function connect(id, password, roomName) {
|
||||
// in future). It's included for the time being for Jitsi Meet and lib-jitsi-meet versions interoperability.
|
||||
connectionConfig.serviceUrl = connectionConfig.bosh = serviceUrl;
|
||||
|
||||
if (connectionConfig.websocketKeepAliveUrl) {
|
||||
connectionConfig.websocketKeepAliveUrl += `?room=${roomName}`;
|
||||
}
|
||||
|
||||
const connection = new JitsiMeetJS.JitsiConnection(null, jwt, connectionConfig);
|
||||
|
||||
if (config.iAmRecorder) {
|
||||
|
||||
@@ -2,65 +2,58 @@
|
||||
* Move the @atlaskit/flag container up a little bit so it does not cover the
|
||||
* toolbar with the first notification.
|
||||
*/
|
||||
.jIMojv{
|
||||
.atlaskit-portal > #notifications-container {
|
||||
bottom: calc(#{$newToolbarSizeWithPadding}) !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the slide-in animation for @atlaskit/flag due to the animation
|
||||
* repeating for each queued flag once it becomes the top flag.
|
||||
*/
|
||||
.mIBKA:first-child {
|
||||
animation: cbfRuT 0s !important;
|
||||
-webkit-animation: cbfRuT 0s !important;
|
||||
}
|
||||
|
||||
.modal-dialog-form {
|
||||
/**
|
||||
* Update the @atlaskit/dropdown-menu trigger wrapper to make sure it looks
|
||||
* click-able.
|
||||
*/
|
||||
.cjJUnw {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override @atlaskit/dropdown-menu styling when in a modal because the
|
||||
* dropdown backgrounds clash with the modal backgrounds.
|
||||
*/
|
||||
.cksvax[data-role=droplistContent] {
|
||||
border: 1px solid #455166;
|
||||
.dropdown-menu div[style*="transform"] {
|
||||
outline: 1px solid #455166;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override @atlaskit/modal-dialog header styling
|
||||
*/
|
||||
.atlaskit-portal [role="dialog"] header {
|
||||
.jitsi-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jitsi-icon svg {
|
||||
fill: #B8C7E0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make header close button more easily tappable on mobile.
|
||||
*/
|
||||
.mobile-browser .atlaskit-portal [role="dialog"] header .jitsi-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background: #2a3a4b;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override @atlaskit/theme styling for the top toolbar so it displays over
|
||||
* the video thumbnail while obscuring as little as possible.
|
||||
*/
|
||||
.videocontainer .tOoji {
|
||||
.videocontainer__toptoolbar > div > div {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override @atlaskit/InlineDialog styling for the overflowmenu so it displays
|
||||
* with the correct height.
|
||||
*/
|
||||
.toolbox-button-wth-dialog .eYJELv {
|
||||
max-height: initial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override @atlaskit/InlineDialog styling for the overflowmenu so it displays
|
||||
* a scrollable list of elements at small screen widths.
|
||||
*/
|
||||
.sc-eNQAEJ {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep overflow menu within screen vertical bounds and make it scrollable.
|
||||
*/
|
||||
.toolbox-button-wth-dialog .sc-ckVGcZ.fdAqDG > :first-child {
|
||||
max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 16px);
|
||||
.toolbox-button-wth-dialog > div:nth-child(2) {
|
||||
max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 46px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -143,8 +143,8 @@
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div > div:nth-child(2) > div > div {
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div:nth-child(2) {
|
||||
outline: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -45,20 +45,12 @@ body {
|
||||
* pad the modal container in order for the modals to be centered
|
||||
* while also taking the chat size into consideration.
|
||||
*/
|
||||
@media (min-width: 480px + $sidebarWidth) {
|
||||
.shift-right [class^="Modal__FillScreen"] {
|
||||
@media (min-width: 581px) {
|
||||
.shift-right .atlaskit-portal > div:not(.Tooltip) {
|
||||
padding-left: $sidebarWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarly, we offset the notifications when the chat is open by
|
||||
* padding the container.
|
||||
*/
|
||||
.shift-right [class^="styledFlagGroup-"] {
|
||||
padding-left: $sidebarWidth;
|
||||
}
|
||||
|
||||
.jitsi-icon svg {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
@@ -103,25 +103,22 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
|
||||
.chat-close {
|
||||
align-items: center;
|
||||
bottom: 8px;
|
||||
color: white;
|
||||
.jitsi-icon {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 18px;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
line-height: 15px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.jitsi-icon > svg {
|
||||
fill: #A4B8D1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +181,10 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 580px) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chatmessage {
|
||||
@@ -389,6 +390,7 @@
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 16px 16px 24px;
|
||||
width: calc(100% - 32px);
|
||||
box-sizing: border-box;
|
||||
@@ -397,8 +399,11 @@
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
|
||||
.jitsi-icon > svg {
|
||||
.jitsi-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jitsi-icon > svg {
|
||||
fill: #A4B8D1;
|
||||
}
|
||||
}
|
||||
@@ -407,3 +412,15 @@
|
||||
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: #2a3a4b;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
}
|
||||
|
||||
// Override @Atlaskit/inline-dialog styles
|
||||
.cpick-container > div > div:nth-child(2) > div > div {
|
||||
.cpick-container > div:nth-child(2) {
|
||||
outline: none;
|
||||
padding: 8px 0 0 0;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.drawer-menu {
|
||||
padding: 12px 16px;
|
||||
padding: 0 16px;
|
||||
max-height: 50vh;
|
||||
background: #242528;
|
||||
border-radius: 16px 16px 0 0;
|
||||
@@ -24,12 +24,14 @@
|
||||
height: 44px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
}
|
||||
}
|
||||
|
||||
svg, path {
|
||||
fill: #b8c7e0;
|
||||
svg {
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +59,7 @@
|
||||
color: $overflowMenuItemColor;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
@@ -65,16 +67,20 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
color: $overflowMenuItemHoverColor;
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
color: $overflowMenuItemHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
}
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
cursor: initial;
|
||||
@@ -93,15 +99,17 @@
|
||||
}
|
||||
|
||||
.overflow-menu-item-icon {
|
||||
margin-right: 10px;
|
||||
margin-right: 16px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -111,11 +119,13 @@
|
||||
|
||||
svg {
|
||||
fill: #B8C7E0 !important;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-text {
|
||||
max-width: 150px;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
transition: right 0.5s;
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
z-index: $labelsZ;
|
||||
|
||||
.circular-label {
|
||||
align-items: center;
|
||||
|
||||
@@ -47,4 +47,5 @@
|
||||
border-radius: 3px;
|
||||
margin: -16px -24px;
|
||||
padding: 16px 24px;
|
||||
z-index: $popoverZ;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,6 @@ ul.popupmenu {
|
||||
margin: -16px -24px;
|
||||
}
|
||||
|
||||
span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
|
||||
span.localvideomenu:hover ul.popupmenu, span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
|
||||
display:block !important;
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
}
|
||||
|
||||
&-dropdown-container {
|
||||
& > div > div:nth-child(2) > div > div {
|
||||
& > div:nth-child(2) {
|
||||
background: #fff;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -143,16 +143,6 @@
|
||||
|
||||
@media only screen and (max-width: $verySmallScreen) {
|
||||
@include very-small-button-size();
|
||||
|
||||
#videoResolutionLabel {
|
||||
display: none;
|
||||
}
|
||||
.vertical-filmstrip .filmstrip {
|
||||
display: none;
|
||||
}
|
||||
.chrome-extension-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.shift-right {
|
||||
@@ -177,23 +167,23 @@
|
||||
}
|
||||
|
||||
@media (min-width: 480px) and (max-width: 580px) {
|
||||
.shift-right [class^="Modal__PositionerAbsolute"] {
|
||||
.shift-right .focus-lock > div > div {
|
||||
@include full-size-modal-positioner();
|
||||
}
|
||||
|
||||
.shift-right [class^="Modal__Dialog-"] {
|
||||
.shift-right .focus-lock [role="dialog"] {
|
||||
@include full-size-modal-dialog();
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 580px) and (max-width: 680px) {
|
||||
.mobile-browser {
|
||||
&.shift-right [class^="Modal__PositionerAbsolute"] {
|
||||
&.shift-right .focus-lock > div > div {
|
||||
@include full-size-modal-positioner();
|
||||
}
|
||||
|
||||
&.shift-right [class^="Modal__Dialog-"] {
|
||||
&.shift-right .focus-lock [role="dialog"] {
|
||||
@include full-size-modal-dialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
@media (min-width: 581px) {
|
||||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-background {
|
||||
@@ -88,31 +90,35 @@
|
||||
margin: 0px 4px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
|
||||
&:hover {
|
||||
background-color: #daebfa;
|
||||
border: 1px solid #daebfa;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: #daebfa;
|
||||
border: 1px solid #daebfa;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.toggled {
|
||||
background: #2a3a4b;
|
||||
border: 1px solid #5e6d7a;
|
||||
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #5e6d7a;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: #5e6d7a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.disabled, .disabled & {
|
||||
cursor: initial;
|
||||
color: #fff;
|
||||
background-color: #a4b8d1;
|
||||
}
|
||||
|
||||
|
||||
svg {
|
||||
fill: #5e6d7a;
|
||||
}
|
||||
@@ -124,9 +130,11 @@
|
||||
border: 1px solid $hangupColor;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&:hover {
|
||||
background-color: $hangupColor;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $hangupColor;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -157,8 +165,9 @@
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
height: 22px;
|
||||
height: 40px;
|
||||
padding: 5px 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
@@ -166,16 +175,20 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
color: $overflowMenuItemHoverColor;
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
color: $overflowMenuItemHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
}
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
cursor: initial;
|
||||
@@ -194,15 +207,17 @@
|
||||
}
|
||||
|
||||
.overflow-menu-item-icon {
|
||||
margin-right: 10px;
|
||||
margin-right: 16px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -212,6 +227,8 @@
|
||||
|
||||
svg {
|
||||
fill: #B8C7E0 !important;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,10 +276,16 @@
|
||||
justify-content: center;
|
||||
width: $newToolbarSize;
|
||||
|
||||
&:hover, &.toggled {
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
|
||||
&.disabled {
|
||||
cursor: initial !important;
|
||||
background-color: #a4b8d1 !important;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
1px 0px 1px rgba(0,0,0,0.3),
|
||||
0px 0px 1px rgba(0,0,0,0.3);
|
||||
transform: translateX(-50%);
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
z-index: $subtitlesZ;
|
||||
|
||||
span {
|
||||
background: black;
|
||||
|
||||
@@ -114,7 +114,10 @@ $zindex1: 1;
|
||||
$zindex2: 2;
|
||||
$zindex3: 3;
|
||||
$toolbarBackgroundZ: 4;
|
||||
$filmstripVideosZ: 5;
|
||||
$labelsZ: 5;
|
||||
$filmstripVideosZ: 6;
|
||||
$subtitlesZ: 7;
|
||||
$popoverZ: 8;
|
||||
$zindex10: 10;
|
||||
$reloadZ: 20;
|
||||
$poweredByZ: 100;
|
||||
@@ -126,7 +129,6 @@ $tooltipsZ: 401;
|
||||
$dropdownMaskZ: 900;
|
||||
$dropdownZ: 901;
|
||||
$centeredVideoLabelZ: 1010;
|
||||
$popoverZ: 1015;
|
||||
$overlayZ: 1016;
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
}
|
||||
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div > div:nth-child(2) > div > div {
|
||||
& > div:nth-child(2) {
|
||||
outline: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -182,10 +182,12 @@
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
&.shift-right {
|
||||
&#largeVideoContainer {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
@media (min-width: 581px) {
|
||||
&.shift-right {
|
||||
&#largeVideoContainer {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,7 +400,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger,
|
||||
.localvideomenu,
|
||||
.remotevideomenu
|
||||
{
|
||||
display: inline-block;
|
||||
@@ -416,6 +420,7 @@
|
||||
cursor: hand;
|
||||
}
|
||||
}
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
margin-top: 7px;
|
||||
}
|
||||
@@ -478,14 +483,6 @@
|
||||
z-index: $reloadZ; /*The reload button should appear on top of the header!*/
|
||||
}
|
||||
|
||||
.audiolevel {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: $zindex0;
|
||||
border-radius:1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#dominantSpeaker {
|
||||
visibility: hidden;
|
||||
width: 300px;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
}
|
||||
|
||||
.horizontal-filmstrip .filmstrip {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 10px 5px;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
0 0 3px $videoThumbnailSelected;
|
||||
}
|
||||
|
||||
.remotevideomenu > .icon-menu {
|
||||
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
||||
0 0 3px $videoThumbnailHovered;
|
||||
|
||||
.remotevideomenu > .icon-menu {
|
||||
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
@@ -42,17 +42,18 @@
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: $filmstripVideosZ;
|
||||
|
||||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
@media (min-width: 581px) {
|
||||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
width: calc(100vw - #{$sidebarWidth});
|
||||
#filmstripRemoteVideos {
|
||||
width: calc(100vw - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +87,7 @@
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
justify-content: center;
|
||||
@@ -100,6 +102,15 @@
|
||||
video {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
|
||||
*/
|
||||
@media only screen and (max-width: 500px) {
|
||||
video {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.has-overflow#filmstripRemoteVideosContainer {
|
||||
|
||||
@@ -22,11 +22,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#remoteConnectionMessage,
|
||||
.watermark {
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The follow styling uses !important to override inline styles set with
|
||||
* javascript.
|
||||
@@ -48,6 +43,7 @@
|
||||
* specifically the various status icons.
|
||||
*/
|
||||
.remotevideomenu,
|
||||
.localvideomenu,
|
||||
.videocontainer__toptoolbar {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
* and tooltips from getting a new location context due to translate3d.
|
||||
*/
|
||||
.connection-indicator,
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger,
|
||||
.indicator-icon-container {
|
||||
transform: translate3d(0, 0, 0);
|
||||
@@ -68,7 +69,9 @@
|
||||
* Move the remote video menu trigger to the bottom left of the video
|
||||
* thumbnail.
|
||||
*/
|
||||
.localvideomenu,
|
||||
.remotevideomenu,
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
@@ -76,6 +79,7 @@
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
margin-bottom: 7px;
|
||||
margin-left: $remoteVideoMenuIconMargin;
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
.device-selector-trigger-text {
|
||||
overflow: hidden;
|
||||
margin-left: 8px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
@@ -2,22 +2,6 @@
|
||||
&-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 16px 16px 24px;
|
||||
width: calc(100% - 32px);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
|
||||
& > div > svg {
|
||||
cursor: pointer;
|
||||
fill: #A4B8D1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-copy {
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
text-align: center;
|
||||
z-index: $zindex2;
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
|
||||
|
||||
&.elevated {
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
@@ -32,8 +28,10 @@
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #278ADF;
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background: #278ADF;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
@@ -47,22 +45,6 @@
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
|
||||
&.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 16px 16px 24px;
|
||||
width: calc(100% - 32px);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
|
||||
& > div > svg {
|
||||
cursor: pointer;
|
||||
fill: #A4B8D1;
|
||||
}
|
||||
}
|
||||
|
||||
&.separator {
|
||||
margin: 24px 0 24px -20px;
|
||||
padding: 0 20px;
|
||||
@@ -112,11 +94,11 @@
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
&:hover > div:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
|
||||
& > :not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
padding: 20px 0px 4px 0px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + svg + span {
|
||||
color: #9FB0CC;
|
||||
}
|
||||
|
||||
.calendar-tab,
|
||||
.more-tab,
|
||||
.profile-edit {
|
||||
|
||||
@@ -111,7 +111,3 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#videoResolutionLabel {
|
||||
z-index: $zindex3 + 1;
|
||||
}
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -33,7 +33,7 @@ Description: Configuration for web serving of Jitsi Meet
|
||||
|
||||
Package: jitsi-meet-prosody
|
||||
Architecture: all
|
||||
Depends: openssl, prosody | prosody-trunk | prosody-0.11
|
||||
Depends: openssl, prosody | prosody-trunk | prosody-0.11, lua-sec
|
||||
Replaces: jitsi-meet-tokens
|
||||
Description: Prosody configuration for Jitsi Meet
|
||||
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
|
||||
|
||||
17
debian/jitsi-meet-web-config.postinst
vendored
17
debian/jitsi-meet-web-config.postinst
vendored
@@ -45,8 +45,9 @@ case "$1" in
|
||||
|
||||
JVB_SERVE="false"
|
||||
# this detect only old installations
|
||||
RET=""
|
||||
db_get jitsi-meet/jvb-serve || true
|
||||
if [ -n "$RET" ] && [ "$RET" = "true" ] ; then
|
||||
if [ "$RET" = "true" ] ; then
|
||||
JVB_SERVE="true"
|
||||
fi
|
||||
|
||||
@@ -69,19 +70,21 @@ case "$1" in
|
||||
FORCE_APACHE="true"
|
||||
fi
|
||||
# In case user enforces apache and if apache is available, unset nginx.
|
||||
db_get jitsi-meet/enforce_apache
|
||||
if [ -n "$RET" ] && [ "$RET" = "true" ] \
|
||||
&& [ "$FORCE_APACHE" = "true" ]; then
|
||||
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"
|
||||
# if first time config ask for certs, or if we are reconfiguring
|
||||
if [ -z "$JVB_HOSTNAME_OLD" ] || [ "$RECONFIGURING" = "true" ] ; then
|
||||
RET=""
|
||||
db_get jitsi-meet/cert-choice
|
||||
CERT_CHOICE="$RET"
|
||||
|
||||
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"
|
||||
@@ -90,6 +93,7 @@ case "$1" in
|
||||
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"
|
||||
@@ -152,12 +156,15 @@ case "$1" in
|
||||
# Removing this value will force nginx or apache to be locally configured
|
||||
JVB_HOSTNAME_OLD=""
|
||||
|
||||
RET=""
|
||||
db_get jitsi-meet/cert-choice
|
||||
CERT_CHOICE="$RET"
|
||||
# Fix certs on upgrade from jetty
|
||||
if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
|
||||
RET=""
|
||||
db_get jitsi-meet/cert-path-key
|
||||
CERT_KEY="$RET"
|
||||
RET=""
|
||||
db_get jitsi-meet/cert-path-crt
|
||||
CERT_CRT="$RET"
|
||||
else
|
||||
@@ -211,7 +218,7 @@ case "$1" in
|
||||
# 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 include
|
||||
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
|
||||
|
||||
2
debian/jitsi-meet-web.install
vendored
2
debian/jitsi-meet-web.install
vendored
@@ -15,3 +15,5 @@ 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/
|
||||
resources/load-test/*.html /usr/share/jitsi-meet/load-test/
|
||||
resources/load-test/libs /usr/share/jitsi-meet/load-test/
|
||||
|
||||
@@ -74,7 +74,7 @@ Component "internal.auth.jitmeet.example.com" "muc"
|
||||
muc_room_default_public_jids = true
|
||||
|
||||
VirtualHost "auth.jitmeet.example.com"
|
||||
authentication = "internal_plain"
|
||||
authentication = "internal_hashed"
|
||||
|
||||
-- Proxy to jicofo's user JID, so that it doesn't have to register as a component.
|
||||
Component "focus.jitmeet.example.com" "client_proxy"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
server_names_hash_bucket_size 64;
|
||||
|
||||
types {
|
||||
# nginx's default mime.types doesn't include a mapping for wasm
|
||||
application/wasm wasm;
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
@@ -96,6 +100,15 @@ server {
|
||||
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 ~ ^/([^/?&:'"]+)$ {
|
||||
try_files $uri @root_path;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
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
|
||||
ProxyPassMatch ^/colibri-ws/default-id ws://localhost:9090
|
||||
|
||||
RewriteEngine on
|
||||
RewriteRule ^/([a-zA-Z0-9]+)$ /index.html
|
||||
|
||||
@@ -142,7 +142,6 @@ pidfile = "/var/run/prosody/prosody.pid"
|
||||
-- server please see http://prosody.im/doc/modules/mod_auth_internal_hashed
|
||||
-- for information about using the hashed backend.
|
||||
|
||||
-- authentication = "internal_plain"
|
||||
authentication = "internal_hashed"
|
||||
|
||||
-- Select the storage backend to use. By default Prosody uses flat files
|
||||
@@ -190,7 +189,7 @@ VirtualHost "auth.jitsi.example.com"
|
||||
key = "/var/lib/prosody/auth.jitsi.example.com.key";
|
||||
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
|
||||
}
|
||||
authentication = "internal_plain"
|
||||
authentication = "internal_hashed"
|
||||
|
||||
------ Components ------
|
||||
-- You can specify components to add hosts that provide special services,
|
||||
|
||||
@@ -139,7 +139,6 @@ pidfile = "/var/run/prosody/prosody.pid"
|
||||
-- server please see http://prosody.im/doc/modules/mod_auth_internal_hashed
|
||||
-- for information about using the hashed backend.
|
||||
|
||||
-- authentication = "internal_plain"
|
||||
authentication = "internal_hashed"
|
||||
|
||||
-- Select the storage backend to use. By default Prosody uses flat files
|
||||
@@ -187,7 +186,7 @@ VirtualHost "auth.jitsi.example.com"
|
||||
key = "/var/lib/prosody/auth.jitsi.example.com.key";
|
||||
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
|
||||
}
|
||||
authentication = "internal_plain"
|
||||
authentication = "internal_hashed"
|
||||
|
||||
------ Components ------
|
||||
-- You can specify components to add hosts that provide special services,
|
||||
|
||||
@@ -67,7 +67,7 @@ VirtualHost "auth.meet.example.com"
|
||||
key = "/etc/prosody/certs/auth.meet.example.com.key";
|
||||
certificate = "/etc/prosody/certs/auth.meet.example.com.crt";
|
||||
}
|
||||
authentication = "internal_plain"
|
||||
authentication = "internal_hashed"
|
||||
|
||||
Component "focus.meet.example.com"
|
||||
component_secret = "jicofo_secret_test"
|
||||
@@ -83,5 +83,5 @@ VirtualHost "recorder.meet.example.com"
|
||||
modules_enabled = {
|
||||
"ping";
|
||||
}
|
||||
authentication = "internal_plain"
|
||||
authentication = "internal_hashed"
|
||||
c2s_require_encryption = false
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!--#include virtual="head.html" -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<meta name="theme-color" content="#2A3A4B">
|
||||
<!--#include virtual="base.html" -->
|
||||
|
||||
|
||||
@@ -115,10 +115,22 @@
|
||||
NSLog(@"%@%@", @"Audio muted changed: ", data[@"muted"]);
|
||||
}
|
||||
|
||||
- (void)endpointTextMessageReceived:(NSDictionary *)data; {
|
||||
- (void)endpointTextMessageReceived:(NSDictionary *)data {
|
||||
NSLog(@"%@%@", @"Endpoint text message received: ", data);
|
||||
}
|
||||
|
||||
- (void)screenShareToggled:(NSDictionary *)data {
|
||||
NSLog(@"%@%@", @"Screen share toggled: ", data);
|
||||
}
|
||||
|
||||
- (void)chatMessageReceived:(NSDictionary *)data {
|
||||
NSLog(@"%@%@", @"Chat message received: ", data);
|
||||
}
|
||||
|
||||
- (void)chatToggled:(NSDictionary *)data {
|
||||
NSLog(@"%@%@", @"Chat toggled: ", data);
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
- (void)terminate {
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
@interface ExternalAPI : RCTEventEmitter<RCTBridgeModule>
|
||||
|
||||
- (void)sendHangUp;
|
||||
- (void)sendSetAudioMuted: (BOOL)muted;
|
||||
- (void)sendSetAudioMuted:(BOOL)muted;
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
|
||||
- (void)toggleScreenShare;
|
||||
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completion;
|
||||
- (void)openChat:(NSString*)to;
|
||||
- (void)closeChat;
|
||||
- (void)sendChatMessage:(NSString*)to :(NSString*)message;
|
||||
|
||||
@end
|
||||
|
||||
@@ -21,16 +21,33 @@
|
||||
static NSString * const hangUpAction = @"org.jitsi.meet.HANG_UP";
|
||||
static NSString * const setAudioMutedAction = @"org.jitsi.meet.SET_AUDIO_MUTED";
|
||||
static NSString * const sendEndpointTextMessageAction = @"org.jitsi.meet.SEND_ENDPOINT_TEXT_MESSAGE";
|
||||
static NSString * const toggleScreenShareAction = @"org.jitsi.meet.TOGGLE_SCREEN_SHARE";
|
||||
static NSString * const retrieveParticipantsInfoAction = @"org.jitsi.meet.RETRIEVE_PARTICIPANTS_INFO";
|
||||
static NSString * const openChatAction = @"org.jitsi.meet.OPEN_CHAT";
|
||||
static NSString * const closeChatAction = @"org.jitsi.meet.CLOSE_CHAT";
|
||||
static NSString * const sendChatMessageAction = @"org.jitsi.meet.SEND_CHAT_MESSAGE";
|
||||
|
||||
@implementation ExternalAPI
|
||||
|
||||
static NSMapTable<NSString*, void (^)(NSArray* participantsInfo)> *participantInfoCompletionHandlers;
|
||||
|
||||
__attribute__((constructor))
|
||||
static void initializeViewsMap() {
|
||||
participantInfoCompletionHandlers = [NSMapTable strongToStrongObjectsMapTable];
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
||||
- (NSDictionary *)constantsToExport {
|
||||
return @{
|
||||
@"HANG_UP": hangUpAction,
|
||||
@"SET_AUDIO_MUTED" : setAudioMutedAction,
|
||||
@"SEND_ENDPOINT_TEXT_MESSAGE": sendEndpointTextMessageAction
|
||||
@"SEND_ENDPOINT_TEXT_MESSAGE": sendEndpointTextMessageAction,
|
||||
@"TOGGLE_SCREEN_SHARE": toggleScreenShareAction,
|
||||
@"RETRIEVE_PARTICIPANTS_INFO": retrieveParticipantsInfoAction,
|
||||
@"OPEN_CHAT": openChatAction,
|
||||
@"CLOSE_CHAT": closeChatAction,
|
||||
@"SEND_CHAT_MESSAGE": sendChatMessageAction
|
||||
};
|
||||
};
|
||||
|
||||
@@ -46,7 +63,15 @@ RCT_EXPORT_MODULE();
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[ hangUpAction, setAudioMutedAction, sendEndpointTextMessageAction ];
|
||||
return @[ hangUpAction,
|
||||
setAudioMutedAction,
|
||||
sendEndpointTextMessageAction,
|
||||
toggleScreenShareAction,
|
||||
retrieveParticipantsInfoAction,
|
||||
openChatAction,
|
||||
closeChatAction,
|
||||
sendChatMessageAction
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,6 +99,11 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
if (!delegate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([name isEqual: @"PARTICIPANTS_INFO_RETRIEVED"]) {
|
||||
[self onParticipantsInfoRetrieved: data];
|
||||
return;
|
||||
}
|
||||
|
||||
SEL sel = NSSelectorFromString([self methodNameFromEventName:name]);
|
||||
|
||||
@@ -82,6 +112,15 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
}
|
||||
}
|
||||
|
||||
- (void) onParticipantsInfoRetrieved:(NSDictionary *)data {
|
||||
NSArray *participantsInfoArray = [data objectForKey:@"participantsInfo"];
|
||||
NSString *completionHandlerId = [data objectForKey:@"requestId"];
|
||||
|
||||
void (^completionHandler)(NSArray*) = [participantInfoCompletionHandlers objectForKey:completionHandlerId];
|
||||
completionHandler(participantsInfoArray);
|
||||
[participantInfoCompletionHandlers removeObjectForKey:completionHandlerId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a specific event name i.e. redux action type description to a
|
||||
* method name.
|
||||
@@ -115,12 +154,43 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
}
|
||||
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
|
||||
NSDictionary *data = @{
|
||||
@"to": to,
|
||||
@"message": message
|
||||
};
|
||||
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
|
||||
data[@"to"] = to;
|
||||
data[@"message"] = message;
|
||||
|
||||
[self sendEventWithName:sendEndpointTextMessageAction body:data];
|
||||
}
|
||||
|
||||
- (void)toggleScreenShare {
|
||||
[self sendEventWithName:toggleScreenShareAction body:nil];
|
||||
}
|
||||
|
||||
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler {
|
||||
NSString *completionHandlerId = [[NSUUID UUID] UUIDString];
|
||||
NSDictionary *data = @{ @"requestId": completionHandlerId};
|
||||
|
||||
[participantInfoCompletionHandlers setObject:[completionHandler copy] forKey:completionHandlerId];
|
||||
|
||||
[self sendEventWithName:retrieveParticipantsInfoAction body:data];
|
||||
}
|
||||
|
||||
- (void)openChat:(NSString*)to {
|
||||
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
|
||||
data[@"to"] = to;
|
||||
|
||||
[self sendEventWithName:openChatAction body:data];
|
||||
}
|
||||
|
||||
- (void)closeChat {
|
||||
[self sendEventWithName:closeChatAction body:nil];
|
||||
}
|
||||
|
||||
- (void)sendChatMessage:(NSString*)to :(NSString*)message {
|
||||
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
|
||||
data[@"to"] = to;
|
||||
data[@"message"] = message;
|
||||
|
||||
[self sendEventWithName:sendChatMessageAction body:data];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.0</string>
|
||||
<string>3.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -36,11 +36,13 @@
|
||||
* Leaves the currently active conference.
|
||||
*/
|
||||
- (void)leave;
|
||||
|
||||
- (void)hangUp;
|
||||
|
||||
- (void)setAudioMuted:(BOOL)muted;
|
||||
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
|
||||
- (void)toggleScreenShare;
|
||||
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler;
|
||||
- (void)openChat:(NSString*)to;
|
||||
- (void)closeChat;
|
||||
- (void)sendChatMessage:(NSString*)to :(NSString*)message;
|
||||
|
||||
@end
|
||||
|
||||
@@ -130,6 +130,31 @@ static void initializeViewsMap() {
|
||||
[externalAPI sendEndpointTextMessage:to :message];
|
||||
}
|
||||
|
||||
- (void)toggleScreenShare {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI toggleScreenShare];
|
||||
}
|
||||
|
||||
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI retrieveParticipantsInfo:completionHandler];
|
||||
}
|
||||
|
||||
- (void)openChat:(NSString*)to {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI openChat:to];
|
||||
}
|
||||
|
||||
- (void)closeChat {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI closeChat];
|
||||
}
|
||||
|
||||
- (void)sendChatMessage:(NSString*)to :(NSString*)message {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI sendChatMessage:to :message];
|
||||
}
|
||||
|
||||
#pragma mark Private methods
|
||||
|
||||
/**
|
||||
|
||||
@@ -83,4 +83,25 @@
|
||||
*/
|
||||
- (void)endpointTextMessageReceived:(NSDictionary *)data;
|
||||
|
||||
/**
|
||||
* Called when a participant toggled shared screen.
|
||||
*
|
||||
* The `data` dictionary contains a `participantId` key with the id of the participant and a 'sharing' key with boolean value.
|
||||
*/
|
||||
- (void)screenShareToggled:(NSDictionary *)data;
|
||||
|
||||
/**
|
||||
* Called when a chat message is received.
|
||||
*
|
||||
* The `data` dictionary contains `message`, `senderId` and `isPrivate` keys.
|
||||
*/
|
||||
- (void)chatMessaageReceived:(NSDictionary *)data;
|
||||
|
||||
/**
|
||||
* Called when the chat dialog is displayed/hidden.
|
||||
*
|
||||
* The `data` dictionary contains a `isOpen` key.
|
||||
*/
|
||||
- (void)chatToggled:(NSDictionary *)data;
|
||||
|
||||
@end
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"lv": "لتونیایی",
|
||||
"nl": "هلندی",
|
||||
"oc": "اکسیتان(قدیمی)",
|
||||
"fa": "فارسی",
|
||||
"pl": "لهستانی",
|
||||
"ptBR": "پرتغالی (برزیل)",
|
||||
"ru": "روسی",
|
||||
@@ -47,4 +48,4 @@
|
||||
"vi": "ویتنامی",
|
||||
"zhCN": "چینی",
|
||||
"zhTW": "چینی (تایوان)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"lv": "Latvian",
|
||||
"nl": "Dutch",
|
||||
"oc": "Occitan",
|
||||
"fa": "Persian",
|
||||
"pl": "Polish",
|
||||
"ptBR": "Portuguese (Brazil)",
|
||||
"ru": "Russian",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"bridgeCount": "Serverzahl: ",
|
||||
"codecs": "Codecs (A/V): ",
|
||||
"connectedTo": "Verbunden mit:",
|
||||
"e2e_rtt": "E2E RTT:",
|
||||
"e2e_rtt": "Ende-zu-Ende-Paketumlaufzeit:",
|
||||
"framerate": "Bildwiederholrate:",
|
||||
"less": "Weniger anzeigen",
|
||||
"localaddress": "Lokale Adresse:",
|
||||
@@ -146,7 +146,7 @@
|
||||
"downloadApp": "App herunterladen",
|
||||
"ifDoNotHaveApp": "Wenn Sie die App noch nicht haben:",
|
||||
"ifHaveApp": "Wenn Sie die App bereits haben:",
|
||||
"joinInApp": "An dem Meeting teilnehmen mit der App",
|
||||
"joinInApp": "Mit der App am Meeting teilnehmen",
|
||||
"launchWebButton": "Im Web öffnen",
|
||||
"title": "Die Konferenz wird in {{app}} geöffnet …",
|
||||
"tryAgainButton": "Erneut mit der nativen Applikation versuchen"
|
||||
@@ -206,9 +206,9 @@
|
||||
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
|
||||
"error": "Fehler",
|
||||
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
||||
"grantModeratorDialog": "Möchten Sie diesen Person wirklich zum Moderator:in machen?",
|
||||
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
|
||||
"grantModeratorTitle": "Moderationsrechte vergeben",
|
||||
"IamHost": "Ich organisiere das Meeting",
|
||||
"IamHost": "Ich leite das Meeting",
|
||||
"incorrectRoomLockPassword": "Falsches Passwort",
|
||||
"incorrectPassword": "Name oder Passwort ungültig",
|
||||
"internalError": "Oh! Es hat etwas nicht funktioniert. Der folgende Fehler ist aufgetreten: {{error}}",
|
||||
@@ -227,7 +227,7 @@
|
||||
"lockTitle": "Sperren fehlgeschlagen",
|
||||
"logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
|
||||
"logoutTitle": "Abmelden",
|
||||
"maxUsersLimitReached": "Das Limit für die maximale Personenzahl ist erreicht. Die Konferenz ist voll. Bitte wenden Sie sich an die verwaltende Person des Meetings oder versuchen Sie es später noch einmal!",
|
||||
"maxUsersLimitReached": "Das Limit für die maximale Personenzahl ist erreicht. Die Konferenz ist voll. Bitte wenden Sie sich an die Konferenzleitung oder versuchen Sie es später noch einmal!",
|
||||
"maxUsersLimitReachedTitle": "Maximale Personenzahl erreicht",
|
||||
"micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht.",
|
||||
"micNotFoundError": "Mikrofon nicht gefunden.",
|
||||
@@ -305,8 +305,8 @@
|
||||
"unlockRoom": "Konferenz$t(lockRoomPassword) entfernen",
|
||||
"user": "Anmeldename",
|
||||
"userPassword": "Passwort",
|
||||
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls sie die Konferenz organisieren, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls sie die Konferenz organisieren, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||
"WaitingForHost": "Warten auf den Beginn der Konferenz …",
|
||||
"Yes": "Ja",
|
||||
"yourEntireScreen": "Ganzer Bildschirm"
|
||||
@@ -391,7 +391,7 @@
|
||||
"localRecording": "Lokale Aufzeichnungssteuerelemente ein- oder ausblenden",
|
||||
"mute": "Stummschaltung aktivieren oder deaktivieren",
|
||||
"pushToTalk": "Push-to-Talk (Sprechtaste)",
|
||||
"raiseHand": "Hand erheben",
|
||||
"raiseHand": "Hand heben",
|
||||
"showSpeakerStats": "Sprechstatistik anzeigen",
|
||||
"toggleChat": "Chat öffnen oder schließen",
|
||||
"toggleFilmstrip": "Video-Miniaturansichten ein- oder ausblenden",
|
||||
@@ -401,8 +401,8 @@
|
||||
"videoQuality": "Anrufqualität verwalten"
|
||||
},
|
||||
"liveStreaming": {
|
||||
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihr Stream auf {{limit}} min. begrenzt. Für unlimitiertes Streaming nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "Ihr Stream ist begrenzt auf {{limit}} min. Für unlimitiertes Streaming, nutzen Sie bitte {{app}}.",
|
||||
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihr Stream auf {{limit}} Min. begrenzt. Für unlimitiertes Streaming nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "Ihr Stream ist begrenzt auf {{limit}} Min. Für unlimitiertes Streaming, nutzen Sie bitte {{app}}.",
|
||||
"busy": "Es werden Ressourcen zum Streamen bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
|
||||
"busyTitle": "Alle Streaming-Instanzen sind in Gebrauch",
|
||||
"changeSignIn": "Konten wechseln.",
|
||||
@@ -454,7 +454,7 @@
|
||||
"finishedModerator": "Aufzeichnung der Sitzung {{token}} ist beendet. Die Aufzeichnung des lokalen Verlaufs wurde gespeichert. Bitten Sie die anderen Personen, ihre Aufzeichnungen zu übermitteln.",
|
||||
"notModerator": "Sie moderieren nicht. Sie können die lokale Aufzeichnung nicht starten oder stoppen."
|
||||
},
|
||||
"moderator": "Moderator:in",
|
||||
"moderator": "Moderation",
|
||||
"no": "Nein",
|
||||
"participant": "Person",
|
||||
"participantStats": "Personenstatistik",
|
||||
@@ -471,8 +471,8 @@
|
||||
"connectedThreePlusMembers": "{{name}} und {{count}} andere Personen nehmen am Meeting teil",
|
||||
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
|
||||
"disconnected": "getrennt",
|
||||
"focus": "Konferenz-Organisator:in",
|
||||
"focusFail": "{{component}} ist im Moment nicht verfügbar - wiederholen in {{ms}} Sekunden",
|
||||
"focus": "Konferenzleitung",
|
||||
"focusFail": "{{component}} ist im Moment nicht verfügbar – wiederholen in {{ms}} Sekunden",
|
||||
"grantedTo": "Moderationsrechte an {{to}} vergeben!",
|
||||
"invitedOneMember": "{{name}} wurde eingeladen",
|
||||
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
|
||||
@@ -526,7 +526,7 @@
|
||||
"goodQuality": "Großartig! Ihre Bild- und Tonqualität sollte super sein.",
|
||||
"noMediaConnectivity": "Es konnte für diesen Test keine Medienverbindung hergestellt werden. Das wird gewöhnlich durch eine Firewall oder ein NAT ausgelöst.",
|
||||
"noVideo": "Ihr Bild wird wahrscheinlich eine schlechte Qualität haben.",
|
||||
"undetectable": "Wenn Sie mit Ihrem Browser weiterhin Probleme in Konferenzen haben, sollten Sie die Verbindung und Funktion Ihrer Lautsprecher, Ihres Mikrofons und Ihrer Kamera überprüfen. Stellen Sie außerdem sicher, dass Ihr Browser die erforderlichen Rechte hat, auf das Mikrofon und die Kamera zuzugreifen, und dass Sie die neuste Browserversion installiert haben. Sollten Sie immer noch Probleme haben, kontaktieren Sie bitte die Entwickler:innen der Webanwendung.",
|
||||
"undetectable": "Wenn Sie mit Ihrem Browser weiterhin Probleme in Konferenzen haben, sollten Sie die Verbindung und Funktion Ihrer Lautsprecher, Ihres Mikrofons und Ihrer Kamera überprüfen. Stellen Sie außerdem sicher, dass Ihr Browser die erforderlichen Rechte hat, auf das Mikrofon und die Kamera zuzugreifen, und dass Sie die neuste Browserversion installiert haben. Sollten Sie immer noch Probleme haben, kontaktieren Sie bitte den Support der Webanwendung.",
|
||||
"veryPoorConnection": "Ihre Konferenzqualität wird wahrscheinlich sehr schlecht sein.",
|
||||
"videoFreezing": "Ihr Bild wird wahrscheinlich einfrieren, schwarz werden und eine geringe Auflösung haben.",
|
||||
"videoHighQuality": "Ihr Bild sollte sehr gut aussehen.",
|
||||
@@ -583,8 +583,8 @@
|
||||
},
|
||||
"raisedHand": "Ich möchte sprechen",
|
||||
"recording": {
|
||||
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} min begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
|
||||
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
|
||||
"authDropboxText": "In Dropbox hochladen",
|
||||
"availableSpace": "Verfügbarer Speicherplatz: {{spaceLeft}} MB (ca. {{duration}} Minuten Aufzeichnung)",
|
||||
"beta": "BETA",
|
||||
@@ -633,7 +633,7 @@
|
||||
"language": "Sprache",
|
||||
"loggedIn": "Als {{name}} angemeldet",
|
||||
"microphones": "Mikrofon",
|
||||
"moderator": "Moderator:in",
|
||||
"moderator": "Moderation",
|
||||
"more": "Mehr",
|
||||
"name": "Name",
|
||||
"noDevice": "Kein",
|
||||
@@ -671,12 +671,12 @@
|
||||
"dialInfoText": "\n\n=====\n\nWollen Sie sich nur auf Ihrem Telefon einwählen?\n\n{{defaultDialInNumber}}Klicken Sie auf diesen Link, um die eingewählten Telefonnummern für dieses Meeting zu sehen\n{{dialInfoPageUrl}}",
|
||||
"mainText": "Klicken Sie auf den folgenden Link, um dem Meeting beizutreten:\n{{roomUrl}}"
|
||||
},
|
||||
"speaker": "Sprecher:in",
|
||||
"speaker": "Sprecher/-in",
|
||||
"speakerStats": {
|
||||
"hours": "{{count}}h",
|
||||
"minutes": "{{count}}m",
|
||||
"hours": "{{count}} Std.",
|
||||
"minutes": "{{count}} Min.",
|
||||
"name": "Name",
|
||||
"seconds": "{{count}}s",
|
||||
"seconds": "{{count}} Sek.",
|
||||
"speakerStats": "Sprechstatistik",
|
||||
"speakerTime": "Sprechzeit"
|
||||
},
|
||||
@@ -853,7 +853,7 @@
|
||||
"flip": "Spiegeln",
|
||||
"grantModerator": "Moderationsrechte vergeben",
|
||||
"kick": "Hinauswerfen",
|
||||
"moderator": "Moderator:in",
|
||||
"moderator": "Moderation",
|
||||
"mute": "Person ist stumm geschaltet",
|
||||
"muted": "Stummgeschaltet",
|
||||
"remoteControl": "Fernsteuerung",
|
||||
@@ -897,7 +897,7 @@
|
||||
},
|
||||
"lonelyMeetingExperience": {
|
||||
"button": "Andere einladen",
|
||||
"youAreAlone": "Sie sind alleine in dieser Konferenz"
|
||||
"youAreAlone": "Nur Sie sind in dieser Konferenz"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "Hilfecenter"
|
||||
@@ -907,7 +907,7 @@
|
||||
"allow": "Annehmen",
|
||||
"backToKnockModeButton": "Kein Passwort, stattdessen Beitritt anfragen",
|
||||
"dialogTitle": "Lobbymodus",
|
||||
"disableDialogContent": "Lobbymodus derzeit aktiviert. Diese Funktion stellt sicher, dass unerwünschte Personen Ihrer Konferenz nicht beitreten können. Funktion deaktivieren?",
|
||||
"disableDialogContent": "Der Lobbymodus ist derzeit aktiviert. Diese Funktion stellt sicher, dass unerwünschte Personen Ihrer Konferenz nicht beitreten können. Funktion deaktivieren?",
|
||||
"disableDialogSubmit": "Deaktivieren",
|
||||
"emailField": "E-Mail-Adresse eingeben",
|
||||
"enableDialogPasswordField": "Passwort setzen (optional)",
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
"dismiss": "رد کردن",
|
||||
"displayNameRequired": "نام خود را وارد نمایید",
|
||||
"done": "تایید",
|
||||
"e2eeDescription": "<p>رمزگذاری دو طرفه در حال حاضر به صورت <strong>آزمایشی</strong> استفاده میشود؛ برای اطلاعات بیشتر میتوانید <a href='https://blog.bebbin.ir/e2ee/' target='_blank'>این مقاله</a> را ببینید.</p><br/><p>در نظر داشته باشید با فعال کردن رمز گذاری دو طرفه قابلیتهای سمت سرو غیرفعال خواهند شد، قابلیتهایی از قبیل:ضبط، پخش زنده و مشارکت تلفنی در جلسات؛ همچنین افرادی میتوانند به این جلسه بپیوندند که مرورگر آنها از قابلیت پخش درون مرورگری پشتیبانی کند،</p>",
|
||||
"e2eeDescription": "<p>رمزگذاری دو طرفه در حال حاضر به صورت <strong>آزمایشی</strong> استفاده میشود؛ برای اطلاعات بیشتر میتوانید <a href='https://jitsi.org/blog/e2ee/' target='_blank'>این مقاله</a> را ببینید.</p><br/><p>در نظر داشته باشید با فعال کردن رمز گذاری دو طرفه قابلیتهای سمت سرو غیرفعال خواهند شد، قابلیتهایی از قبیل:ضبط، پخش زنده و مشارکت تلفنی در جلسات؛ همچنین افرادی میتوانند به این جلسه بپیوندند که مرورگر آنها از قابلیت پخش درون مرورگری پشتیبانی کند،</p>",
|
||||
"e2eeLabel": "کلید E2EE",
|
||||
"e2eeWarning": "<br /><p><strong>هشدار:</strong> همهی مشارکت کنندگان رمزنگاری دو طرفه را پشتیبانی نمیکنند؛ اگر این قابلیت را فعال کنید آنها صدای جلسه را نمیشنوند و تصویر را مشاهده نخواهند کرد</p>",
|
||||
"enterDisplayName": "لطفا نام نمایشی خود را وارد نمایید",
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
"cameraNotSendingData": "Non possiamo accedere alla tua videocamera. Controlla che non sia già usata da un'altra applicazione, seleziona un altro dispositivo dalle impostazioni o prova a ricaricare l'applicazione.",
|
||||
"cameraNotSendingDataTitle": "Impossibile accedere alla videocamera",
|
||||
"cameraPermissionDeniedError": "Non hai concesso il permesso di usare la videocamera. Potrai partecipare comunque alla riunione ma gli altri non potranno vederti. Usa il pulsante a forma di videocamera nella barra degli indirizzi per risolvere il problema.",
|
||||
"cameraTimeoutError": "Impossibile avviare la sorgente video. Tempo di attesa scaduto.",
|
||||
"cameraUnknownError": "Impossibile usare la videocamera per un motivo sconosciuto.",
|
||||
"cameraUnsupportedResolutionError": "La tua videocamera non supporta la risoluzione richiesta.",
|
||||
"Cancel": "Annulla",
|
||||
@@ -233,6 +234,7 @@
|
||||
"micNotSendingData": "Apri le impostazioni del computer per togliere il «muto» al microfono e imposta il volume.",
|
||||
"micNotSendingDataTitle": "Il microfono è muto per impostazione di sistema",
|
||||
"micPermissionDeniedError": "Non hai concesso il permesso di usare il microfono. Puoi comunque partecipare alla riunione ma gli altri non potranno sentirti. Usa il bottone a forma di telecamera nella barra degli indirizzi per cambiare impostazioni.",
|
||||
"micTimeoutError": "Impossibile avviare la fonte audio. Tempo di attesa scaduto.",
|
||||
"micUnknownError": "Impossibile usare il microfono per un motivo sconosciuto.",
|
||||
"muteEveryoneElseDialog": "Una volta zittiti, non potrai riattivargli i microfoni, ma loro potranno farlo in qualsiasi momento.",
|
||||
"muteEveryoneElseTitle": "Zittisco tutti eccetto {{whom}}?",
|
||||
@@ -280,6 +282,7 @@
|
||||
"sendPrivateMessageTitle": "Invio privatamente?",
|
||||
"serviceUnavailable": "Servizio non disponibile",
|
||||
"sessTerminated": "Chiamata terminata",
|
||||
"sessionRestarted": "Chiamata riavviata automaticamente",
|
||||
"Share": "Condividi",
|
||||
"shareVideoLinkError": "Fornire un link youtube corretto.",
|
||||
"shareVideoTitle": "Condividi un video",
|
||||
@@ -734,7 +737,7 @@
|
||||
},
|
||||
"addPeople": "Aggiungi persone alla chiamata",
|
||||
"audioOnlyOff": "Disabilita modalità per banda limitata",
|
||||
"audioOnlyOn": "Abilita modalità per banda limitatao",
|
||||
"audioOnlyOn": "Abilita modalità per banda limitata",
|
||||
"audioRoute": "Scegli l'uscita audio",
|
||||
"authenticate": "Autenticazione",
|
||||
"callQuality": "Imposta qualità video",
|
||||
|
||||
@@ -489,7 +489,7 @@
|
||||
"suboptimalBrowserWarning": "あなたのミーティングの体験が、ここではあまり良好ではないのではと心配しています。これを改善する方法を模索していますが、それまでは<a href='{{recommendedBrowserPageLink}}' target='_blank'> フルサポートしているブラウザー</a>のいずれかを利用してください。",
|
||||
"suboptimalExperienceTitle": "ブラウザーの警告",
|
||||
"unmute": "ミュート解除",
|
||||
"newDeviceCameraTitle": "新しいカメラを麺出しました",
|
||||
"newDeviceCameraTitle": "新しいカメラを検出しました",
|
||||
"newDeviceAudioTitle": "新しいオーディオデバイスを検出しました",
|
||||
"newDeviceAction": "使用する",
|
||||
"OldElectronAPPTitle": "セキュリティ上の脆弱性があります!",
|
||||
|
||||
@@ -233,9 +233,9 @@
|
||||
"passwordRequired": "비밀번호 필수",
|
||||
"popupError": "브라우저가이 사이트의 팝업 창을 차단하고 있습니다. 브라우저의 보안 설정에서 팝업을 활성화하고 다시 시도하십시오.",
|
||||
"popupErrorTitle": "팝업 차단됨",
|
||||
"recording": "레코딩",
|
||||
"recordingDisabledForGuestTooltip": "게스트는 녹음을 시작할 수 없습니다.",
|
||||
"recordingDisabledTooltip": "녹화이 비활성화 되었습니다.",
|
||||
"recording": "녹화",
|
||||
"recordingDisabledForGuestTooltip": "게스트는 녹화를 시작할 수 없습니다.",
|
||||
"recordingDisabledTooltip": "녹화가 비활성화 되었습니다.",
|
||||
"rejoinNow": "지금 재가입",
|
||||
"remoteControlAllowedMessage": "{{user}}이(가) 원격 제어 요청을 수락했습니다",
|
||||
"remoteControlDeniedMessage": "{{user}}이(가) 원격 제어 요청을 거부했습니다",
|
||||
@@ -270,11 +270,11 @@
|
||||
"shareYourScreenDisabled": "화면 공유가 비활성화 되었습니다.",
|
||||
"shareYourScreenDisabledForGuest": "게스트는 화면을 공유 할 수 없습니다.",
|
||||
"startLiveStreaming": "라이브 스트리밍 시작",
|
||||
"startRecording": "레코딩 시작",
|
||||
"startRecording": "녹화 시작",
|
||||
"startRemoteControlErrorMessage": "원격 제어 세션을 시작하는 동안 오류가 발생했습니다",
|
||||
"stopLiveStreaming": "라이브 스트리밍 종료",
|
||||
"stopRecording": "레코딩 종료",
|
||||
"stopRecordingWarning": "레코딩을 중단하고 싶으십니까?",
|
||||
"stopRecording": "녹화 종료",
|
||||
"stopRecordingWarning": "녹화를 중단하고 싶으십니까?",
|
||||
"stopStreamingWarning": "라이브 스트리밍을 중단하고 싶으십니까?",
|
||||
"streamKey": "라이브 스트리밍 키",
|
||||
"Submit": "제출",
|
||||
@@ -362,7 +362,7 @@
|
||||
"focusRemote": "다른 발신자의 동영상에 포커스",
|
||||
"fullScreen": "전체화면 표시 또는 종료",
|
||||
"keyboardShortcuts": "키보드 단축키",
|
||||
"localRecording": "로컬 녹음 컨트롤 표시 또는 숨기기",
|
||||
"localRecording": "로컬 녹화 컨트롤 표시 또는 숨기기",
|
||||
"mute": "마이크 음소거 또는 음소거 해제",
|
||||
"pushToTalk": "대화 요청",
|
||||
"raiseHand": "말하기 요청/해제",
|
||||
@@ -429,8 +429,8 @@
|
||||
"participant": "",
|
||||
"participantStats": "",
|
||||
"sessionToken": "",
|
||||
"start": "레코딩 시작",
|
||||
"stop": "레코딩 종료",
|
||||
"start": "녹화 시작",
|
||||
"stop": "녹화 종료",
|
||||
"yes": "예"
|
||||
},
|
||||
"lockRoomPassword": "비밀번호",
|
||||
@@ -494,26 +494,26 @@
|
||||
"authDropboxText": "Dropbox에 업로드",
|
||||
"availableSpace": "사용 가능한 공간 : {{spaceLeft}}MB (약 {{duration}}분 녹화)",
|
||||
"beta": "베타",
|
||||
"busy": "레코딩 자원을 확보하고 있습니다. 몇 분 후에 다시 시도하십시오.",
|
||||
"busyTitle": "모든 레코더가 현재 사용중입니다",
|
||||
"error": "레코딩이 실패했습니다. 다시 시도하십시오.",
|
||||
"expandedOff": "레코딩이 중지됨",
|
||||
"expandedOn": "회의가 현재 녹화중입니다.",
|
||||
"busy": "녹화 자원을 확보하고 있습니다. 몇 분 후에 다시 시도하십시오.",
|
||||
"busyTitle": "모든 레코더가 현재 사용 중입니다",
|
||||
"error": "녹화가 실패했습니다. 다시 시도하십시오.",
|
||||
"expandedOff": "녹화가 중지됨",
|
||||
"expandedOn": "회의가 현재 녹화 중입니다.",
|
||||
"expandedPending": "녹화가 시작됩니다 ...",
|
||||
"failedToStart": "레코딩을 시작하지 못했습니다",
|
||||
"fileSharingdescription": "회의 참가자와 녹음 공유",
|
||||
"failedToStart": "녹화를 시작하지 못했습니다",
|
||||
"fileSharingdescription": "회의 참가자와 녹화 공유",
|
||||
"live": "라이브",
|
||||
"loggedIn": "{{userName}}으로 로그인했습니다.",
|
||||
"off": "레코딩이 중지됨",
|
||||
"on": "레코딩",
|
||||
"off": "녹화가 중지됨",
|
||||
"on": "녹화",
|
||||
"pending": "참석할 멤버를 기다리는 중입니다…",
|
||||
"rec": "녹음",
|
||||
"serviceDescription": "녹음은 녹음 서비스에 의해 저장됩니다.",
|
||||
"serviceName": "레코딩 서비스",
|
||||
"rec": "녹화",
|
||||
"serviceDescription": "녹화는 녹화 서비스에 의해 저장됩니다.",
|
||||
"serviceName": "녹화 서비스",
|
||||
"signIn": "로그인",
|
||||
"signOut": "로그아웃",
|
||||
"unavailable": "죄송합니다. {{serviceName}}은 현재 사용할 수 없습니다. 저희는 문제를 해결하기 위해 노력하고 있습니다. 나중에 다시 시도 해주십시오.",
|
||||
"unavailableTitle": "레코딩을 사용할 수 없습니다"
|
||||
"unavailableTitle": "녹화를 사용할 수 없습니다"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "당겨서 새로고침"
|
||||
@@ -665,8 +665,8 @@
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "자막 시작/종료",
|
||||
"error": "레코딩이 실패했습니다. 다시 시도하십시오.",
|
||||
"expandedLabel": "현재 스크립트 작성중",
|
||||
"error": "녹화가 실패했습니다. 다시 시도하십시오.",
|
||||
"expandedLabel": "현재 스크립트 작성 중",
|
||||
"failedToStart": "스크립트 작성을 시작하지 못했습니다.",
|
||||
"labelToolTip": "회의가 기록되고 있습니다.",
|
||||
"off": "스크립트 작성이 중지되었습니다.",
|
||||
|
||||
@@ -908,6 +908,8 @@
|
||||
"getHelp": "Справка",
|
||||
"go": "ОК",
|
||||
"goSmall": "ОК",
|
||||
"headerTitle":"Сервер видеоконференцсвязи Jitsi Meet",
|
||||
"headerSubtitle":"Защищенная высококачественная видеосвязь",
|
||||
"info": "Инфо",
|
||||
"join": "СОЗДАТЬ / ПРИСОЕДИНИТЬСЯ",
|
||||
"moderatedMessage": "Или заранее <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">зарезервируйте URL-адрес встречи</a>, где вы будете единственным модератором.",
|
||||
@@ -920,6 +922,7 @@
|
||||
"roomname": "Укажите название комнаты",
|
||||
"roomnameHint": "Укажите название комнаты или ее адрес. Можете сами создать название и передать его будущим участникам встречи, чтобы они использовали именно его.",
|
||||
"sendFeedback": "Обратная связь",
|
||||
"startMeeting": "Создать конференцию",
|
||||
"terms": "Условия",
|
||||
"title": "Защищенная, полнофункциональная и совершенно бесплатная система видеоконференций"
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.",
|
||||
"cameraNotSendingDataTitle": "Unable to access camera",
|
||||
"cameraPermissionDeniedError": "You have not granted permission to use your camera. You can still join the conference but others won't see you. Use the camera button in the address bar to fix this.",
|
||||
"cameraTimeoutError": "Could not start video source. Timeout occured!",
|
||||
"cameraUnknownError": "Cannot use camera for an unknown reason.",
|
||||
"cameraUnsupportedResolutionError": "Your camera does not support required video resolution.",
|
||||
"Cancel": "Cancel",
|
||||
@@ -233,6 +234,7 @@
|
||||
"micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level",
|
||||
"micNotSendingDataTitle": "Your mic is muted by your system settings",
|
||||
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
|
||||
"micTimeoutError": "Could not start audio source. Timeout occured!",
|
||||
"micUnknownError": "Cannot use microphone for an unknown reason.",
|
||||
"muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
|
||||
@@ -280,6 +282,7 @@
|
||||
"sendPrivateMessageTitle": "Send privately?",
|
||||
"serviceUnavailable": "Service unavailable",
|
||||
"sessTerminated": "Call terminated",
|
||||
"sessionRestarted": "Call restarted by the bridge",
|
||||
"Share": "Share",
|
||||
"shareVideoLinkError": "Please provide a correct youtube link.",
|
||||
"shareVideoTitle": "Share a video",
|
||||
@@ -809,7 +812,7 @@
|
||||
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"edgeGrantPermissions": "Select <b><i>Yes</i></b> when your browser asks for permissions.",
|
||||
"electronGrantPermissions": "Please grant permissions to use your camera and microphone",
|
||||
"electronGrantPermissions": "Trying to access your camera and microphone",
|
||||
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
|
||||
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
|
||||
"nwjsGrantPermissions": "Please grant permissions to use your camera and microphone",
|
||||
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
} from '../../react/features/base/conference';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
|
||||
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
|
||||
import { pinParticipant, getParticipantById } from '../../react/features/base/participants';
|
||||
import { pinParticipant, getParticipantById, kickParticipant } from '../../react/features/base/participants';
|
||||
import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
|
||||
import { openChat } from '../../react/features/chat/actions.web';
|
||||
import {
|
||||
processExternalDeviceRequest
|
||||
} from '../../react/features/device-selection/functions';
|
||||
@@ -30,8 +31,8 @@ import {
|
||||
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
|
||||
import { RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { getActiveSession } from '../../react/features/recording/functions';
|
||||
import { muteAllParticipants } from '../../react/features/remote-video-menu/actions';
|
||||
import { toggleTileView } from '../../react/features/video-layout';
|
||||
import { muteAllParticipants } from '../../react/features/video-menu/actions';
|
||||
import { setVideoQuality } from '../../react/features/video-quality';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
@@ -342,13 +343,16 @@ function initCommands() {
|
||||
if (!isChatOpen) {
|
||||
APP.UI.toggleChat();
|
||||
}
|
||||
APP.store.dispatch(setPrivateMessageRecipient(participant));
|
||||
APP.store.dispatch(openChat(participant));
|
||||
} else {
|
||||
logger.error('No participant found for the given participantId');
|
||||
}
|
||||
},
|
||||
'cancel-private-chat': () => {
|
||||
APP.store.dispatch(setPrivateMessageRecipient());
|
||||
},
|
||||
'kick-participant': participantId => {
|
||||
APP.store.dispatch(kickParticipant(participantId));
|
||||
}
|
||||
};
|
||||
transport.on('event', ({ data, name }) => {
|
||||
@@ -441,6 +445,23 @@ function initCommands() {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'get-livestream-url': {
|
||||
const state = APP.store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
let livestreamUrl;
|
||||
|
||||
if (conference) {
|
||||
const activeSession = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
|
||||
|
||||
livestreamUrl = activeSession?.liveStreamViewURL;
|
||||
} else {
|
||||
logger.error('Conference is not defined');
|
||||
}
|
||||
callback({
|
||||
livestreamUrl
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -556,6 +577,21 @@ class API {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that the chat state has been updated.
|
||||
*
|
||||
* @param {number} unreadCount - The unread messages counter.
|
||||
* @param {boolean} isOpen - True if the chat panel is open.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyChatUpdated(unreadCount: number, isOpen: boolean) {
|
||||
this._sendEvent({
|
||||
name: 'chat-updated',
|
||||
unreadCount,
|
||||
isOpen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that message was sent.
|
||||
*
|
||||
|
||||
22
modules/API/external/external_api.js
vendored
22
modules/API/external/external_api.js
vendored
@@ -35,6 +35,7 @@ const commands = {
|
||||
toggleLobby: 'toggle-lobby',
|
||||
hangup: 'video-hangup',
|
||||
intiatePrivateChat: 'initiate-private-chat',
|
||||
kickParticipant: 'kick-participant',
|
||||
muteEveryone: 'mute-everyone',
|
||||
password: 'password',
|
||||
pinParticipant: 'pin-participant',
|
||||
@@ -64,6 +65,7 @@ const events = {
|
||||
'audio-availability-changed': 'audioAvailabilityChanged',
|
||||
'audio-mute-status-changed': 'audioMuteStatusChanged',
|
||||
'camera-error': 'cameraError',
|
||||
'chat-updated': 'chatUpdated',
|
||||
'content-sharing-participants-changed': 'contentSharingParticipantsChanged',
|
||||
'device-list-changed': 'deviceListChanged',
|
||||
'display-name-change': 'displayNameChange',
|
||||
@@ -318,7 +320,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
const frameName = `jitsiConferenceFrame${id}`;
|
||||
|
||||
this._frame = document.createElement('iframe');
|
||||
this._frame.allow = 'camera; microphone; display-capture';
|
||||
this._frame.allow = 'camera; microphone; display-capture; autoplay;';
|
||||
this._frame.src = this._url;
|
||||
this._frame.name = frameName;
|
||||
this._frame.id = frameName;
|
||||
@@ -572,6 +574,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* logLevel: the message log level
|
||||
* arguments: an array of strings that compose the actual log message
|
||||
* }}
|
||||
* {@code chatUpdated} - receives event notifications about chat state being
|
||||
* updated. The listener will receive object with the following structure:
|
||||
* {{
|
||||
* 'unreadCount': unreadCounter, // the unread message(s) counter,
|
||||
* 'isOpen': isOpen, // whether the chat panel is open or not
|
||||
* }}
|
||||
* {@code incomingMessage} - receives event notifications about incoming
|
||||
* messages. The listener will receive object with the following structure:
|
||||
* {{
|
||||
@@ -746,6 +754,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
return getCurrentDevices(this._transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current livestream url.
|
||||
*
|
||||
* @returns {Promise} - Resolves with the current livestream URL if exists, with
|
||||
* undefined if not and rejects on failure.
|
||||
*/
|
||||
getLivestreamUrl() {
|
||||
return this._transport.sendRequest({
|
||||
name: 'get-livestream-url'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the conference participants information.
|
||||
*
|
||||
|
||||
@@ -115,7 +115,6 @@ UI.start = function() {
|
||||
// Set the defaults for prompt dialogs.
|
||||
$.prompt.setDefaults({ persistent: false });
|
||||
|
||||
VideoLayout.init(eventEmitter);
|
||||
VideoLayout.initLargeVideo();
|
||||
|
||||
// Do not animate the video area on UI start (second argument passed into
|
||||
@@ -135,7 +134,6 @@ UI.start = function() {
|
||||
if (config.iAmRecorder) {
|
||||
// in case of iAmSipGateway keep local video visible
|
||||
if (!config.iAmSipGateway) {
|
||||
VideoLayout.setLocalVideoVisible(false);
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
}
|
||||
|
||||
@@ -179,14 +177,6 @@ UI.unbindEvents = () => {
|
||||
$(window).off('resize');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show local video stream on UI.
|
||||
* @param {JitsiTrack} track stream to show
|
||||
*/
|
||||
UI.addLocalVideoStream = track => {
|
||||
VideoLayout.changeLocalVideo(track);
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup and show Etherpad.
|
||||
* @param {string} name etherpad id
|
||||
@@ -227,14 +217,6 @@ UI.addUser = function(user) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update videotype for specified user.
|
||||
* @param {string} id user id
|
||||
* @param {string} newVideoType new videotype
|
||||
*/
|
||||
UI.onPeerVideoTypeChanged
|
||||
= (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
|
||||
|
||||
/**
|
||||
* Updates the user status.
|
||||
*
|
||||
@@ -289,19 +271,14 @@ UI.setAudioMuted = function(id) {
|
||||
* Sets muted video state for participant
|
||||
*/
|
||||
UI.setVideoMuted = function(id) {
|
||||
VideoLayout.onVideoMute(id);
|
||||
VideoLayout._updateLargeVideoIfDisplayed(id, true);
|
||||
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
APP.conference.updateVideoIconEnabled();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers an update of remote video and large video displays so they may pick
|
||||
* up any state changes that have occurred elsewhere.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.updateAllVideos = () => VideoLayout.updateAllVideos();
|
||||
UI.updateLargeVideo = (id, forceUpdate) => VideoLayout.updateLargeVideo(id, forceUpdate);
|
||||
|
||||
/**
|
||||
* Adds a listener that would be notified on the given type of event.
|
||||
@@ -340,8 +317,6 @@ UI.removeListener = function(type, listener) {
|
||||
*/
|
||||
UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
|
||||
|
||||
UI.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber);
|
||||
|
||||
// Used by torture.
|
||||
UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/* global $, APP */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import { Thumbnail } from '../../../react/features/filmstrip';
|
||||
import SmallVideo from '../videolayout/SmallVideo';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class SharedVideoThumb extends SmallVideo {
|
||||
/**
|
||||
*
|
||||
* @param {*} participant
|
||||
*/
|
||||
constructor(participant) {
|
||||
super();
|
||||
this.id = participant.id;
|
||||
this.isLocal = false;
|
||||
this.url = participant.id;
|
||||
this.videoSpanId = 'sharedVideoContainer';
|
||||
this.container = this.createContainer(this.videoSpanId);
|
||||
this.$container = $(this.container);
|
||||
this.renderThumbnail();
|
||||
this._setThumbnailSize();
|
||||
this.bindHoverHandler();
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} spanId
|
||||
*/
|
||||
createContainer(spanId) {
|
||||
const container = document.createElement('span');
|
||||
|
||||
container.id = spanId;
|
||||
container.className = 'videocontainer';
|
||||
|
||||
const remoteVideosContainer
|
||||
= document.getElementById('filmstripRemoteVideosContainer');
|
||||
const localVideoContainer
|
||||
= document.getElementById('localVideoTileViewContainer');
|
||||
|
||||
remoteVideosContainer.insertBefore(container, localVideoContainer);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>, this.container);
|
||||
}
|
||||
}
|
||||
@@ -25,129 +25,6 @@ const Filmstrip = {
|
||||
*/
|
||||
getVerticalFilmstripWidth() {
|
||||
return isFilmstripVisible(APP.store) ? getVerticalFilmstripVisibleAreaWidth() : 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes thumbnails for tile view.
|
||||
*
|
||||
* @param {number} width - The new width of the thumbnails.
|
||||
* @param {number} height - The new height of the thumbnails.
|
||||
* @param {boolean} forceUpdate
|
||||
* @returns {void}
|
||||
*/
|
||||
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
|
||||
const thumbs = this._getThumbs(!forceUpdate);
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
thumbs.localThumb.css({
|
||||
'padding-top': '',
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
thumbs.remoteThumbs.css({
|
||||
'padding-top': '',
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes thumbnails for horizontal view.
|
||||
*
|
||||
* @param {Object} dimensions - The new dimensions of the thumbnails.
|
||||
* @param {boolean} forceUpdate
|
||||
* @returns {void}
|
||||
*/
|
||||
resizeThumbnailsForHorizontalView({ local = {}, remote = {} }, forceUpdate = false) {
|
||||
const thumbs = this._getThumbs(!forceUpdate);
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
const { height, width } = local;
|
||||
|
||||
thumbs.localThumb.css({
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
const { height, width } = remote;
|
||||
|
||||
thumbs.remoteThumbs.css({
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes thumbnails for vertical view.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
resizeThumbnailsForVerticalView() {
|
||||
const thumbs = this._getThumbs(true);
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
const heightToWidthPercent = 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
|
||||
|
||||
thumbs.localThumb.css({
|
||||
'padding-top': `${heightToWidthPercent}%`,
|
||||
width: '',
|
||||
height: '',
|
||||
'min-width': '',
|
||||
'min-height': ''
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
const heightToWidthPercent = 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO;
|
||||
|
||||
thumbs.remoteThumbs.css({
|
||||
'padding-top': `${heightToWidthPercent}%`,
|
||||
width: '',
|
||||
height: '',
|
||||
'min-width': '',
|
||||
'min-height': ''
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns thumbnails of the filmstrip
|
||||
* @param onlyVisible
|
||||
* @returns {object} thumbnails
|
||||
*/
|
||||
_getThumbs(onlyVisible = false) {
|
||||
let selector = 'span';
|
||||
|
||||
if (onlyVisible) {
|
||||
selector += ':visible';
|
||||
}
|
||||
|
||||
const localThumb = $('#localVideoContainer');
|
||||
const remoteThumbs = $('#filmstripRemoteVideosContainer').children(selector);
|
||||
|
||||
// Exclude the local video container if it has been hidden.
|
||||
if (localThumb.hasClass('hidden')) {
|
||||
return { remoteThumbs };
|
||||
}
|
||||
|
||||
return { remoteThumbs,
|
||||
localThumb };
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { createDeferred } from '../../util/helpers';
|
||||
import AudioLevels from '../audio_levels/AudioLevels';
|
||||
|
||||
@@ -51,21 +50,19 @@ export default class LargeVideoManager {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(emitter) {
|
||||
constructor() {
|
||||
/**
|
||||
* The map of <tt>LargeContainer</tt>s where the key is the video
|
||||
* container type.
|
||||
* @type {Object.<string, LargeContainer>}
|
||||
*/
|
||||
this.containers = {};
|
||||
this.eventEmitter = emitter;
|
||||
|
||||
this.state = VIDEO_CONTAINER_TYPE;
|
||||
|
||||
// FIXME: We are passing resizeContainer as parameter which is calling
|
||||
// Container.resize. Probably there's better way to implement this.
|
||||
this.videoContainer = new VideoContainer(
|
||||
() => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
|
||||
this.videoContainer = new VideoContainer(() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
|
||||
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
||||
|
||||
// use the same video container to handle desktop tracks
|
||||
@@ -300,7 +297,6 @@ export default class LargeVideoManager {
|
||||
// after everything is done check again if there are any pending
|
||||
// new streams.
|
||||
this.updateInProcess = false;
|
||||
this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
|
||||
this.scheduleLargeVideoUpdate();
|
||||
});
|
||||
}
|
||||
@@ -372,7 +368,7 @@ export default class LargeVideoManager {
|
||||
let widthToUse = this.preferredWidth || window.innerWidth;
|
||||
const { isOpen } = APP.store.getState()['features/chat'];
|
||||
|
||||
if (isOpen) {
|
||||
if (isOpen && window.innerWidth > 580) {
|
||||
/**
|
||||
* If chat state is open, we re-compute the container width
|
||||
* by subtracting the default width of the chat.
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
/* global $, config, APP */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { VideoTrack } from '../../../react/features/base/media';
|
||||
import { updateSettings } from '../../../react/features/base/settings';
|
||||
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
|
||||
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
|
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import SmallVideo from './SmallVideo';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class LocalVideo extends SmallVideo {
|
||||
/**
|
||||
*
|
||||
* @param {*} emitter
|
||||
* @param {*} streamEndedCallback
|
||||
*/
|
||||
constructor(emitter, streamEndedCallback) {
|
||||
super();
|
||||
this.videoSpanId = 'localVideoContainer';
|
||||
this.streamEndedCallback = streamEndedCallback;
|
||||
this.container = this.createContainer();
|
||||
this.$container = $(this.container);
|
||||
this.isLocal = true;
|
||||
this._setThumbnailSize();
|
||||
this.updateDOMLocation();
|
||||
this.renderThumbnail();
|
||||
|
||||
this.localVideoId = null;
|
||||
this.bindHoverHandler();
|
||||
if (!config.disableLocalVideoFlip) {
|
||||
this._buildContextMenu();
|
||||
}
|
||||
this.emitter = emitter;
|
||||
|
||||
Object.defineProperty(this, 'id', {
|
||||
get() {
|
||||
return APP.conference.getMyUserId();
|
||||
}
|
||||
});
|
||||
this.initBrowserSpecificProperties();
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
createContainer() {
|
||||
const containerSpan = document.createElement('span');
|
||||
|
||||
containerSpan.classList.add('videocontainer');
|
||||
containerSpan.id = this.videoSpanId;
|
||||
|
||||
return containerSpan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>, this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} stream
|
||||
*/
|
||||
changeVideo(stream) {
|
||||
this.localVideoId = `localVideo_${stream.getId()}`;
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const isVideo = stream.videoType != 'desktop';
|
||||
const settings = APP.store.getState()['features/base/settings'];
|
||||
|
||||
this._enableDisableContextMenu(isVideo);
|
||||
this.setFlipX(isVideo ? settings.localFlipX : false);
|
||||
|
||||
const endedHandler = () => {
|
||||
this._notifyOfStreamEnded();
|
||||
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||
};
|
||||
|
||||
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify any subscribers of the local video stream ending.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_notifyOfStreamEnded() {
|
||||
if (this.streamEndedCallback) {
|
||||
this.streamEndedCallback(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides the local video container.
|
||||
* @param {boolean} true to make the local video container visible, false
|
||||
* otherwise
|
||||
*/
|
||||
setVisible(visible) {
|
||||
// We toggle the hidden class as an indication to other interested parties
|
||||
// that this container has been hidden on purpose.
|
||||
this.$container.toggleClass('hidden');
|
||||
|
||||
// We still show/hide it as we need to overwrite the style property if we
|
||||
// want our action to take effect. Toggling the display property through
|
||||
// the above css class didn't succeed in overwriting the style.
|
||||
if (visible) {
|
||||
this.$container.show();
|
||||
} else {
|
||||
this.$container.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flipX state of the video.
|
||||
* @param val {boolean} true for flipped otherwise false;
|
||||
*/
|
||||
setFlipX(val) {
|
||||
this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
|
||||
if (!this.localVideoId) {
|
||||
return;
|
||||
}
|
||||
if (val) {
|
||||
this.selectVideoElement().addClass('flipVideoX');
|
||||
} else {
|
||||
this.selectVideoElement().removeClass('flipVideoX');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the context menu for the local video.
|
||||
*/
|
||||
_buildContextMenu() {
|
||||
$.contextMenu({
|
||||
selector: `#${this.videoSpanId}`,
|
||||
zIndex: 10000,
|
||||
items: {
|
||||
flip: {
|
||||
name: 'Flip',
|
||||
callback: () => {
|
||||
const { store } = APP;
|
||||
const val = !store.getState()['features/base/settings']
|
||||
.localFlipX;
|
||||
|
||||
this.setFlipX(val);
|
||||
store.dispatch(updateSettings({
|
||||
localFlipX: val
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
events: {
|
||||
show(options) {
|
||||
options.items.flip.name
|
||||
= APP.translation.generateTranslationHTML(
|
||||
'videothumbnail.flip');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the context menu for the local video.
|
||||
* @param enable {boolean} true for enable, false for disable
|
||||
*/
|
||||
_enableDisableContextMenu(enable) {
|
||||
if (this.$container.contextMenu) {
|
||||
this.$container.contextMenu(enable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the {@code LocalVideo} in the DOM based on the current video layout.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
updateDOMLocation() {
|
||||
if (!this.container) {
|
||||
return;
|
||||
}
|
||||
if (this.container.parentElement) {
|
||||
this.container.parentElement.removeChild(this.container);
|
||||
}
|
||||
|
||||
const appendTarget = shouldDisplayTileView(APP.store.getState())
|
||||
? document.getElementById('localVideoTileViewContainer')
|
||||
: document.getElementById('filmstripLocalVideoThumbnail');
|
||||
|
||||
appendTarget && appendTarget.appendChild(this.container);
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
/* global $, APP, config */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import {
|
||||
JitsiParticipantConnectionStatus
|
||||
} from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { getParticipantById } from '../../../react/features/base/participants';
|
||||
import { isTestModeEnabled } from '../../../react/features/base/testing';
|
||||
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
|
||||
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
|
||||
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIUtils from '../util/UIUtil';
|
||||
|
||||
import SmallVideo from './SmallVideo';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
/**
|
||||
* List of container events that we are going to process, will be added as listener to the
|
||||
* container for every event in the list. The latest event will be stored in redux.
|
||||
*/
|
||||
const containerEvents = [
|
||||
'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart',
|
||||
'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting'
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} spanId
|
||||
*/
|
||||
function createContainer(spanId) {
|
||||
const container = document.createElement('span');
|
||||
|
||||
container.id = spanId;
|
||||
container.className = 'videocontainer';
|
||||
|
||||
const remoteVideosContainer
|
||||
= document.getElementById('filmstripRemoteVideosContainer');
|
||||
const localVideoContainer
|
||||
= document.getElementById('localVideoTileViewContainer');
|
||||
|
||||
remoteVideosContainer.insertBefore(container, localVideoContainer);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class RemoteVideo extends SmallVideo {
|
||||
/**
|
||||
* Creates new instance of the <tt>RemoteVideo</tt>.
|
||||
* @param user {JitsiParticipant} the user for whom remote video instance will
|
||||
* be created.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(user) {
|
||||
super();
|
||||
|
||||
this.user = user;
|
||||
this.id = user.getId();
|
||||
this.videoSpanId = `participant_${this.id}`;
|
||||
|
||||
this.addRemoteVideoContainer();
|
||||
this.bindHoverHandler();
|
||||
this.flipX = false;
|
||||
this.isLocal = false;
|
||||
|
||||
/**
|
||||
* The flag is set to <tt>true</tt> after the 'canplay' event has been
|
||||
* triggered on the current video element. It goes back to <tt>false</tt>
|
||||
* when the stream is removed. It is used to determine whether the video
|
||||
* playback has ever started.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._canPlayEventReceived = false;
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
addRemoteVideoContainer() {
|
||||
this.container = createContainer(this.videoSpanId);
|
||||
this.$container = $(this.container);
|
||||
this.renderThumbnail();
|
||||
this._setThumbnailSize();
|
||||
this.initBrowserSpecificProperties();
|
||||
|
||||
return this.container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>, this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the remote stream element corresponding to the given stream and
|
||||
* parent container.
|
||||
*
|
||||
* @param stream the MediaStream
|
||||
* @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
|
||||
*/
|
||||
removeRemoteStreamElement(stream) {
|
||||
if (!this.container) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isVideo = stream.isVideoTrack();
|
||||
const elementID = `remoteVideo_${stream.getId()}`;
|
||||
const select = $(`#${elementID}`);
|
||||
|
||||
select.remove();
|
||||
if (isVideo) {
|
||||
this._canPlayEventReceived = false;
|
||||
}
|
||||
|
||||
logger.info(`Video removed ${this.id}`, select);
|
||||
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* The remote video is considered "playable" once the can play event has been received.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @override
|
||||
*/
|
||||
isVideoPlayable() {
|
||||
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
updateView() {
|
||||
this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
|
||||
super.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes RemoteVideo from the page.
|
||||
*/
|
||||
remove() {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
super.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} streamElement
|
||||
* @param {*} stream
|
||||
*/
|
||||
waitForPlayback(streamElement, stream) {
|
||||
$(streamElement).hide();
|
||||
|
||||
const webRtcStream = stream.getOriginalStream();
|
||||
const isVideo = stream.isVideoTrack();
|
||||
|
||||
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
|
||||
return;
|
||||
}
|
||||
|
||||
const listener = () => {
|
||||
this._canPlayEventReceived = true;
|
||||
|
||||
logger.info(`${this.id} video is now active`, streamElement);
|
||||
if (streamElement) {
|
||||
$(streamElement).show();
|
||||
}
|
||||
|
||||
streamElement.removeEventListener('canplay', listener);
|
||||
|
||||
// Refresh to show the video
|
||||
this.updateView();
|
||||
};
|
||||
|
||||
streamElement.addEventListener('canplay', listener);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} stream
|
||||
*/
|
||||
addRemoteStreamElement(stream) {
|
||||
if (!this.container) {
|
||||
logger.debug('Not attaching remote stream due to no container');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isVideo = stream.isVideoTrack();
|
||||
|
||||
if (!stream.getOriginalStream()) {
|
||||
logger.debug('Remote video stream has no original stream');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let streamElement = document.createElement('video');
|
||||
|
||||
streamElement.autoplay = !config.testing?.noAutoPlayVideo;
|
||||
streamElement.id = `remoteVideo_${stream.getId()}`;
|
||||
|
||||
// Put new stream element always in front
|
||||
streamElement = UIUtils.prependChild(this.container, streamElement);
|
||||
|
||||
this.waitForPlayback(streamElement, stream);
|
||||
stream.attach(streamElement);
|
||||
|
||||
if (isVideo && isTestModeEnabled(APP.store.getState())) {
|
||||
|
||||
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
|
||||
|
||||
containerEvents.forEach(event => {
|
||||
streamElement.addEventListener(event, cb.bind(this, event));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
/* global $, APP, interfaceConfig */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
|
||||
import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator';
|
||||
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import { MEDIA_TYPE } from '../../../react/features/base/media';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
getPinnedParticipant,
|
||||
pinParticipant
|
||||
} from '../../../react/features/base/participants';
|
||||
import {
|
||||
getLocalVideoTrack,
|
||||
getTrackByMediaTypeAndParticipant,
|
||||
isLocalTrackMuted,
|
||||
isRemoteTrackMuted
|
||||
} from '../../../react/features/base/tracks';
|
||||
import { ConnectionIndicator } from '../../../react/features/connection-indicator';
|
||||
import { DisplayName } from '../../../react/features/display-name';
|
||||
import {
|
||||
DominantSpeakerIndicator,
|
||||
RaisedHandIndicator,
|
||||
StatusIndicators,
|
||||
isVideoPlayable
|
||||
} from '../../../react/features/filmstrip';
|
||||
import {
|
||||
LAYOUTS,
|
||||
getCurrentLayout,
|
||||
setTileView,
|
||||
shouldDisplayTileView
|
||||
} from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Display mode constant used when video is being displayed on the small video.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_VIDEO = 0;
|
||||
|
||||
/**
|
||||
* Display mode constant used when the user's avatar is being displayed on
|
||||
* the small video.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_AVATAR = 1;
|
||||
|
||||
/**
|
||||
* Display mode constant used when neither video nor avatar is being displayed
|
||||
* on the small video. And we just show the display name.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_BLACKNESS_WITH_NAME = 2;
|
||||
|
||||
/**
|
||||
* Display mode constant used when video is displayed and display name
|
||||
* at the same time.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_VIDEO_WITH_NAME = 3;
|
||||
|
||||
/**
|
||||
* Display mode constant used when neither video nor avatar is being displayed
|
||||
* on the small video. And we just show the display name.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_AVATAR_WITH_NAME = 4;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class SmallVideo {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
constructor() {
|
||||
this.videoIsHovered = false;
|
||||
this.videoType = undefined;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this.updateView = this.updateView.bind(this);
|
||||
|
||||
this._onContainerClick = this._onContainerClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier of this small video.
|
||||
*
|
||||
* @returns the identifier of this small video
|
||||
*/
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this small video is currently visible.
|
||||
*
|
||||
* @return <tt>true</tt> if this small video isn't currently visible and
|
||||
* <tt>false</tt> - otherwise.
|
||||
*/
|
||||
isVisible() {
|
||||
return this.$container.is(':visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
|
||||
*/
|
||||
bindHoverHandler() {
|
||||
// Add hover handler
|
||||
this.$container.hover(
|
||||
() => {
|
||||
this.videoIsHovered = true;
|
||||
this.renderThumbnail(true);
|
||||
this.updateView();
|
||||
},
|
||||
() => {
|
||||
this.videoIsHovered = false;
|
||||
this.renderThumbnail(false);
|
||||
this.updateView();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
renderThumbnail() {
|
||||
// Should be implemented by in subclasses.
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an especially interesting function. A naive reader might think that
|
||||
* it returns this SmallVideo's "video" element. But it is much more exciting.
|
||||
* It first finds this video's parent element using jquery, then uses a utility
|
||||
* from lib-jitsi-meet to extract the video element from it (with two more
|
||||
* jquery calls), and finally uses jquery again to encapsulate the video element
|
||||
* in an array. This last step allows (some might prefer "forces") users of
|
||||
* this function to access the video element via the 0th element of the returned
|
||||
* array (after checking its length of course!).
|
||||
*/
|
||||
selectVideoElement() {
|
||||
return $($(this.container).find('video')[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables / disables the css responsible for focusing/pinning a video
|
||||
* thumbnail.
|
||||
*
|
||||
* @param isFocused indicates if the thumbnail should be focused/pinned or not
|
||||
*/
|
||||
focus(isFocused) {
|
||||
const focusedCssClass = 'videoContainerFocused';
|
||||
const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
|
||||
|
||||
if (!isFocused && isFocusClassEnabled) {
|
||||
this.$container.removeClass(focusedCssClass);
|
||||
} else if (isFocused && !isFocusClassEnabled) {
|
||||
this.$container.addClass(focusedCssClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
hasVideo() {
|
||||
return this.selectVideoElement().length !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user associated with this <tt>SmallVideo</tt> is currently
|
||||
* being displayed on the "large video".
|
||||
*
|
||||
* @return {boolean} <tt>true</tt> if the user is displayed on the large video
|
||||
* or <tt>false</tt> otherwise.
|
||||
*/
|
||||
isCurrentlyOnLargeVideo() {
|
||||
return APP.store.getState()['features/large-video']?.participantId === this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is a playable video stream available for the user
|
||||
* associated with this <tt>SmallVideo</tt>.
|
||||
*
|
||||
* @return {boolean} <tt>true</tt> if there is a playable video stream available
|
||||
* or <tt>false</tt> otherwise.
|
||||
*/
|
||||
isVideoPlayable() {
|
||||
return isVideoPlayable(APP.store.getState(), this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what should be display on the thumbnail.
|
||||
*
|
||||
* @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
|
||||
* or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
|
||||
*/
|
||||
selectDisplayMode(input) {
|
||||
if (!input.tileViewActive && input.isScreenSharing) {
|
||||
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
||||
} else if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
|
||||
// Display name is always and only displayed when user is on the stage
|
||||
return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
|
||||
} else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
|
||||
// check hovering and change state to video with name
|
||||
return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
|
||||
}
|
||||
|
||||
// check hovering and change state to avatar with name
|
||||
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes information that determine the display mode.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
computeDisplayModeInput() {
|
||||
let isScreenSharing = false;
|
||||
let connectionStatus;
|
||||
const state = APP.store.getState();
|
||||
const id = this.id;
|
||||
const participant = getParticipantById(state, id);
|
||||
const isLocal = participant?.local ?? true;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const videoTrack
|
||||
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
||||
|
||||
if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
|
||||
isScreenSharing = videoTrack?.videoType === 'desktop';
|
||||
connectionStatus = participant.connectionStatus;
|
||||
}
|
||||
|
||||
return {
|
||||
isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
|
||||
isHovered: this._isHovered(),
|
||||
isAudioOnly: APP.conference.isAudioOnly(),
|
||||
tileViewActive: shouldDisplayTileView(state),
|
||||
isVideoPlayable: this.isVideoPlayable(),
|
||||
hasVideo: Boolean(this.selectVideoElement().length),
|
||||
connectionStatus,
|
||||
canPlayEventReceived: this._canPlayEventReceived,
|
||||
videoStream: Boolean(videoTrack),
|
||||
isScreenSharing,
|
||||
videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether current video is considered hovered. Currently it is hovered
|
||||
* if the mouse is over the video, or if the connection
|
||||
* indicator is shown(hovered).
|
||||
* @private
|
||||
*/
|
||||
_isHovered() {
|
||||
return this.videoIsHovered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the css classes of the thumbnail based on the current state.
|
||||
*/
|
||||
updateView() {
|
||||
this.$container.removeClass((index, classNames) =>
|
||||
classNames.split(' ').filter(name => name.startsWith('display-')));
|
||||
|
||||
const oldDisplayMode = this.displayMode;
|
||||
let displayModeString = '';
|
||||
|
||||
const displayModeInput = this.computeDisplayModeInput();
|
||||
|
||||
// Determine whether video, avatar or blackness should be displayed
|
||||
this.displayMode = this.selectDisplayMode(displayModeInput);
|
||||
|
||||
switch (this.displayMode) {
|
||||
case DISPLAY_AVATAR_WITH_NAME:
|
||||
displayModeString = 'avatar-with-name';
|
||||
this.$container.addClass('display-avatar-with-name');
|
||||
break;
|
||||
case DISPLAY_BLACKNESS_WITH_NAME:
|
||||
displayModeString = 'blackness-with-name';
|
||||
this.$container.addClass('display-name-on-black');
|
||||
break;
|
||||
case DISPLAY_VIDEO:
|
||||
displayModeString = 'video';
|
||||
this.$container.addClass('display-video');
|
||||
break;
|
||||
case DISPLAY_VIDEO_WITH_NAME:
|
||||
displayModeString = 'video-with-name';
|
||||
this.$container.addClass('display-name-on-video');
|
||||
break;
|
||||
case DISPLAY_AVATAR:
|
||||
default:
|
||||
displayModeString = 'avatar';
|
||||
this.$container.addClass('display-avatar-only');
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.displayMode !== oldDisplayMode) {
|
||||
logger.debug(`Displaying ${displayModeString} for ${this.id}, data: [${JSON.stringify(displayModeInput)}]`);
|
||||
}
|
||||
|
||||
if (this.displayMode !== DISPLAY_VIDEO
|
||||
&& this.displayMode !== DISPLAY_VIDEO_WITH_NAME
|
||||
&& displayModeInput.tileViewActive
|
||||
&& displayModeInput.isScreenSharing
|
||||
&& !displayModeInput.isAudioOnly) {
|
||||
// send the event
|
||||
sendAnalytics(createScreenSharingIssueEvent({
|
||||
source: 'thumbnail',
|
||||
...displayModeInput
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides the dominant speaker indicator.
|
||||
* @param show whether to show or hide.
|
||||
*/
|
||||
showDominantSpeakerIndicator(show) {
|
||||
// Don't create and show dominant speaker indicator if
|
||||
// DISABLE_DOMINANT_SPEAKER_INDICATOR is true
|
||||
if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.container) {
|
||||
logger.warn(`Unable to set dominant speaker indicator - ${this.videoSpanId} does not exist`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$container.toggleClass('active-speaker', show);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initalizes any browser specific properties. Currently sets the overflow
|
||||
* property for Qt browsers on Windows to hidden, thus fixing the following
|
||||
* problem:
|
||||
* Some browsers don't have full support of the object-fit property for the
|
||||
* video element and when we set video object-fit to "cover" the video
|
||||
* actually overflows the boundaries of its container, so it's important
|
||||
* to indicate that the "overflow" should be hidden.
|
||||
*
|
||||
* Setting this property for all browsers will result in broken audio levels,
|
||||
* which makes this a temporary solution, before reworking audio levels.
|
||||
*/
|
||||
initBrowserSpecificProperties() {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
|
||||
if (userAgent.indexOf('QtWebEngine') > -1
|
||||
&& (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1)) {
|
||||
this.$container.css('overflow', 'hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up components on {@code SmallVideo} and removes itself from the DOM.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
remove() {
|
||||
logger.log('Remove thumbnail', this.id);
|
||||
this._unmountThumbnail();
|
||||
|
||||
// Remove whole container
|
||||
if (this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for re-rendering multiple react components of the small
|
||||
* video.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
rerender() {
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the thumbnail is clicked and potentially trigger
|
||||
* pinning of the participant.
|
||||
*
|
||||
* @param {MouseEvent} event - The click event to intercept.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onContainerClick(event) {
|
||||
const triggerPin = this._shouldTriggerPin(event);
|
||||
|
||||
if (event.stopPropagation && triggerPin) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if (triggerPin) {
|
||||
this.togglePin();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a click event is targeted at certain elements which
|
||||
* should not trigger a pin.
|
||||
*
|
||||
* @param {MouseEvent} event - The click event to intercept.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_shouldTriggerPin(event) {
|
||||
// TODO Checking the classes is a workround to allow events to bubble into
|
||||
// the DisplayName component if it was clicked. React's synthetic events
|
||||
// will fire after jQuery handlers execute, so stop propogation at this
|
||||
// point will prevent DisplayName from getting click events. This workaround
|
||||
// should be removeable once LocalVideo is a React Component because then
|
||||
// the components share the same eventing system.
|
||||
const $source = $(event.target || event.srcElement);
|
||||
|
||||
return $source.parents('.displayNameContainer').length === 0
|
||||
&& $source.parents('.popover').length === 0
|
||||
&& !event.target.classList.contains('popover');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins the participant displayed by this thumbnail or unpins if already pinned.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
togglePin() {
|
||||
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
|
||||
const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
|
||||
|
||||
APP.store.dispatch(pinParticipant(participantIdToPin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmounts the thumbnail.
|
||||
*/
|
||||
_unmountThumbnail() {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the thumbnail.
|
||||
*/
|
||||
_setThumbnailSize() {
|
||||
const layout = getCurrentLayout(APP.store.getState());
|
||||
const heightToWidthPercent = 100
|
||||
/ (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO);
|
||||
|
||||
switch (layout) {
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
this.$container.css('padding-top', `${heightToWidthPercent}%`);
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const state = APP.store.getState();
|
||||
const { local, remote } = state['features/filmstrip'].horizontalViewDimensions;
|
||||
const size = this.isLocal ? local : remote;
|
||||
|
||||
if (typeof size !== 'undefined') {
|
||||
const { height, width } = size;
|
||||
|
||||
this.$container.css({
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const state = APP.store.getState();
|
||||
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
|
||||
|
||||
if (typeof thumbnailSize !== 'undefined') {
|
||||
const { height, width } = thumbnailSize;
|
||||
|
||||
this.$container.css({
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { isTestModeEnabled } from '../../../react/features/base/testing';
|
||||
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
|
||||
import Filmstrip from './Filmstrip';
|
||||
@@ -187,16 +186,13 @@ export class VideoContainer extends LargeContainer {
|
||||
* Creates new VideoContainer instance.
|
||||
* @param resizeContainer {Function} function that takes care of the size
|
||||
* of the video container.
|
||||
* @param emitter {EventEmitter} the event emitter that will be used by
|
||||
* this instance.
|
||||
*/
|
||||
constructor(resizeContainer, emitter) {
|
||||
constructor(resizeContainer) {
|
||||
super();
|
||||
this.stream = null;
|
||||
this.userId = null;
|
||||
this.videoType = null;
|
||||
this.localFlipX = true;
|
||||
this.emitter = emitter;
|
||||
this.resizeContainer = resizeContainer;
|
||||
|
||||
/**
|
||||
@@ -534,7 +530,6 @@ export class VideoContainer extends LargeContainer {
|
||||
this.$avatar.css('visibility', show ? 'visible' : 'hidden');
|
||||
this.avatarDisplayed = show;
|
||||
|
||||
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE, show);
|
||||
APP.API.notifyLargeVideoVisibilityChanged(show);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,88 +4,29 @@ import Logger from 'jitsi-meet-logger';
|
||||
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
|
||||
import {
|
||||
getLocalParticipant as getLocalParticipantFromStore,
|
||||
getPinnedParticipant,
|
||||
getParticipantById,
|
||||
pinParticipant
|
||||
getParticipantById
|
||||
} from '../../../react/features/base/participants';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
||||
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
||||
|
||||
import LargeVideoManager from './LargeVideoManager';
|
||||
import LocalVideo from './LocalVideo';
|
||||
import RemoteVideo from './RemoteVideo';
|
||||
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
const remoteVideos = {};
|
||||
let localVideoThumbnail = null;
|
||||
|
||||
let eventEmitter = null;
|
||||
|
||||
let largeVideo;
|
||||
|
||||
/**
|
||||
* flipX state of the localVideo
|
||||
*/
|
||||
let localFlipX = null;
|
||||
|
||||
/**
|
||||
* Handler for local flip X changed event.
|
||||
* @param {Object} val
|
||||
*/
|
||||
function onLocalFlipXChanged(val) {
|
||||
localFlipX = val;
|
||||
if (largeVideo) {
|
||||
largeVideo.onLocalFlipXChange(val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all thumbnails in the filmstrip.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array}
|
||||
*/
|
||||
function getAllThumbnails() {
|
||||
return [
|
||||
...localVideoThumbnail ? [ localVideoThumbnail ] : [],
|
||||
...Object.values(remoteVideos)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper to get the redux representation of the local participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getLocalParticipant() {
|
||||
return getLocalParticipantFromStore(APP.store.getState());
|
||||
}
|
||||
|
||||
const VideoLayout = {
|
||||
init(emitter) {
|
||||
eventEmitter = emitter;
|
||||
|
||||
localVideoThumbnail = new LocalVideo(
|
||||
emitter,
|
||||
this._updateLargeVideoIfDisplayed.bind(this));
|
||||
|
||||
this.registerListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
* Registering listeners for UI events in Video layout component.
|
||||
*
|
||||
* @returns {void}
|
||||
* Handler for local flip X changed event.
|
||||
*/
|
||||
registerListeners() {
|
||||
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
|
||||
onLocalFlipXChanged);
|
||||
onLocalFlipXChanged() {
|
||||
if (largeVideo) {
|
||||
const { store } = APP;
|
||||
const { localFlipX } = store.getState()['features/base/settings'];
|
||||
|
||||
largeVideo.onLocalFlipXChange(localFlipX);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -95,13 +36,16 @@ const VideoLayout = {
|
||||
*/
|
||||
reset() {
|
||||
this._resetLargeVideo();
|
||||
this._resetFilmstrip();
|
||||
},
|
||||
|
||||
initLargeVideo() {
|
||||
this._resetLargeVideo();
|
||||
|
||||
largeVideo = new LargeVideoManager(eventEmitter);
|
||||
largeVideo = new LargeVideoManager();
|
||||
|
||||
const { store } = APP;
|
||||
const { localFlipX } = store.getState()['features/base/settings'];
|
||||
|
||||
if (localFlipX) {
|
||||
largeVideo.onLocalFlipXChange(localFlipX);
|
||||
}
|
||||
@@ -120,55 +64,6 @@ const VideoLayout = {
|
||||
}
|
||||
},
|
||||
|
||||
changeLocalVideo(stream) {
|
||||
const localId = getLocalParticipant().id;
|
||||
|
||||
this.onVideoTypeChanged(localId, stream.videoType);
|
||||
|
||||
localVideoThumbnail.changeVideo(stream);
|
||||
|
||||
this._updateLargeVideoIfDisplayed(localId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides local video.
|
||||
* @param {boolean} true to make the local video visible, false - otherwise
|
||||
*/
|
||||
setLocalVideoVisible(visible) {
|
||||
localVideoThumbnail.setVisible(visible);
|
||||
},
|
||||
|
||||
onRemoteStreamAdded(stream) {
|
||||
const id = stream.getParticipantId();
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
logger.debug(`Received a new ${stream.getType()} stream for ${id}`);
|
||||
|
||||
if (!remoteVideo) {
|
||||
logger.debug('No remote video element to add stream');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
remoteVideo.addRemoteStreamElement(stream);
|
||||
|
||||
this.onVideoMute(id);
|
||||
remoteVideo.updateView();
|
||||
},
|
||||
|
||||
onRemoteStreamRemoved(stream) {
|
||||
const id = stream.getParticipantId();
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
// Remote stream may be removed after participant left the conference.
|
||||
if (remoteVideo) {
|
||||
remoteVideo.removeRemoteStreamElement(stream);
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
|
||||
this.updateVideoMutedForNoTracks(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* FIXME get rid of this method once muted indicator are reactified (by
|
||||
* making sure that user with no tracks is displayed as muted )
|
||||
@@ -180,7 +75,7 @@ const VideoLayout = {
|
||||
const participant = APP.conference.getParticipantById(participantId);
|
||||
|
||||
if (participant && !participant.getTracksByMediaType('video').length) {
|
||||
APP.UI.setVideoMuted(participantId);
|
||||
VideoLayout._updateLargeVideoIfDisplayed(participantId, true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -202,110 +97,12 @@ const VideoLayout = {
|
||||
return videoTrack?.videoType;
|
||||
},
|
||||
|
||||
isPinned(id) {
|
||||
return id === this.getPinnedId();
|
||||
},
|
||||
|
||||
getPinnedId() {
|
||||
const { id } = getPinnedParticipant(APP.store.getState()) || {};
|
||||
|
||||
return id || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a thumbnail to pin or unpin itself.
|
||||
*
|
||||
* @param {number} videoNumber - The index of the video to toggle pin on.
|
||||
* @private
|
||||
*/
|
||||
togglePin(videoNumber) {
|
||||
const videos = getAllThumbnails();
|
||||
const videoView = videos[videoNumber];
|
||||
|
||||
videoView && videoView.togglePin();
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback invoked to update display when the pin participant has changed.
|
||||
*
|
||||
* @paramn {string|null} pinnedParticipantID - The participant ID of the
|
||||
* participant that is pinned or null if no one is pinned.
|
||||
* @returns {void}
|
||||
*/
|
||||
onPinChange(pinnedParticipantID) {
|
||||
getAllThumbnails().forEach(thumbnail =>
|
||||
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a participant container for the given id.
|
||||
*
|
||||
* @param {Object} participant - The redux representation of a remote
|
||||
* participant.
|
||||
* @returns {void}
|
||||
*/
|
||||
addRemoteParticipantContainer(participant) {
|
||||
if (!participant || participant.local) {
|
||||
return;
|
||||
} else if (participant.isFakeParticipant) {
|
||||
const sharedVideoThumb = new SharedVideoThumb(participant);
|
||||
|
||||
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const id = participant.id;
|
||||
const jitsiParticipant = APP.conference.getParticipantById(id);
|
||||
const remoteVideo = new RemoteVideo(jitsiParticipant);
|
||||
|
||||
this.addRemoteVideoContainer(id, remoteVideo);
|
||||
this.updateVideoMutedForNoTracks(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds remote video container for the given id and <tt>SmallVideo</tt>.
|
||||
*
|
||||
* @param {string} the id of the video to add
|
||||
* @param {SmallVideo} smallVideo the small video instance to add as a
|
||||
* remote video
|
||||
*/
|
||||
addRemoteVideoContainer(id, remoteVideo) {
|
||||
remoteVideos[id] = remoteVideo;
|
||||
|
||||
// Initialize the view
|
||||
remoteVideo.updateView();
|
||||
},
|
||||
|
||||
/**
|
||||
* On video muted event.
|
||||
*/
|
||||
onVideoMute(id) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
localVideoThumbnail && localVideoThumbnail.updateView();
|
||||
} else {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
}
|
||||
|
||||
// large video will show avatar instead of muted stream
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* On dominant speaker changed event.
|
||||
*
|
||||
* @param {string} id - The participant ID of the new dominant speaker.
|
||||
* @returns {void}
|
||||
*/
|
||||
onDominantSpeakerChanged(id) {
|
||||
getAllThumbnails().forEach(thumbnail =>
|
||||
thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides warning about a user's connectivity issues.
|
||||
*
|
||||
@@ -321,12 +118,6 @@ const VideoLayout = {
|
||||
// We have to trigger full large video update to transition from
|
||||
// avatar to video on connectivity restored.
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -339,58 +130,14 @@ const VideoLayout = {
|
||||
*/
|
||||
onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
|
||||
if (endpointsLeavingLastN) {
|
||||
endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
|
||||
endpointsLeavingLastN.forEach(this._updateLargeVideoIfDisplayed, this);
|
||||
}
|
||||
|
||||
if (endpointsEnteringLastN) {
|
||||
endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
|
||||
endpointsEnteringLastN.forEach(this._updateLargeVideoIfDisplayed, this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates remote video by id if it exists.
|
||||
* @param {string} id of the remote video
|
||||
* @private
|
||||
*/
|
||||
_updateRemoteVideo(id) {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
this._updateLargeVideoIfDisplayed(id);
|
||||
}
|
||||
},
|
||||
|
||||
removeParticipantContainer(id) {
|
||||
// Unlock large video
|
||||
if (this.getPinnedId() === id) {
|
||||
logger.info('Focused video owner has left the conference');
|
||||
APP.store.dispatch(pinParticipant(null));
|
||||
}
|
||||
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
// Remove remote video
|
||||
logger.info(`Removing remote video: ${id}`);
|
||||
delete remoteVideos[id];
|
||||
remoteVideo.remove();
|
||||
} else {
|
||||
logger.warn(`No remote video for ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
onVideoTypeChanged(id, newVideoType) {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (!remoteVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Peer video type changed: ', id, newVideoType);
|
||||
remoteVideo.updateView();
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes the video area.
|
||||
*/
|
||||
@@ -401,15 +148,6 @@ const VideoLayout = {
|
||||
}
|
||||
},
|
||||
|
||||
getSmallVideo(id) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
return localVideoThumbnail;
|
||||
}
|
||||
|
||||
return remoteVideos[id];
|
||||
|
||||
},
|
||||
|
||||
changeUserAvatar(id, avatarUrl) {
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
largeVideo.updateAvatar(avatarUrl);
|
||||
@@ -432,24 +170,6 @@ const VideoLayout = {
|
||||
return largeVideo && largeVideo.id === id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers an update of remote video and large video displays so they may
|
||||
* pick up any state changes that have occurred elsewhere.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
updateAllVideos() {
|
||||
const displayedUserId = this.getLargeVideoID();
|
||||
|
||||
if (displayedUserId) {
|
||||
this.updateLargeVideo(displayedUserId, true);
|
||||
}
|
||||
|
||||
Object.keys(remoteVideos).forEach(video => {
|
||||
remoteVideos[video].updateView();
|
||||
});
|
||||
},
|
||||
|
||||
updateLargeVideo(id, forceUpdate) {
|
||||
if (!largeVideo) {
|
||||
return;
|
||||
@@ -510,13 +230,6 @@ const VideoLayout = {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const currentId = largeVideo.id;
|
||||
let oldSmallVideo;
|
||||
|
||||
if (currentId) {
|
||||
oldSmallVideo = this.getSmallVideo(currentId);
|
||||
}
|
||||
|
||||
let containerTypeToShow = type;
|
||||
|
||||
// if we are hiding a container and there is focusedVideo
|
||||
@@ -533,12 +246,7 @@ const VideoLayout = {
|
||||
}
|
||||
}
|
||||
|
||||
return largeVideo.showContainer(containerTypeToShow)
|
||||
.then(() => {
|
||||
if (oldSmallVideo) {
|
||||
oldSmallVideo && oldSmallVideo.updateView();
|
||||
}
|
||||
});
|
||||
return largeVideo.showContainer(containerTypeToShow);
|
||||
},
|
||||
|
||||
isLargeContainerTypeVisible(type) {
|
||||
@@ -561,14 +269,6 @@ const VideoLayout = {
|
||||
return largeVideo;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the flipX state of the local video.
|
||||
* @param {boolean} true for flipped otherwise false;
|
||||
*/
|
||||
setLocalFlipX(val) {
|
||||
this.localFlipX = val;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the wrapper jquery selector for the largeVideo
|
||||
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
|
||||
@@ -577,15 +277,6 @@ const VideoLayout = {
|
||||
return this.getCurrentlyOnLargeContainer().$wrapper;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the number of remove video ids.
|
||||
*
|
||||
* @returns {number} The number of remote videos.
|
||||
*/
|
||||
getRemoteVideosCount() {
|
||||
return Object.keys(remoteVideos).length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to invoke when the video layout has changed and elements
|
||||
* have to be re-arranged and resized.
|
||||
@@ -593,12 +284,7 @@ const VideoLayout = {
|
||||
* @returns {void}
|
||||
*/
|
||||
refreshLayout() {
|
||||
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
|
||||
VideoLayout.resizeVideoArea();
|
||||
|
||||
// Rerender the thumbnails since they are dependant on the layout because of the tooltip positioning.
|
||||
localVideoThumbnail && localVideoThumbnail.rerender();
|
||||
Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender());
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -615,26 +301,6 @@ const VideoLayout = {
|
||||
largeVideo = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleans up filmstrip state. While a separate {@code Filmstrip} exists, its
|
||||
* implementation is mainly for querying and manipulating the DOM while
|
||||
* state mostly remains in {@code VideoLayout}.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_resetFilmstrip() {
|
||||
Object.keys(remoteVideos).forEach(remoteVideoId => {
|
||||
this.removeParticipantContainer(remoteVideoId);
|
||||
delete remoteVideos[remoteVideoId];
|
||||
});
|
||||
|
||||
if (localVideoThumbnail) {
|
||||
localVideoThumbnail.remove();
|
||||
localVideoThumbnail = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers an update of large video if the passed in participant is
|
||||
* currently displayed on large video.
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
sendAnalytics
|
||||
} from '../../react/features/analytics';
|
||||
import { toggleDialog } from '../../react/features/base/dialog';
|
||||
import { clickOnVideo } from '../../react/features/filmstrip/actions';
|
||||
import { KeyboardShortcutsDialog }
|
||||
from '../../react/features/keyboard-shortcuts';
|
||||
import { SpeakerStats } from '../../react/features/speaker-stats';
|
||||
@@ -54,7 +55,7 @@ const KeyboardShortcut = {
|
||||
if (_shortcuts.has(key)) {
|
||||
_shortcuts.get(key).function(e);
|
||||
} else if (!isNaN(num) && num >= 0 && num <= 9) {
|
||||
APP.UI.clickOnVideo(num);
|
||||
APP.store.dispatch(clickOnVideo(num));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
3437
package-lock.json
generated
3437
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -15,23 +15,23 @@
|
||||
"author": "",
|
||||
"readmeFilename": "README.md",
|
||||
"dependencies": {
|
||||
"@atlaskit/button": "10.1.1",
|
||||
"@atlaskit/checkbox": "5.0.10",
|
||||
"@atlaskit/dropdown-menu": "6.1.25",
|
||||
"@atlaskit/field-text": "7.0.19",
|
||||
"@atlaskit/field-text-area": "4.0.15",
|
||||
"@atlaskit/flag": "13.0.0",
|
||||
"@atlaskit/icon": "15.0.3",
|
||||
"@atlaskit/inline-dialog": "5.3.0",
|
||||
"@atlaskit/inline-message": "7.0.10",
|
||||
"@atlaskit/lozenge": "6.2.4",
|
||||
"@atlaskit/modal-dialog": "8.0.1",
|
||||
"@atlaskit/multi-select": "11.0.13",
|
||||
"@atlaskit/spinner": "9.0.13",
|
||||
"@atlaskit/tabs": "8.0.11",
|
||||
"@atlaskit/theme": "7.0.2",
|
||||
"@atlaskit/toggle": "5.0.14",
|
||||
"@atlaskit/tooltip": "12.1.13",
|
||||
"@atlaskit/button": "15.1.4",
|
||||
"@atlaskit/checkbox": "12.0.0",
|
||||
"@atlaskit/dropdown-menu": "10.1.2",
|
||||
"@atlaskit/field-text": "11.0.4",
|
||||
"@atlaskit/field-text-area": "8.0.4",
|
||||
"@atlaskit/flag": "14.1.0",
|
||||
"@atlaskit/icon": "21.2.0",
|
||||
"@atlaskit/inline-dialog": "13.0.9",
|
||||
"@atlaskit/inline-message": "11.0.8",
|
||||
"@atlaskit/lozenge": "10.1.1",
|
||||
"@atlaskit/modal-dialog": "11.2.4",
|
||||
"@atlaskit/multi-select": "15.0.5",
|
||||
"@atlaskit/spinner": "15.0.6",
|
||||
"@atlaskit/tabs": "12.1.2",
|
||||
"@atlaskit/theme": "11.0.2",
|
||||
"@atlaskit/toggle": "12.0.3",
|
||||
"@atlaskit/tooltip": "17.1.2",
|
||||
"@jitsi/js-utils": "1.0.5",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
"@react-native-async-storage/async-storage": "1.13.2",
|
||||
@@ -52,11 +52,10 @@
|
||||
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
|
||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
|
||||
"jquery": "3.5.1",
|
||||
"jquery-contextmenu": "2.4.5",
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#be18ff34bedf38c7475fe4953074c7959000e15f",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#beaff3dd02e9e99da2f61c2a9ed5ca3a34d51dcf",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.19",
|
||||
"moment": "2.19.4",
|
||||
@@ -64,8 +63,8 @@
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"pixelmatch": "5.1.0",
|
||||
"punycode": "2.1.1",
|
||||
"react": "16.9",
|
||||
"react-dom": "16.9",
|
||||
"react": "16.12",
|
||||
"react-dom": "16.12",
|
||||
"react-emoji-render": "1.2.4",
|
||||
"react-i18next": "10.11.4",
|
||||
"react-linkify": "1.0.0-alpha",
|
||||
@@ -95,10 +94,10 @@
|
||||
"redux-thunk": "2.2.0",
|
||||
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
|
||||
"rtcstats": "github:jitsi/rtcstats#v6.2.0",
|
||||
"stackblur-canvas": "2.3.0",
|
||||
"styled-components": "3.4.9",
|
||||
"util": "0.12.1",
|
||||
"uuid": "3.1.0",
|
||||
"wasm-check": "2.0.1",
|
||||
"windows-iana": "^3.1.0",
|
||||
"xmldom": "0.1.27",
|
||||
"zxcvbn": "4.4.2"
|
||||
|
||||
@@ -29,7 +29,6 @@ import '../blur/reducer';
|
||||
import '../calendar-sync/reducer';
|
||||
import '../chat/reducer';
|
||||
import '../deep-linking/reducer';
|
||||
import '../device-selection/reducer';
|
||||
import '../dropbox/reducer';
|
||||
import '../dynamic-branding/reducer';
|
||||
import '../etherpad/reducer';
|
||||
|
||||
@@ -4,6 +4,7 @@ import '../authentication/reducer';
|
||||
import '../mobile/audio-mode/reducer';
|
||||
import '../mobile/background/reducer';
|
||||
import '../mobile/call-integration/reducer';
|
||||
import '../mobile/external-api/reducer';
|
||||
import '../mobile/full-screen/reducer';
|
||||
import '../mobile/incoming-call/reducer';
|
||||
import '../mobile/watchos/reducer';
|
||||
|
||||
@@ -62,6 +62,16 @@ export const CONFERENCE_SUBJECT_CHANGED = 'CONFERENCE_SUBJECT_CHANGED';
|
||||
*/
|
||||
export const CONFERENCE_TIMESTAMP_CHANGED = 'CONFERENCE_TIMESTAMP_CHANGED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that an uuid for a conference has been set.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_UNIQUE_ID_SET,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_UNIQUE_ID_SET = 'CONFERENCE_UNIQUE_ID_SET';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific conference will be
|
||||
* joined.
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_SUBJECT_CHANGED,
|
||||
CONFERENCE_TIMESTAMP_CHANGED,
|
||||
CONFERENCE_UNIQUE_ID_SET,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
DATA_CHANNEL_OPENED,
|
||||
@@ -327,6 +328,22 @@ export function conferenceTimestampChanged(conferenceTimestamp: number) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the unique identifier for conference has been set.
|
||||
*
|
||||
* @param {JitsiConference} conference - The JitsiConference instance, where the uuid has been set.
|
||||
* @returns {{
|
||||
* type: CONFERENCE_UNIQUE_ID_SET,
|
||||
* conference: JitsiConference,
|
||||
* }}
|
||||
*/
|
||||
export function conferenceUniqueIdSet(conference: Object) {
|
||||
return {
|
||||
type: CONFERENCE_UNIQUE_ID_SET,
|
||||
conference
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds any existing local tracks to a specific conference before the conference
|
||||
* is joined. Then signals the intention of the application to have the local
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createPinnedEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { reloadNow } from '../../app/actions';
|
||||
import { openDisplayNamePrompt } from '../../display-name';
|
||||
import { showErrorNotification } from '../../notifications';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
|
||||
@@ -117,6 +118,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
function _conferenceFailed({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
const { conference, error } = action;
|
||||
const { enableForcedReload } = getState()['features/base/config'];
|
||||
|
||||
// Handle specific failure reasons.
|
||||
switch (error.name) {
|
||||
@@ -130,6 +132,16 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
|
||||
|
||||
break;
|
||||
}
|
||||
case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
|
||||
if (enableForcedReload) {
|
||||
dispatch(showErrorNotification({
|
||||
description: 'Restart initiated because of a bridge failure',
|
||||
titleKey: 'dialog.sessionRestarted'
|
||||
}));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
||||
const [ msg ] = error.params;
|
||||
|
||||
@@ -147,26 +159,26 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
|
||||
break;
|
||||
}
|
||||
|
||||
// FIXME: Workaround for the web version. Currently, the creation of the
|
||||
// conference is handled by /conference.js and appropriate failure handlers
|
||||
// are set there.
|
||||
if (typeof APP !== 'undefined') {
|
||||
if (typeof beforeUnloadHandler !== 'undefined') {
|
||||
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
||||
beforeUnloadHandler = undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// XXX After next(action), it is clear whether the error is recoverable.
|
||||
!error.recoverable
|
||||
if (typeof APP === 'undefined') {
|
||||
!error.recoverable
|
||||
&& conference
|
||||
&& conference.leave().catch(reason => {
|
||||
// Even though we don't care too much about the failure, it may be
|
||||
// good to know that it happen, so log it (on the info level).
|
||||
logger.info('JitsiConference.leave() rejected with:', reason);
|
||||
});
|
||||
} else if (typeof beforeUnloadHandler !== 'undefined') {
|
||||
// FIXME: Workaround for the web version. Currently, the creation of the
|
||||
// conference is handled by /conference.js and appropriate failure handlers
|
||||
// are set there.
|
||||
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
||||
beforeUnloadHandler = undefined;
|
||||
}
|
||||
|
||||
if (enableForcedReload && error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
|
||||
dispatch(conferenceWillLeave(conference));
|
||||
dispatch(reloadNow());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
// @flow
|
||||
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import { setPrejoinPageVisibility, setSkipPrejoinOnReload } from '../../prejoin';
|
||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
|
||||
|
||||
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from './actionTypes';
|
||||
import './middleware.any';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
MiddlewareRegistry.register((/* store */) => next => action => {
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const { enableForcedReload } = getState()['features/base/config'];
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
if (enableForcedReload) {
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
dispatch(setSkipPrejoinOnReload(false));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_FAILED: {
|
||||
enableForcedReload
|
||||
&& action.error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED
|
||||
&& dispatch(setSkipPrejoinOnReload(true));
|
||||
|
||||
break;
|
||||
}
|
||||
case TOGGLE_SCREENSHARING: {
|
||||
if (typeof APP === 'object') {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
|
||||
|
||||
@@ -89,6 +89,7 @@ export default [
|
||||
'disableProfile',
|
||||
'disableRemoteControl',
|
||||
'disableRemoteMute',
|
||||
'disableResponsiveTiles',
|
||||
'disableRtx',
|
||||
'disableShortcuts',
|
||||
'disableSimulcast',
|
||||
@@ -96,6 +97,7 @@ export default [
|
||||
'disableTileView',
|
||||
'displayJids',
|
||||
'doNotStoreRoom',
|
||||
'dropbox',
|
||||
'e2eping',
|
||||
'enableDisplayNameInStats',
|
||||
'enableEmailInStats',
|
||||
|
||||
@@ -84,3 +84,13 @@ export const REMOVE_PENDING_DEVICE_REQUESTS = 'REMOVE_PENDING_DEVICE_REQUESTS';
|
||||
* }
|
||||
*/
|
||||
export const CHECK_AND_NOTIFY_FOR_NEW_DEVICE = 'CHECK_AND_NOTIFY_FOR_NEW_DEVICE';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the device permissions have changed.
|
||||
*
|
||||
* {
|
||||
* type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE
|
||||
* permissions: Object
|
||||
* }
|
||||
*/
|
||||
export const DEVICE_PERMISSIONS_CHANGED = 'DEVICE_PERMISSIONS_CHANGED';
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
ADD_PENDING_DEVICE_REQUEST,
|
||||
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
DEVICE_PERMISSIONS_CHANGED,
|
||||
NOTIFY_CAMERA_ERROR,
|
||||
NOTIFY_MIC_ERROR,
|
||||
REMOVE_PENDING_DEVICE_REQUESTS,
|
||||
@@ -320,3 +321,19 @@ export function checkAndNotifyForNewDevice(newDevices, oldDevices) {
|
||||
oldDevices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the device permissions have changed.
|
||||
*
|
||||
* @param {Object} permissions - Object with the permissions.
|
||||
* @returns {{
|
||||
* type: DEVICE_PERMISSIONS_CHANGED,
|
||||
* permissions: Object
|
||||
* }}
|
||||
*/
|
||||
export function devicePermissionsChanged(permissions) {
|
||||
return {
|
||||
type: DEVICE_PERMISSIONS_CHANGED,
|
||||
permissions
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import { processExternalDeviceRequest } from '../../device-selection';
|
||||
import { showNotification, showWarningNotification } from '../../notifications';
|
||||
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||
import JitsiMeetJS, { JitsiMediaDevicesEvents, JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { updateSettings } from '../settings';
|
||||
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
import {
|
||||
devicePermissionsChanged,
|
||||
removePendingDeviceRequests,
|
||||
setAudioInputDevice,
|
||||
setVideoInputDevice
|
||||
@@ -35,17 +37,25 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
|
||||
[JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
|
||||
[JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError'
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError',
|
||||
[JitsiTrackErrors.TIMEOUT]: 'dialog.micTimeoutError'
|
||||
},
|
||||
camera: {
|
||||
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
|
||||
[JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
|
||||
[JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError',
|
||||
[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError'
|
||||
[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError',
|
||||
[JitsiTrackErrors.TIMEOUT]: 'dialog.cameraTimeoutError'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A listener for device permissions changed reported from lib-jitsi-meet.
|
||||
*/
|
||||
let permissionsListener;
|
||||
|
||||
/**
|
||||
* Logs the current device list.
|
||||
*
|
||||
@@ -73,6 +83,36 @@ function logDeviceList(deviceList) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT: {
|
||||
const _permissionsListener = permissions => {
|
||||
store.dispatch(devicePermissionsChanged(permissions));
|
||||
};
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
permissionsListener = _permissionsListener;
|
||||
mediaDevices.addEventListener(JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
|
||||
Promise.all([
|
||||
mediaDevices.isDevicePermissionGranted('audio'),
|
||||
mediaDevices.isDevicePermissionGranted('video')
|
||||
])
|
||||
.then(results => {
|
||||
_permissionsListener({
|
||||
audio: results[0],
|
||||
video: results[1]
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
break;
|
||||
}
|
||||
case APP_WILL_UNMOUNT:
|
||||
if (typeof permissionsListener === 'function') {
|
||||
JitsiMeetJS.mediaDevices.removeEventListener(
|
||||
JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
|
||||
permissionsListener = undefined;
|
||||
}
|
||||
break;
|
||||
case NOTIFY_CAMERA_ERROR: {
|
||||
if (typeof APP !== 'object' || !action.error) {
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReducerRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
ADD_PENDING_DEVICE_REQUEST,
|
||||
DEVICE_PERMISSIONS_CHANGED,
|
||||
REMOVE_PENDING_DEVICE_REQUESTS,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
@@ -16,7 +17,11 @@ const DEFAULT_STATE = {
|
||||
audioOutput: [],
|
||||
videoInput: []
|
||||
},
|
||||
pendingRequests: []
|
||||
pendingRequests: [],
|
||||
permissions: {
|
||||
audio: false,
|
||||
video: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -68,6 +73,12 @@ ReducerRegistry.register(
|
||||
|
||||
return state;
|
||||
}
|
||||
case DEVICE_PERMISSIONS_CHANGED: {
|
||||
return {
|
||||
...state,
|
||||
permissions: action.permissions
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
90
react/features/base/dialog/components/web/ModalHeader.js
Normal file
90
react/features/base/dialog/components/web/ModalHeader.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// @flow
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import ErrorIcon from '@atlaskit/icon/glyph/error';
|
||||
import WarningIcon from '@atlaskit/icon/glyph/warning';
|
||||
import {
|
||||
Header,
|
||||
Title,
|
||||
titleIconWrapperStyles,
|
||||
TitleText
|
||||
} from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, IconClose } from '../../../icons';
|
||||
|
||||
const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
|
||||
if (!appearance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const IconSymbol = appearance === 'danger' ? ErrorIcon : WarningIcon;
|
||||
|
||||
return (
|
||||
<span css = { titleIconWrapperStyles(appearance) }>
|
||||
<IconSymbol label = { `${appearance} icon` } />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
appearance?: 'danger' | 'warning',
|
||||
heading: string,
|
||||
onClose: Function,
|
||||
showKeyline: boolean,
|
||||
isHeadingMultiline: boolean,
|
||||
testId: string,
|
||||
t: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* A default header for modal-dialog components
|
||||
*
|
||||
* @export
|
||||
* @class ModalHeader
|
||||
* @extends {React.Component<Props>}
|
||||
*/
|
||||
export default class ModalHeader extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
isHeadingMultiline: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
appearance,
|
||||
heading,
|
||||
onClose,
|
||||
showKeyline,
|
||||
isHeadingMultiline,
|
||||
testId
|
||||
} = this.props;
|
||||
|
||||
if (!heading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Header showKeyline = { showKeyline }>
|
||||
<Title>
|
||||
<TitleIcon appearance = { appearance } />
|
||||
<TitleText
|
||||
data-testid = { testId && `${testId}-heading` }
|
||||
id = { id }
|
||||
isHeadingMultiline = { isHeadingMultiline }>
|
||||
{heading}
|
||||
</TitleText>
|
||||
</Title>
|
||||
<Icon
|
||||
onClick = { onClose }
|
||||
src = { IconClose } />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import Button, { ButtonGroup } from '@atlaskit/button';
|
||||
import ButtonGroup from '@atlaskit/button/button-group';
|
||||
import Button from '@atlaskit/button/standard-button';
|
||||
import Modal, { ModalFooter } from '@atlaskit/modal-dialog';
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
@@ -8,6 +9,8 @@ import React, { Component } from 'react';
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import type { DialogProps } from '../../constants';
|
||||
|
||||
import ModalHeader from './ModalHeader';
|
||||
|
||||
/**
|
||||
* The ID to be used for the cancel button if enabled.
|
||||
* @type {string}
|
||||
@@ -127,10 +130,13 @@ class StatelessDialog extends Component<Props> {
|
||||
<Modal
|
||||
autoFocus = { true }
|
||||
components = {{
|
||||
Header: customHeader
|
||||
Header: customHeader ? customHeader : props => (
|
||||
<ModalHeader
|
||||
{ ...props }
|
||||
heading = { titleString || t(titleKey) } />
|
||||
)
|
||||
}}
|
||||
footer = { this._renderFooter }
|
||||
heading = { customHeader ? undefined : titleString || t(titleKey) }
|
||||
i18n = { this.props.i18n }
|
||||
onClose = { this._onDialogDismissed }
|
||||
onDialogDismissed = { this._onDialogDismissed }
|
||||
|
||||
25
react/features/base/dialog/components/web/ThemedDialog.js
Normal file
25
react/features/base/dialog/components/web/ThemedDialog.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
Dialog,
|
||||
FillScreen,
|
||||
dialogWidth,
|
||||
dialogHeight,
|
||||
PositionerAbsolute,
|
||||
PositionerRelative
|
||||
} from '@atlaskit/modal-dialog/dist/es2019/styled/Modal.js';
|
||||
import { N0, DN50 } from '@atlaskit/theme/colors';
|
||||
import { themed } from '@atlaskit/theme/components';
|
||||
import React from 'react';
|
||||
|
||||
const ThemedDialog = props => {
|
||||
const style = { backgroundColor: props.isChromeless ? 'transparent' : themed({ light: N0,
|
||||
dark: DN50 })({ theme: { mode: 'dark' } }) };
|
||||
|
||||
return (<Dialog
|
||||
{ ...props }
|
||||
aria-modal = { true }
|
||||
style = { style }
|
||||
theme = {{ mode: 'dark' }} />);
|
||||
};
|
||||
|
||||
|
||||
export { ThemedDialog as Dialog, FillScreen, dialogWidth, dialogHeight, PositionerAbsolute, PositionerRelative };
|
||||
@@ -6,6 +6,19 @@
|
||||
*/
|
||||
export const ADD_PEOPLE_ENABLED = 'add-people.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the SDK should not require the audio focus.
|
||||
* Used by apps that do not use Jitsi audio.
|
||||
* Default: disabled (false)
|
||||
*/
|
||||
export const ANDROID_AUDIO_FOCUS_DISABLED = 'android.audio-focus.disabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the audio mute button should be displayed.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const AUDIO_MUTE_BUTTON_ENABLED = 'audio-mute.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if calendar integration should be enabled.
|
||||
* Default: enabled (true) on Android, auto-detected on iOS.
|
||||
@@ -43,6 +56,12 @@ export const CHAT_ENABLED = 'chat.enabled';
|
||||
*/
|
||||
export const FILMSTRIP_ENABLED = 'filmstrip.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the Help button should be enabled.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const HELP_BUTTON_ENABLED = 'help.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if invite functionality should be enabled.
|
||||
* Default: enabled (true).
|
||||
@@ -81,13 +100,18 @@ export const MEETING_NAME_ENABLED = 'meeting-name.enabled';
|
||||
*/
|
||||
export const MEETING_PASSWORD_ENABLED = 'meeting-password.enabled';
|
||||
|
||||
|
||||
/**
|
||||
* Flag indicating if the notifications should be enabled.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const NOTIFICATIONS_ENABLED = 'notifications.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the audio overflow menu button should be displayed.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const OVERFLOW_MENU_ENABLED = 'overflow-menu.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if Picture-in-Picture should be enabled.
|
||||
* Default: auto-detected.
|
||||
@@ -137,6 +161,12 @@ export const TOOLBOX_ALWAYS_VISIBLE = 'toolbox.alwaysVisible';
|
||||
*/
|
||||
export const TOOLBOX_ENABLED = 'toolbox.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the video mute button should be displayed.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const VIDEO_MUTE_BUTTON_ENABLED = 'video-mute.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the video share button should be enabled
|
||||
* Default: enabled (true).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user