mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-08-03 19:17:50 +00:00
Compare commits
38 Commits
android-sd
...
saghul-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4c036174 | ||
|
|
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 |
@@ -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.
|
||||
|
||||
20
Makefile
20
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 = .
|
||||
@@ -20,13 +22,13 @@ compile: compile-load-test
|
||||
$(WEBPACK) -p
|
||||
|
||||
compile-load-test:
|
||||
${NPM} install --prefix load-test && ${NPM} run build --prefix 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)
|
||||
@@ -42,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 \
|
||||
@@ -84,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) ; \
|
||||
@@ -93,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:
|
||||
|
||||
@@ -63,7 +63,10 @@ public class BroadcastAction {
|
||||
HANG_UP("org.jitsi.meet.HANG_UP"),
|
||||
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");
|
||||
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;
|
||||
|
||||
|
||||
@@ -83,7 +83,9 @@ public class BroadcastEvent {
|
||||
PARTICIPANT_LEFT("org.jitsi.meet.PARTICIPANT_LEFT"),
|
||||
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");
|
||||
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";
|
||||
@@ -94,6 +96,8 @@ public class BroadcastEvent {
|
||||
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;
|
||||
|
||||
@@ -134,6 +138,10 @@ public class BroadcastEvent {
|
||||
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;
|
||||
|
||||
@@ -23,4 +23,21 @@ public class BroadcastIntentHelper {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ class ExternalAPIModule
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -504,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()
|
||||
@@ -512,7 +517,10 @@ export default {
|
||||
return [ desktopStream ];
|
||||
}
|
||||
|
||||
return createLocalTracksF({ devices: [ 'audio' ] }, true)
|
||||
return createLocalTracksF({
|
||||
devices: [ 'audio' ],
|
||||
timeout
|
||||
}, true)
|
||||
.then(([ audioStream ]) =>
|
||||
[ desktopStream, audioStream ])
|
||||
.catch(error => {
|
||||
@@ -526,7 +534,10 @@ export default {
|
||||
errors.screenSharingError = error;
|
||||
|
||||
return requestedAudio
|
||||
? createLocalTracksF({ devices: [ 'audio' ] }, true)
|
||||
? createLocalTracksF({
|
||||
devices: [ 'audio' ],
|
||||
timeout
|
||||
}, true)
|
||||
: [];
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -538,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;
|
||||
|
||||
@@ -567,7 +596,10 @@ export default {
|
||||
|
||||
// Try video only...
|
||||
return requestedVideo
|
||||
? createLocalTracksF({ devices: [ 'video' ] }, true)
|
||||
? createLocalTracksF({
|
||||
devices: [ 'video' ],
|
||||
timeout
|
||||
}, true)
|
||||
: [];
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -480,14 +480,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;
|
||||
|
||||
@@ -42,10 +42,9 @@
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: $filmstripVideosZ;
|
||||
|
||||
@media (min-width: 581px) {
|
||||
&.shift-right {
|
||||
|
||||
@@ -22,11 +22,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#remoteConnectionMessage,
|
||||
.watermark {
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The follow styling uses !important to override inline styles set with
|
||||
* javascript.
|
||||
|
||||
@@ -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 {
|
||||
@@ -98,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;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,3 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#videoResolutionLabel {
|
||||
z-index: $zindex3 + 1;
|
||||
}
|
||||
|
||||
2
debian/jitsi-meet-web-config.postinst
vendored
2
debian/jitsi-meet-web-config.postinst
vendored
@@ -218,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
|
||||
|
||||
4
debian/jitsi-meet-web.install
vendored
4
debian/jitsi-meet-web.install
vendored
@@ -15,5 +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/
|
||||
load-test/*.html /usr/share/jitsi-meet/load-test/
|
||||
load-test/libs /usr/share/jitsi-meet/load-test/
|
||||
resources/load-test/*.html /usr/share/jitsi-meet/load-test/
|
||||
resources/load-test/libs /usr/share/jitsi-meet/load-test/
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
ProxyPreserveHost on
|
||||
ProxyPass /http-bind http://localhost:5280/http-bind/
|
||||
ProxyPassReverse /http-bind http://localhost:5280/http-bind/
|
||||
ProxyPass /xmpp-websocket http://localhost:5280/xmpp-websocket
|
||||
ProxyPassReverse /xmpp-websocket http://localhost:5280/xmpp-websocket
|
||||
ProxyPassMatch ^/colibri-ws/default-id http://localhost:9090
|
||||
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
|
||||
|
||||
@@ -123,6 +123,14 @@
|
||||
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,9 +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
|
||||
|
||||
@@ -23,6 +23,9 @@ 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
|
||||
|
||||
@@ -30,7 +33,7 @@ static NSMapTable<NSString*, void (^)(NSArray* participantsInfo)> *participantIn
|
||||
|
||||
__attribute__((constructor))
|
||||
static void initializeViewsMap() {
|
||||
participantInfoCompletionHandlers = [NSMapTable strongToWeakObjectsMapTable];
|
||||
participantInfoCompletionHandlers = [NSMapTable strongToStrongObjectsMapTable];
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
@@ -41,7 +44,10 @@ RCT_EXPORT_MODULE();
|
||||
@"SET_AUDIO_MUTED" : setAudioMutedAction,
|
||||
@"SEND_ENDPOINT_TEXT_MESSAGE": sendEndpointTextMessageAction,
|
||||
@"TOGGLE_SCREEN_SHARE": toggleScreenShareAction,
|
||||
@"RETRIEVE_PARTICIPANTS_INFO": retrieveParticipantsInfoAction
|
||||
@"RETRIEVE_PARTICIPANTS_INFO": retrieveParticipantsInfoAction,
|
||||
@"OPEN_CHAT": openChatAction,
|
||||
@"CLOSE_CHAT": closeChatAction,
|
||||
@"SEND_CHAT_MESSAGE": sendChatMessageAction
|
||||
};
|
||||
};
|
||||
|
||||
@@ -61,7 +67,11 @@ RCT_EXPORT_MODULE();
|
||||
setAudioMutedAction,
|
||||
sendEndpointTextMessageAction,
|
||||
toggleScreenShareAction,
|
||||
retrieveParticipantsInfoAction];
|
||||
retrieveParticipantsInfoAction,
|
||||
openChatAction,
|
||||
closeChatAction,
|
||||
sendChatMessageAction
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,10 +154,9 @@ 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];
|
||||
}
|
||||
@@ -160,8 +169,28 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
NSString *completionHandlerId = [[NSUUID UUID] UUIDString];
|
||||
NSDictionary *data = @{ @"requestId": completionHandlerId};
|
||||
|
||||
[participantInfoCompletionHandlers setObject:completionHandler forKey: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
|
||||
|
||||
@@ -36,15 +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
|
||||
|
||||
@@ -140,6 +140,21 @@ static void initializeViewsMap() {
|
||||
[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
|
||||
|
||||
/**
|
||||
|
||||
@@ -90,4 +90,18 @@
|
||||
*/
|
||||
- (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
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}}?",
|
||||
@@ -810,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';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
15
modules/API/external/external_api.js
vendored
15
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',
|
||||
@@ -319,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;
|
||||
@@ -753,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.
|
||||
*
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -10343,8 +10343,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#77978f0e621f463ed606fe94dda25ff18f1d5f64",
|
||||
"from": "github:jitsi/lib-jitsi-meet#77978f0e621f463ed606fe94dda25ff18f1d5f64",
|
||||
"version": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
|
||||
"from": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.2",
|
||||
"@jitsi/sdp-interop": "1.0.3",
|
||||
@@ -15491,11 +15491,6 @@
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA=="
|
||||
},
|
||||
"stackblur-canvas": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.3.0.tgz",
|
||||
"integrity": "sha512-3ZHJv+43D8YttgumssIxkfs3hBXW7XaMS5Ux65fOBhKDYMjbG5hF8Ey8a90RiiJ58aQnAhWbGilPzZ9rkIlWgQ=="
|
||||
},
|
||||
"stacktrace-parser": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.8.tgz",
|
||||
@@ -17071,6 +17066,11 @@
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"wasm-check": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wasm-check/-/wasm-check-2.0.1.tgz",
|
||||
"integrity": "sha512-5otny2JrfRNKIc+zi1YSOrNxXe47trEQbpY6g/MtHrFwLumKSJyAIobGXH1tlEBezE95eIsmDokBbUZtIZTvvA=="
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz",
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#77978f0e621f463ed606fe94dda25ff18f1d5f64",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.19",
|
||||
"moment": "2.19.4",
|
||||
@@ -95,10 +95,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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -56,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).
|
||||
|
||||
@@ -13,9 +13,10 @@ const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
* @param {string} type - The media type of track being created. Expected values
|
||||
* are "video" or "audio".
|
||||
* @param {string} deviceId - The id of the target media source.
|
||||
* @param {number} [timeout] - A timeout for the JitsiMeetJS.createLocalTracks function call.
|
||||
* @returns {Promise<JitsiLocalTrack>}
|
||||
*/
|
||||
export function createLocalTrack(type: string, deviceId: string) {
|
||||
export function createLocalTrack(type: string, deviceId: string, timeout: ?number) {
|
||||
return (
|
||||
JitsiMeetJS.createLocalTracks({
|
||||
cameraDeviceId: deviceId,
|
||||
@@ -24,7 +25,8 @@ export function createLocalTrack(type: string, deviceId: string) {
|
||||
// eslint-disable-next-line camelcase
|
||||
firefox_fake_device:
|
||||
window.config && window.config.firefox_fake_device,
|
||||
micDeviceId: deviceId
|
||||
micDeviceId: deviceId,
|
||||
timeout
|
||||
})
|
||||
.then(([ jitsiLocalTrack ]) => jitsiLocalTrack));
|
||||
}
|
||||
|
||||
@@ -15,16 +15,13 @@ export function preloadImage(src: string | Object): Promise<string> {
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(src, { referrer: '' })
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
resolve(src);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
reject(e);
|
||||
});
|
||||
const image = document.createElement('img');
|
||||
|
||||
image.onload = () => resolve(src);
|
||||
image.onerror = reject;
|
||||
|
||||
// $FlowExpectedError
|
||||
image.referrerPolicy = 'no-referrer';
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function isLargeVideoReceived({ getState }: Object): boolean {
|
||||
const largeVideoParticipantId = getState()['features/large-video'].participantId;
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(
|
||||
getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, largeVideoParticipantId);
|
||||
const lastMediaEvent = getState()['features/large-video'].lastMediaEvent;
|
||||
const lastMediaEvent = getState()['features/large-video']?.lastMediaEvent;
|
||||
|
||||
return videoTrack && !videoTrack.muted && (lastMediaEvent === 'playing' || lastMediaEvent === 'canplaythrough');
|
||||
}
|
||||
@@ -54,5 +54,5 @@ export function isRemoteVideoReceived({ getState }: Object, id: String): boolean
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||
const lastMediaEvent = videoTrack?.lastMediaEvent;
|
||||
|
||||
return !videoTrack.muted && (lastMediaEvent === 'playing' || lastMediaEvent === 'canplaythrough');
|
||||
return videoTrack && !videoTrack.muted && (lastMediaEvent === 'playing' || lastMediaEvent === 'canplaythrough');
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
|
||||
* and/or 'video'.
|
||||
* @param {string|null} [options.micDeviceId] - Microphone device id or
|
||||
* {@code undefined} to use app's settings.
|
||||
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
|
||||
* @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
|
||||
* should check for a {@code getUserMedia} permission prompt and fire a
|
||||
* corresponding event.
|
||||
@@ -71,6 +72,7 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
|
||||
*/
|
||||
export function createLocalTracksF(options = {}, firePermissionPromptIsShownEvent, store) {
|
||||
let { cameraDeviceId, micDeviceId } = options;
|
||||
const { desktopSharingSourceDevice, desktopSharingSources, timeout } = options;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
// TODO The app's settings should go in the redux store and then the
|
||||
@@ -105,16 +107,16 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
|
||||
cameraDeviceId,
|
||||
constraints,
|
||||
desktopSharingFrameRate,
|
||||
desktopSharingSourceDevice:
|
||||
options.desktopSharingSourceDevice,
|
||||
desktopSharingSources: options.desktopSharingSources,
|
||||
desktopSharingSourceDevice,
|
||||
desktopSharingSources,
|
||||
|
||||
// Copy array to avoid mutations inside library.
|
||||
devices: options.devices.slice(0),
|
||||
effects,
|
||||
firefox_fake_device, // eslint-disable-line camelcase
|
||||
micDeviceId,
|
||||
resolution
|
||||
resolution,
|
||||
timeout
|
||||
},
|
||||
firePermissionPromptIsShownEvent)
|
||||
.catch(err => {
|
||||
@@ -318,7 +320,7 @@ export function getTrackByMediaTypeAndParticipant(
|
||||
mediaType,
|
||||
participantId) {
|
||||
return tracks.find(
|
||||
t => t.participantId === participantId && t.mediaType === mediaType
|
||||
t => Boolean(t.jitsiTrack) && t.participantId === participantId && t.mediaType === mediaType
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ export async function sendCountRequest({ baseUrl, billingId, jwt, tenant }: {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBillingId() {
|
||||
|
||||
let billingId = jitsiLocalStorage.getItem(BILLING_ID);
|
||||
|
||||
if (!billingId) {
|
||||
@@ -88,3 +87,15 @@ export function getBillingId() {
|
||||
|
||||
return billingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the billing id for vpaas meetings.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getVpaasBillingId(state: Object) {
|
||||
if (isVpaasMeeting(state)) {
|
||||
return getBillingId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,24 @@ export const ADD_MESSAGE = 'ADD_MESSAGE';
|
||||
*/
|
||||
export const CLEAR_MESSAGES = 'CLEAR_MESSAGES';
|
||||
|
||||
/**
|
||||
* The type of the action which signals the cancelation the chat panel.
|
||||
*
|
||||
* {
|
||||
* type: CLOSE_CHAT
|
||||
* }
|
||||
*/
|
||||
export const CLOSE_CHAT = 'CLOSE_CHAT';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to display the chat panel.
|
||||
*
|
||||
* {
|
||||
* type: OPEN_CHAT
|
||||
* }
|
||||
*/
|
||||
export const OPEN_CHAT = 'OPEN_CHAT';
|
||||
|
||||
/**
|
||||
* The type of the action which signals a send a chat message to everyone in the
|
||||
* conference.
|
||||
@@ -46,12 +64,3 @@ export const SEND_MESSAGE = 'SEND_MESSAGE';
|
||||
* }
|
||||
*/
|
||||
export const SET_PRIVATE_MESSAGE_RECIPIENT = 'SET_PRIVATE_MESSAGE_RECIPIENT';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to toggle the display of the chat panel.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_CHAT
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_CHAT = 'TOGGLE_CHAT';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
CLEAR_MESSAGES,
|
||||
CLOSE_CHAT,
|
||||
SEND_MESSAGE,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT
|
||||
} from './actionTypes';
|
||||
@@ -49,6 +50,19 @@ export function clearMessages() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal the closing of the chat dialog.
|
||||
*
|
||||
* @returns {{
|
||||
* type: CLOSE_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function closeChat() {
|
||||
return {
|
||||
type: CLOSE_CHAT
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a chat message to everyone in the conference.
|
||||
*
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
// @flow
|
||||
|
||||
import { TOGGLE_CHAT } from './actionTypes';
|
||||
import { OPEN_CHAT } from './actionTypes';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Toggles display of the chat panel.
|
||||
* Displays the chat panel.
|
||||
*
|
||||
* @returns {Function}
|
||||
* @param {Object} participant - The recipient for the private chat.
|
||||
*
|
||||
* @returns {{
|
||||
* participant: Participant,
|
||||
* type: OPEN_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function toggleChat() {
|
||||
return function(dispatch: (Object) => Object) {
|
||||
dispatch({ type: TOGGLE_CHAT });
|
||||
export function openChat(participant: Object) {
|
||||
return {
|
||||
participant,
|
||||
type: OPEN_CHAT
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,44 @@
|
||||
// @flow
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
|
||||
import { TOGGLE_CHAT } from './actionTypes';
|
||||
import { OPEN_CHAT } from './actionTypes';
|
||||
import { closeChat } from './actions.any';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Toggles display of the chat side panel while also taking window
|
||||
* resize into account.
|
||||
* Displays the chat panel.
|
||||
*
|
||||
* @param {Object} participant - The recipient for the private chat.
|
||||
* @returns {{
|
||||
* participant: Participant,
|
||||
* type: OPEN_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function openChat(participant: Object) {
|
||||
return function(dispatch: (Object) => Object) {
|
||||
dispatch({ participant,
|
||||
type: OPEN_CHAT });
|
||||
VideoLayout.onResize();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles display of the chat panel.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleChat() {
|
||||
return function(dispatch: (Object) => Object) {
|
||||
dispatch({ type: TOGGLE_CHAT });
|
||||
VideoLayout.onResize();
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const isOpen = getState()['features/chat'].isOpen;
|
||||
|
||||
if (isOpen) {
|
||||
dispatch(closeChat());
|
||||
} else {
|
||||
dispatch(openChat());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Dispatch } from 'redux';
|
||||
|
||||
import { isMobileBrowser } from '../../base/environment/utils';
|
||||
import { getLocalParticipant } from '../../base/participants';
|
||||
import { sendMessage, toggleChat } from '../actions';
|
||||
import { sendMessage } from '../actions';
|
||||
import { DESKTOP_SMALL_WIDTH_THRESHOLD, MOBILE_SMALL_WIDTH_THRESHOLD } from '../constants';
|
||||
|
||||
/**
|
||||
@@ -59,41 +59,34 @@ export type Props = {
|
||||
/**
|
||||
* Implements an abstract chat panel.
|
||||
*/
|
||||
export default class AbstractChat<P: Props> extends Component<P> {}
|
||||
export default class AbstractChat<P: Props> extends Component<P> {
|
||||
|
||||
/**
|
||||
* Maps redux actions to the props of the component.
|
||||
*
|
||||
* @param {Function} dispatch - The redux action {@code dispatch} function.
|
||||
* @returns {{
|
||||
* _onSendMessage: Function,
|
||||
* _onToggleChat: Function
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
export function _mapDispatchToProps(dispatch: Dispatch<any>) {
|
||||
return {
|
||||
/**
|
||||
* Toggles the chat window.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onToggleChat() {
|
||||
dispatch(toggleChat());
|
||||
},
|
||||
/**
|
||||
* Initializes a new {@code AbstractChat} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code AbstractChat} instance with.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
*
|
||||
* @private
|
||||
* @param {string} text - The text message to be sent.
|
||||
* @returns {void}
|
||||
* @type {Function}
|
||||
*/
|
||||
_onSendMessage(text: string) {
|
||||
dispatch(sendMessage(text));
|
||||
}
|
||||
};
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSendMessage = this._onSendMessage.bind(this);
|
||||
}
|
||||
|
||||
_onSendMessage: (string) => void;
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
*
|
||||
* @private
|
||||
* @param {string} text - The text message to be sent.
|
||||
* @returns {void}
|
||||
* @type {Function}
|
||||
*/
|
||||
_onSendMessage(text: string) {
|
||||
this.props.dispatch(sendMessage(text));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IconMessage, IconReply } from '../../base/icons';
|
||||
import { getParticipantById } from '../../base/participants';
|
||||
import { connect } from '../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||
import { setPrivateMessageRecipient } from '../actions';
|
||||
import { openChat } from '../actions';
|
||||
|
||||
export type Props = AbstractButtonProps & {
|
||||
|
||||
@@ -24,15 +24,15 @@ export type Props = AbstractButtonProps & {
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The participant object retreived from Redux.
|
||||
*/
|
||||
_participant: Object,
|
||||
|
||||
/**
|
||||
* Function to dispatch the result of the participant selection to send a private message.
|
||||
*/
|
||||
_setPrivateMessageRecipient: Function
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -51,9 +51,9 @@ class PrivateMessageButton extends AbstractButton<Props, any> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { _participant, _setPrivateMessageRecipient } = this.props;
|
||||
const { dispatch, _participant } = this.props;
|
||||
|
||||
_setPrivateMessageRecipient(_participant);
|
||||
dispatch(openChat(_participant));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,20 +69,6 @@ class PrivateMessageButton extends AbstractButton<Props, any> {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the props of this component to Redux actions.
|
||||
*
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
|
||||
return {
|
||||
_setPrivateMessageRecipient: participant => {
|
||||
dispatch(setPrivateMessageRecipient(participant));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux store to the props of this component.
|
||||
*
|
||||
@@ -96,4 +82,4 @@ export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props>
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageButton));
|
||||
export default translate(connect(_mapStateToProps)(PrivateMessageButton));
|
||||
|
||||
@@ -5,9 +5,9 @@ import React from 'react';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiModal } from '../../../base/modal';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { closeChat } from '../../actions.any';
|
||||
import { CHAT_VIEW_MODAL_ID } from '../../constants';
|
||||
import AbstractChat, {
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps,
|
||||
type Props
|
||||
} from '../AbstractChat';
|
||||
@@ -48,11 +48,13 @@ class Chat extends AbstractChat<Props> {
|
||||
|
||||
<MessageContainer messages = { this.props._messages } />
|
||||
<MessageRecipient />
|
||||
<ChatInputBar onSend = { this.props._onSendMessage } />
|
||||
<ChatInputBar onSend = { this._onSendMessage } />
|
||||
</JitsiModal>
|
||||
);
|
||||
}
|
||||
|
||||
_onSendMessage: (string) => void;
|
||||
|
||||
_onClose: () => boolean
|
||||
|
||||
/**
|
||||
@@ -61,10 +63,10 @@ class Chat extends AbstractChat<Props> {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onClose() {
|
||||
this.props._onToggleChat();
|
||||
this.props.dispatch(closeChat());
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));
|
||||
export default translate(connect(_mapStateToProps)(Chat));
|
||||
|
||||
@@ -2,41 +2,22 @@
|
||||
|
||||
import { CHAT_ENABLED, getFeatureFlag } from '../../../base/flags';
|
||||
import { IconChat, IconChatUnread } from '../../../base/icons';
|
||||
import { setActiveModalId } from '../../../base/modal';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import {
|
||||
AbstractButton,
|
||||
type AbstractButtonProps
|
||||
} from '../../../base/toolbox/components';
|
||||
import { openDisplayNamePrompt } from '../../../display-name';
|
||||
import { CHAT_VIEW_MODAL_ID } from '../../constants';
|
||||
import { openChat } from '../../actions.native';
|
||||
import { getUnreadCount } from '../../functions';
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Function to display chat.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_displayChat: Function,
|
||||
|
||||
/**
|
||||
* Function to diaply the name prompt before displaying the chat
|
||||
* window, if the user has no display name set.
|
||||
*/
|
||||
_displayNameInputDialog: Function,
|
||||
|
||||
/**
|
||||
* Whether or not to block chat access with a nickname input form.
|
||||
*/
|
||||
_showNamePrompt: boolean,
|
||||
|
||||
/**
|
||||
* The unread message count.
|
||||
*/
|
||||
_unreadMessageCount: number
|
||||
_unreadMessageCount: number,
|
||||
|
||||
dispatch: Function
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,13 +36,7 @@ class ChatButton extends AbstractButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
if (this.props._showNamePrompt) {
|
||||
this.props._displayNameInputDialog(() => {
|
||||
this.props._displayChat();
|
||||
});
|
||||
} else {
|
||||
this.props._displayChat();
|
||||
}
|
||||
this.props.dispatch(openChat());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,41 +50,6 @@ class ChatButton extends AbstractButton<Props, *> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux actions to the props of the component.
|
||||
*
|
||||
* @param {Function} dispatch - The redux action {@code dispatch} function.
|
||||
* @returns {{
|
||||
* _displayChat,
|
||||
* _displayNameInputDialog
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch: Function) {
|
||||
return {
|
||||
/**
|
||||
* Launches native invite dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayChat() {
|
||||
dispatch(setActiveModalId(CHAT_VIEW_MODAL_ID));
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a display name prompt.
|
||||
*
|
||||
* @param {Function} onPostSubmit - The function to invoke after a
|
||||
* succesfulsetting of the display name.
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayNameInputDialog(onPostSubmit) {
|
||||
dispatch(openDisplayNamePrompt(onPostSubmit));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the redux state to the component's props.
|
||||
*
|
||||
@@ -118,15 +58,13 @@ function _mapDispatchToProps(dispatch: Function) {
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
|
||||
const { visible = enabled } = ownProps;
|
||||
|
||||
return {
|
||||
_showNamePrompt: !localParticipant.name,
|
||||
_unreadMessageCount: getUnreadCount(state),
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(ChatButton);
|
||||
export default connect(_mapStateToProps)(ChatButton);
|
||||
|
||||
@@ -4,8 +4,8 @@ import React from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { toggleChat } from '../../actions.web';
|
||||
import AbstractChat, {
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps,
|
||||
type Props
|
||||
} from '../AbstractChat';
|
||||
@@ -49,9 +49,8 @@ class Chat extends AbstractChat<Props> {
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._renderPanelContent = this._renderPanelContent.bind(this);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onChatInputResize = this._onChatInputResize.bind(this);
|
||||
this._onToggleChat = this._onToggleChat.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +118,7 @@ class Chat extends AbstractChat<Props> {
|
||||
<MessageRecipient />
|
||||
<ChatInput
|
||||
onResize = { this._onChatInputResize }
|
||||
onSend = { this.props._onSendMessage } />
|
||||
onSend = { this._onSendMessage } />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -135,7 +134,7 @@ class Chat extends AbstractChat<Props> {
|
||||
return (
|
||||
<Header
|
||||
className = 'chat-header'
|
||||
onCancel = { this.props._onToggleChat } />
|
||||
onCancel = { this._onToggleChat } />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,6 +196,20 @@ class Chat extends AbstractChat<Props> {
|
||||
this._messageContainerRef.current.scrollToBottom(withAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
_onSendMessage: (string) => void;
|
||||
|
||||
_onToggleChat: () => void;
|
||||
|
||||
/**
|
||||
* Toggles the chat window.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onToggleChat() {
|
||||
this.props.dispatch(toggleChat());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));
|
||||
export default translate(connect(_mapStateToProps)(Chat));
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconClose } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { toggleChat } from '../../../chat';
|
||||
import { closeChat } from '../../actions.any';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -42,6 +42,6 @@ function Header({ onCancel, className, t }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = { onCancel: toggleChat };
|
||||
const mapDispatchToProps = { onCancel: closeChat };
|
||||
|
||||
export default translate(connect(null, mapDispatchToProps)(Header));
|
||||
|
||||
@@ -18,10 +18,12 @@ import {
|
||||
} from '../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
import { openDisplayNamePrompt } from '../display-name';
|
||||
import { showToolbox } from '../toolbox/actions';
|
||||
|
||||
import { ADD_MESSAGE, TOGGLE_CHAT, SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
|
||||
import { addMessage, clearMessages, toggleChat } from './actions';
|
||||
import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT } from './actionTypes';
|
||||
import { addMessage, clearMessages } from './actions';
|
||||
import { closeChat } from './actions.any';
|
||||
import { ChatPrivacyDialog } from './components';
|
||||
import {
|
||||
CHAT_VIEW_MODAL_ID,
|
||||
@@ -52,6 +54,7 @@ const PRIVACY_NOTICE_TIMEOUT = 20 * 1000;
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
const localParticipant = getLocalParticipant(getState());
|
||||
let isOpen, unreadCount;
|
||||
|
||||
switch (action.type) {
|
||||
@@ -63,14 +66,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
APP.API.notifyChatUpdated(unreadCount, isOpen);
|
||||
}
|
||||
break;
|
||||
case TOGGLE_CHAT:
|
||||
unreadCount = 0;
|
||||
isOpen = !getState()['features/chat'].isOpen;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyChatUpdated(unreadCount, isOpen);
|
||||
}
|
||||
break;
|
||||
case APP_WILL_MOUNT:
|
||||
dispatch(
|
||||
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
|
||||
@@ -84,6 +80,34 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
_addChatMsgListener(action.conference, store);
|
||||
break;
|
||||
|
||||
case OPEN_CHAT:
|
||||
if (localParticipant.name) {
|
||||
dispatch(setActiveModalId(CHAT_VIEW_MODAL_ID));
|
||||
_maybeFocusField();
|
||||
} else {
|
||||
dispatch(openDisplayNamePrompt(() => {
|
||||
dispatch(setActiveModalId(CHAT_VIEW_MODAL_ID));
|
||||
_maybeFocusField();
|
||||
}));
|
||||
}
|
||||
|
||||
unreadCount = 0;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyChatUpdated(unreadCount, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case CLOSE_CHAT:
|
||||
unreadCount = 0;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyChatUpdated(unreadCount, true);
|
||||
}
|
||||
|
||||
dispatch(setActiveModalId());
|
||||
break;
|
||||
|
||||
case SEND_MESSAGE: {
|
||||
const state = store.getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
@@ -117,12 +141,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_PRIVATE_MESSAGE_RECIPIENT: {
|
||||
Boolean(action.participant) && dispatch(setActiveModalId(CHAT_VIEW_MODAL_ID));
|
||||
_maybeFocusField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
@@ -141,7 +159,7 @@ StateListenerRegistry.register(
|
||||
|
||||
if (getState()['features/chat'].isOpen) {
|
||||
// Closes the chat if it's left open.
|
||||
dispatch(toggleChat());
|
||||
dispatch(closeChat());
|
||||
}
|
||||
|
||||
// Clear chat messages.
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { SET_ACTIVE_MODAL_ID } from '../base/modal';
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
CLEAR_MESSAGES,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
TOGGLE_CHAT
|
||||
CLOSE_CHAT,
|
||||
OPEN_CHAT,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT
|
||||
} from './actionTypes';
|
||||
import { CHAT_VIEW_MODAL_ID } from './constants';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
isOpen: false,
|
||||
@@ -58,38 +57,28 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
messages: []
|
||||
};
|
||||
|
||||
case SET_ACTIVE_MODAL_ID:
|
||||
if (action.activeModalId === CHAT_VIEW_MODAL_ID) {
|
||||
return updateChatState(state);
|
||||
}
|
||||
|
||||
break;
|
||||
case SET_PRIVATE_MESSAGE_RECIPIENT:
|
||||
return {
|
||||
...state,
|
||||
isOpen: Boolean(action.participant) || state.isOpen,
|
||||
privateMessageRecipient: action.participant
|
||||
};
|
||||
|
||||
case TOGGLE_CHAT:
|
||||
return updateChatState(state);
|
||||
case OPEN_CHAT:
|
||||
return {
|
||||
...state,
|
||||
isOpen: true,
|
||||
privateMessageRecipient: action.participant
|
||||
};
|
||||
|
||||
case CLOSE_CHAT:
|
||||
return {
|
||||
...state,
|
||||
isOpen: false,
|
||||
lastReadMessage: state.messages[
|
||||
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
|
||||
privateMessageRecipient: action.participant
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the chat status on opening the chat view.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function updateChatState(state) {
|
||||
return {
|
||||
...state,
|
||||
isOpen: !state.isOpen,
|
||||
lastReadMessage: state.messages[
|
||||
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
|
||||
privateMessageRecipient: state.isOpen ? undefined : state.privateMessageRecipient
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,17 +8,11 @@ import { getParticipantCount } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { beginAddPeople } from '../../../invite';
|
||||
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import { shouldDisplayTileView } from '../../../video-layout/functions';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether tile view is enabled.
|
||||
*/
|
||||
_tileViewEnabled: Boolean,
|
||||
|
||||
/**
|
||||
* Whether to show the option to invite more people
|
||||
* instead of the subject.
|
||||
@@ -44,14 +38,13 @@ type Props = {
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function InviteMore({
|
||||
_tileViewEnabled,
|
||||
_visible,
|
||||
onClick,
|
||||
t
|
||||
}: Props) {
|
||||
return (
|
||||
_visible
|
||||
? <div className = { `invite-more-container${_tileViewEnabled ? ' elevated' : ''}` }>
|
||||
? <div className = 'invite-more-container'>
|
||||
<div className = 'invite-more-header'>
|
||||
{t('addPeople.inviteMoreHeader')}
|
||||
</div>
|
||||
@@ -81,7 +74,6 @@ function mapStateToProps(state) {
|
||||
const hide = interfaceConfig.HIDE_INVITE_MORE_HEADER;
|
||||
|
||||
return {
|
||||
_tileViewEnabled: shouldDisplayTileView(state),
|
||||
_visible: isToolboxVisible(state) && isButtonEnabled('invite') && isAlone && !hide
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { translateToHTML } from '../base/i18n';
|
||||
import { getLocalParticipant } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import { getBackendSafePath, getJitsiMeetGlobalNS } from '../base/util';
|
||||
import { getVpaasBillingId } from '../billing-counter/functions';
|
||||
import { showWarningNotification } from '../notifications';
|
||||
import { createRnnoiseProcessorPromise } from '../rnnoise';
|
||||
|
||||
@@ -84,6 +85,7 @@ export function getConferenceOptions(stateful) {
|
||||
options.applicationName = getName();
|
||||
options.getWiFiStatsMethod = getWiFiStatsMethod;
|
||||
options.createVADProcessor = createRnnoiseProcessorPromise;
|
||||
options.billingId = getVpaasBillingId(state);
|
||||
|
||||
// Disable CallStats, if requessted.
|
||||
if (options.disableThirdPartyRequests) {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* The type of Redux action which Sets information about device selection popup.
|
||||
*
|
||||
* {{
|
||||
* type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||
* popupDialogData: Object
|
||||
* }}
|
||||
*/
|
||||
export const SET_DEVICE_SELECTION_POPUP_DATA
|
||||
= 'SET_DEVICE_SELECTION_POPUP_DATA';
|
||||
@@ -1,8 +1,3 @@
|
||||
import { API_ID } from '../../../modules/API/constants';
|
||||
import {
|
||||
PostMessageTransportBackend,
|
||||
Transport
|
||||
} from '../../../modules/transport';
|
||||
import { createDeviceChangedEvent, sendAnalytics } from '../analytics';
|
||||
import {
|
||||
getDeviceLabelById,
|
||||
@@ -10,93 +5,11 @@ import {
|
||||
setAudioOutputDeviceId,
|
||||
setVideoInputDevice
|
||||
} from '../base/devices';
|
||||
import { i18next } from '../base/i18n';
|
||||
import { updateSettings } from '../base/settings';
|
||||
|
||||
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||
import { getDeviceSelectionDialogProps, processExternalDeviceRequest } from './functions';
|
||||
import { getDeviceSelectionDialogProps } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Opens a popup window with the device selection dialog in it.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openDeviceSelectionPopup() {
|
||||
return (dispatch, getState) => {
|
||||
const { popupDialogData } = getState()['features/device-selection'];
|
||||
|
||||
if (popupDialogData) {
|
||||
popupDialogData.popup.focus();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// API_ID will always be defined because the iframe api is enabled
|
||||
const scope = `dialog_${API_ID}`;
|
||||
const url = `${
|
||||
window.location.origin}/static/deviceSelectionPopup.html#scope=${
|
||||
encodeURIComponent(JSON.stringify(scope))}`;
|
||||
const popup
|
||||
= window.open(
|
||||
url,
|
||||
'device-selection-popup',
|
||||
'toolbar=no,scrollbars=no,resizable=no,width=720,height=458');
|
||||
|
||||
popup.addEventListener('DOMContentLoaded', () => {
|
||||
popup.init(i18next);
|
||||
});
|
||||
|
||||
const transport = new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
postisOptions: {
|
||||
scope,
|
||||
window: popup
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
transport.on('request',
|
||||
processExternalDeviceRequest.bind(undefined, dispatch, getState));
|
||||
transport.on('event', event => {
|
||||
if (event.type === 'devices-dialog' && event.name === 'close') {
|
||||
popup.close();
|
||||
transport.dispose();
|
||||
dispatch(_setDeviceSelectionPopupData());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
dispatch(_setDeviceSelectionPopupData({
|
||||
popup,
|
||||
transport
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets information about device selection popup in the store.
|
||||
*
|
||||
* @param {Object} popupDialogData - Information about the popup.
|
||||
* @param {Object} popupDialog.popup - The popup object returned from
|
||||
* window.open.
|
||||
* @param {Object} popupDialogData.transport - The transport instance used for
|
||||
* communication with the popup window.
|
||||
* @returns {{
|
||||
* type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||
* popupDialogData: Object
|
||||
* }}
|
||||
*/
|
||||
function _setDeviceSelectionPopupData(popupDialogData) {
|
||||
return {
|
||||
type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||
popupDialogData
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings related to device selection.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,6 @@ import AbstractDialogTab, {
|
||||
type Props as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet/_';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
|
||||
import logger from '../logger';
|
||||
|
||||
@@ -41,6 +40,16 @@ export type Props = {
|
||||
*/
|
||||
disableDeviceChange: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasAudioPermission: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasVideoPermission: boolean,
|
||||
|
||||
/**
|
||||
* If true, the audio meter will not display. Necessary for browsers or
|
||||
* configurations that do not support local stats to prevent a
|
||||
@@ -87,16 +96,6 @@ export type Props = {
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasAudioPermission: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasVideoPermission: boolean,
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing audio input.
|
||||
*/
|
||||
@@ -141,8 +140,6 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasAudioPermission: false,
|
||||
hasVideoPermission: false,
|
||||
previewAudioTrack: null,
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError: null
|
||||
@@ -170,27 +167,9 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
* video input previews.
|
||||
*
|
||||
* @param {Object} prevProps - Previous props this component received.
|
||||
* @param {Object} prevState - Previous state this component had.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { previewAudioTrack, previewVideoTrack } = prevState;
|
||||
|
||||
if ((!previewAudioTrack && this.state.previewAudioTrack)
|
||||
|| (!previewVideoTrack && this.state.previewVideoTrack)) {
|
||||
Promise.all([
|
||||
JitsiMeetJS.mediaDevices.isDevicePermissionGranted('audio'),
|
||||
JitsiMeetJS.mediaDevices.isDevicePermissionGranted('video')
|
||||
]).then(r => {
|
||||
const [ hasAudioPermission, hasVideoPermission ] = r;
|
||||
|
||||
this.setState({
|
||||
hasAudioPermission,
|
||||
hasVideoPermission
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.selectedAudioInputId
|
||||
!== this.props.selectedAudioInputId) {
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId);
|
||||
@@ -258,7 +237,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
*/
|
||||
_createAudioInputTrack(deviceId) {
|
||||
return this._disposeAudioInputPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId))
|
||||
.then(() => createLocalTrack('audio', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
@@ -286,7 +265,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
*/
|
||||
_createVideoInputTrack(deviceId) {
|
||||
return this._disposeVideoInputPreview()
|
||||
.then(() => createLocalTrack('video', deviceId))
|
||||
.then(() => createLocalTrack('video', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (!jitsiLocalTrack) {
|
||||
return Promise.reject();
|
||||
@@ -360,8 +339,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
* @returns {Array<ReactElement>} DeviceSelector instances.
|
||||
*/
|
||||
_renderSelectors() {
|
||||
const { availableDevices } = this.props;
|
||||
const { hasAudioPermission, hasVideoPermission } = this.state;
|
||||
const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
|
||||
|
||||
const configurations = [
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const settings = state['features/base/settings'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { permissions } = state['features/base/devices'];
|
||||
let disableAudioInputChange = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
|
||||
let selectedAudioInputId = settings.micDeviceId;
|
||||
let selectedAudioOutputId = getAudioOutputDeviceId();
|
||||
@@ -55,6 +56,8 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
|
||||
disableAudioInputChange,
|
||||
disableDeviceChange:
|
||||
!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
||||
hasAudioPermission: permissions.audio,
|
||||
hasVideoPermission: permissions.video,
|
||||
hideAudioInputPreview:
|
||||
!JitsiMeetJS.isCollectingLocalStats(),
|
||||
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
|
||||
@@ -17,16 +17,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
if (action.type === UPDATE_DEVICE_LIST) {
|
||||
const state = store.getState();
|
||||
const { popupDialogData } = state['features/device-selection'];
|
||||
const { availableDevices } = state['features/base/devices'] || {};
|
||||
|
||||
if (popupDialogData) {
|
||||
popupDialogData.transport.sendEvent({
|
||||
name: 'deviceListChanged',
|
||||
devices: availableDevices
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyDeviceListChanged(availableDevices);
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Listen for actions which changes the state of the popup window for the device
|
||||
* selection.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature
|
||||
* features/device-selection.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @param {Object} action.popupDialogData - Object that stores the current
|
||||
* Window object of the popup and the Transport instance. If no popup is shown
|
||||
* the value will be undefined.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register('features/device-selection',
|
||||
(state = {}, action) => {
|
||||
if (action.type === SET_DEVICE_SELECTION_POPUP_DATA) {
|
||||
return {
|
||||
...state,
|
||||
popupDialogData: action.popupDialogData
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -33,7 +33,7 @@ function EmbedMeeting({ t, url }: Props) {
|
||||
* @returns {string} The iframe embed code.
|
||||
*/
|
||||
const getEmbedCode = () =>
|
||||
`<iframe allow="camera; microphone; fullscreen; display-capture" src="${url}"`
|
||||
`<iframe allow="camera; microphone; fullscreen; display-capture; autoplay" src="${url}"`
|
||||
+ ' style="height: 100%; width: 100%; border: 0px;"></iframe>';
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,12 +7,18 @@ import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { CustomSubmitDialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
import { toggleLobbyMode } from '../../actions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the feature.
|
||||
*/
|
||||
_dialogStyles: StyleType,
|
||||
|
||||
/**
|
||||
* The Redux Dispatch function.
|
||||
*/
|
||||
@@ -51,7 +57,7 @@ class EnableLobbyModeDialog extends PureComponent<Props> {
|
||||
onSubmit = { this._onEnableLobbyMode }
|
||||
titleKey = 'lobby.dialogTitle'>
|
||||
<View style = { styles.formWrapper }>
|
||||
<Text>
|
||||
<Text style = { this.props._dialogStyles.text } >
|
||||
{ this.props.t('lobby.enableDialogText') }
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -27,9 +27,12 @@ import {
|
||||
} from '../../base/connection';
|
||||
import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
|
||||
import { SET_AUDIO_MUTED } from '../../base/media/actionTypes';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants } from '../../base/participants';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
|
||||
import { toggleScreensharing } from '../../base/tracks';
|
||||
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
|
||||
import { openChat } from '../../chat/actions';
|
||||
import { sendMessage, setPrivateMessageRecipient, closeChat } from '../../chat/actions.any';
|
||||
import { muteLocal } from '../../remote-video-menu/actions';
|
||||
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
|
||||
|
||||
@@ -37,6 +40,17 @@ import { setParticipantsWithScreenShare } from './actions';
|
||||
import { sendEvent } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Event which will be emitted on the native side when a chat message is received
|
||||
* through the channel.
|
||||
*/
|
||||
const CHAT_MESSAGE_RECEIVED = 'CHAT_MESSAGE_RECEIVED';
|
||||
|
||||
/**
|
||||
* Event which will be emitted on the native side when the chat dialog is displayed/closed.
|
||||
*/
|
||||
const CHAT_TOGGLED = 'CHAT_TOGGLED';
|
||||
|
||||
/**
|
||||
* Event which will be emitted on the native side to indicate the conference
|
||||
* has ended either by user request or because an error was produced.
|
||||
@@ -152,6 +166,17 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case OPEN_CHAT:
|
||||
case CLOSE_CHAT: {
|
||||
sendEvent(
|
||||
store,
|
||||
CHAT_TOGGLED,
|
||||
/* data */ {
|
||||
isOpen: action.type === OPEN_CHAT
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_JOINED:
|
||||
case PARTICIPANT_LEFT: {
|
||||
const { participant } = action;
|
||||
@@ -237,7 +262,9 @@ StateListenerRegistry.register(
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _registerForNativeEvents({ getState, dispatch }) {
|
||||
function _registerForNativeEvents(store) {
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.HANG_UP, () => {
|
||||
dispatch(appNavigate(undefined));
|
||||
});
|
||||
@@ -264,7 +291,6 @@ function _registerForNativeEvents({ getState, dispatch }) {
|
||||
});
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO, ({ requestId }) => {
|
||||
const store = getState();
|
||||
|
||||
const participantsInfo = getParticipants(store).map(participant => {
|
||||
return {
|
||||
@@ -286,6 +312,27 @@ function _registerForNativeEvents({ getState, dispatch }) {
|
||||
requestId
|
||||
});
|
||||
});
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.OPEN_CHAT, ({ to }) => {
|
||||
const participant = getParticipantById(store, to);
|
||||
|
||||
dispatch(openChat(participant));
|
||||
});
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.CLOSE_CHAT, () => {
|
||||
dispatch(closeChat());
|
||||
});
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.SEND_CHAT_MESSAGE, ({ message, to }) => {
|
||||
const participant = getParticipantById(store, to);
|
||||
|
||||
if (participant) {
|
||||
dispatch(setPrivateMessageRecipient(participant));
|
||||
}
|
||||
|
||||
dispatch(sendMessage(message));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,6 +362,36 @@ function _registerForEndpointTextMessages(store) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
(id, message, timestamp) => {
|
||||
sendEvent(
|
||||
store,
|
||||
CHAT_MESSAGE_RECEIVED,
|
||||
/* data */ {
|
||||
senderId: id,
|
||||
message,
|
||||
isPrivate: false,
|
||||
timestamp
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
||||
(id, message, timestamp) => {
|
||||
sendEvent(
|
||||
store,
|
||||
CHAT_MESSAGE_RECEIVED,
|
||||
/* data */ {
|
||||
senderId: id,
|
||||
message,
|
||||
isPrivate: true,
|
||||
timestamp
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,7 +46,9 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
await dispatch(replaceLocalTrack(localVideoTrack.jitsiTrack, null));
|
||||
}
|
||||
|
||||
const jitsiTracks = getState()['features/base/tracks'].map(t => t.jitsiTrack);
|
||||
const jitsiTracks = getState()['features/base/tracks']
|
||||
.map(t => t.jitsiTrack)
|
||||
.filter(t => Boolean(t)); // Filter out GUM in progress tracks...
|
||||
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
APP.conference.prejoinStart(jitsiTracks);
|
||||
|
||||
@@ -5,8 +5,8 @@ import React, { Component } from 'react';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconMessage } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { openChat } from '../../../chat/';
|
||||
import {
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps as _abstractMapStateToProps,
|
||||
type Props as AbstractProps
|
||||
} from '../../../chat/components/PrivateMessageButton';
|
||||
@@ -72,9 +72,9 @@ class PrivateMessageMenuButton extends Component<Props> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { _participant, _setPrivateMessageRecipient } = this.props;
|
||||
const { dispatch, _participant } = this.props;
|
||||
|
||||
_setPrivateMessageRecipient(_participant);
|
||||
dispatch(openChat(_participant));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,4 +93,4 @@ function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageMenuButton));
|
||||
export default translate(connect(_mapStateToProps)(PrivateMessageMenuButton));
|
||||
|
||||
@@ -216,7 +216,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||
participantID = { participantID } />
|
||||
);
|
||||
|
||||
if (onVolumeChange && initialVolumeValue && !isNaN(initialVolumeValue)) {
|
||||
if (onVolumeChange && typeof initialVolumeValue === 'number' && !isNaN(initialVolumeValue)) {
|
||||
buttons.push(
|
||||
<VolumeSlider
|
||||
initialValue = { initialVolumeValue }
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
/* global JitsiMeetJS */
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
import {
|
||||
getAvailableDevices,
|
||||
getCurrentDevices,
|
||||
isDeviceChangeAvailable,
|
||||
isDeviceListAvailable,
|
||||
isMultipleAudioInputSupported,
|
||||
setAudioInputDevice,
|
||||
setAudioOutputDevice,
|
||||
setVideoInputDevice
|
||||
} from '../../../../../modules/API/external/functions';
|
||||
import {
|
||||
PostMessageTransportBackend,
|
||||
Transport
|
||||
} from '../../../../../modules/transport';
|
||||
import DialogWithTabs from '../../../base/dialog/components/web/DialogWithTabs';
|
||||
import { parseURLParams } from '../../../base/util/parseURLParams';
|
||||
import DeviceSelection from '../../../device-selection/components/DeviceSelection';
|
||||
|
||||
/**
|
||||
* Implements a class that renders the React components for the device selection
|
||||
* popup page and handles the communication between the components and Jitsi
|
||||
* Meet.
|
||||
*/
|
||||
export default class DeviceSelectionPopup {
|
||||
/**
|
||||
* Initializes a new DeviceSelectionPopup instance.
|
||||
*
|
||||
* @param {Object} i18next - The i18next instance used for translation.
|
||||
*/
|
||||
constructor(i18next) {
|
||||
this.close = this.close.bind(this);
|
||||
this._i18next = i18next;
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
|
||||
const { scope } = parseURLParams(window.location);
|
||||
|
||||
this._transport = new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
postisOptions: {
|
||||
scope,
|
||||
window: window.opener
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
this._transport.on('event', event => {
|
||||
if (event.name === 'deviceListChanged') {
|
||||
this._updateAvailableDevices();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this._dialogProps = {
|
||||
availableDevices: {},
|
||||
selectedAudioInputId: '',
|
||||
selectedAudioOutputId: '',
|
||||
selectedVideoInputId: '',
|
||||
disableAudioInputChange: true,
|
||||
disableBlanketClickDismiss: true,
|
||||
disableDeviceChange: true,
|
||||
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
|
||||
hideAudioOutputSelect: true
|
||||
|
||||
};
|
||||
this._initState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends event to Jitsi Meet to close the popup dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
close() {
|
||||
this._transport.sendEvent({
|
||||
type: 'devices-dialog',
|
||||
name: 'close'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the properties of the react component and re-renders it.
|
||||
*
|
||||
* @param {Object} newProps - The new properties that will be assigned to
|
||||
* the current ones.
|
||||
* @returns {void}
|
||||
*/
|
||||
_changeDialogProps(newProps) {
|
||||
this._dialogProps = {
|
||||
...this._dialogProps,
|
||||
...newProps
|
||||
};
|
||||
this._render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with result an list of available devices.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_getAvailableDevices() {
|
||||
return getAvailableDevices(this._transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with current selected devices.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_getCurrentDevices() {
|
||||
return getCurrentDevices(this._transport).then(currentDevices => {
|
||||
const {
|
||||
audioInput = {},
|
||||
audioOutput = {},
|
||||
videoInput = {}
|
||||
} = currentDevices;
|
||||
|
||||
return {
|
||||
audioInput: audioInput.deviceId,
|
||||
audioOutput: audioOutput.deviceId,
|
||||
videoInput: videoInput.deviceId
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_initState() {
|
||||
return Promise.all([
|
||||
this._getAvailableDevices(),
|
||||
this._isDeviceListAvailable(),
|
||||
this._isDeviceChangeAvailable(),
|
||||
this._isDeviceChangeAvailable('output'),
|
||||
this._getCurrentDevices(),
|
||||
this._isMultipleAudioInputSupported()
|
||||
]).then(([
|
||||
availableDevices,
|
||||
listAvailable,
|
||||
changeAvailable,
|
||||
changeOutputAvailable,
|
||||
currentDevices,
|
||||
multiAudioInputSupported
|
||||
]) => {
|
||||
this._changeDialogProps({
|
||||
availableDevices,
|
||||
selectedAudioInputId: currentDevices.audioInput,
|
||||
selectedAudioOutputId: currentDevices.audioOutput,
|
||||
selectedVideoInputId: currentDevices.videoInput,
|
||||
disableAudioInputChange: !multiAudioInputSupported,
|
||||
disableDeviceChange: !listAvailable || !changeAvailable,
|
||||
hideAudioOutputSelect: !changeOutputAvailable
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with true if the device change is available
|
||||
* and with false if not.
|
||||
*
|
||||
* @param {string} [deviceType] - Values - 'output', 'input' or undefined.
|
||||
* Default - 'input'.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_isDeviceChangeAvailable(deviceType) {
|
||||
return isDeviceChangeAvailable(this._transport, deviceType).catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with true if the device list is available
|
||||
* and with false if not.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_isDeviceListAvailable() {
|
||||
return isDeviceListAvailable(this._transport).catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with true if multiple audio input is supported
|
||||
* and with false if not.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_isMultipleAudioInputSupported() {
|
||||
return isMultipleAudioInputSupported(this._transport).catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to save changes to selected devices and close the
|
||||
* dialog.
|
||||
*
|
||||
* @param {Object} newSettings - The chosen device IDs.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit(newSettings) {
|
||||
const promises = [];
|
||||
|
||||
if (newSettings.selectedVideoInputId
|
||||
!== this._dialogProps.selectedVideoInputId) {
|
||||
promises.push(
|
||||
this._setVideoInputDevice(newSettings.selectedVideoInputId));
|
||||
}
|
||||
|
||||
if (newSettings.selectedAudioInputId
|
||||
!== this._dialogProps.selectedAudioInputId) {
|
||||
promises.push(
|
||||
this._setAudioInputDevice(newSettings.selectedAudioInputId));
|
||||
}
|
||||
|
||||
if (newSettings.selectedAudioOutputId
|
||||
!== this._dialogProps.selectedAudioOutputId) {
|
||||
promises.push(
|
||||
this._setAudioOutputDevice(newSettings.selectedAudioOutputId));
|
||||
}
|
||||
|
||||
Promise.all(promises).then(this.close, this.close);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the React components for the popup page.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_render() {
|
||||
const onSubmit = this.close;
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n = { this._i18next }>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<DialogWithTabs
|
||||
closeDialog = { this.close }
|
||||
cssClassName = 'settings-dialog'
|
||||
onSubmit = { onSubmit }
|
||||
tabs = { [ {
|
||||
component: DeviceSelection,
|
||||
label: 'settings.devices',
|
||||
props: this._dialogProps,
|
||||
submit: this._onSubmit
|
||||
} ] }
|
||||
titleKey = 'settings.title' />
|
||||
</AtlasKitThemeProvider>
|
||||
</I18nextProvider>,
|
||||
document.getElementById('react'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio input device to the one with the id that is passed.
|
||||
*
|
||||
* @param {string} id - The id of the new device.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_setAudioInputDevice(id) {
|
||||
return setAudioInputDevice(this._transport, undefined, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio output device to the one with the id that is passed.
|
||||
*
|
||||
* @param {string} id - The id of the new device.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_setAudioOutputDevice(id) {
|
||||
return setAudioOutputDevice(this._transport, undefined, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the video input device to the one with the id that is passed.
|
||||
*
|
||||
* @param {string} id - The id of the new device.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_setVideoInputDevice(id) {
|
||||
return setVideoInputDevice(this._transport, undefined, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the available devices.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateAvailableDevices() {
|
||||
this._getAvailableDevices().then(devices =>
|
||||
this._changeDialogProps({ availableDevices: devices })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -182,9 +182,7 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||
|
||||
this._disposeTracks(this.state.audioTracks);
|
||||
|
||||
const audioTracks = await createLocalAudioTracks(
|
||||
this.props.microphoneDevices
|
||||
);
|
||||
const audioTracks = await createLocalAudioTracks(this.props.microphoneDevices, 5000);
|
||||
|
||||
if (this._componentWasUnmounted) {
|
||||
this._disposeTracks(audioTracks);
|
||||
|
||||
@@ -85,9 +85,7 @@ class VideoSettingsContent extends Component<Props, State> {
|
||||
async _setTracks() {
|
||||
this._disposeTracks(this.state.trackData);
|
||||
|
||||
const trackData = await createLocalVideoTracks(
|
||||
this.props.videoDeviceIds,
|
||||
);
|
||||
const trackData = await createLocalVideoTracks(this.props.videoDeviceIds, 5000);
|
||||
|
||||
// In case the component gets unmounted before the tracks are created
|
||||
// avoid a leak by not setting the state
|
||||
|
||||
@@ -156,11 +156,12 @@ export function getProfileTabProps(stateful: Object | Function) {
|
||||
* all the video jitsiTracks and appropriate errors for the given device ids.
|
||||
*
|
||||
* @param {string[]} ids - The list of the camera ids for wich to create tracks.
|
||||
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
|
||||
*
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
export function createLocalVideoTracks(ids: string[]) {
|
||||
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId)
|
||||
export function createLocalVideoTracks(ids: string[], timeout: ?number) {
|
||||
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout)
|
||||
.then(jitsiTrack => {
|
||||
return {
|
||||
jitsiTrack,
|
||||
@@ -182,6 +183,7 @@ export function createLocalVideoTracks(ids: string[]) {
|
||||
* the audio track and the corresponding audio device information.
|
||||
*
|
||||
* @param {Object[]} devices - A list of microphone devices.
|
||||
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
|
||||
* @returns {Promise<{
|
||||
* deviceId: string,
|
||||
* hasError: boolean,
|
||||
@@ -189,14 +191,14 @@ export function createLocalVideoTracks(ids: string[]) {
|
||||
* label: string
|
||||
* }[]>}
|
||||
*/
|
||||
export function createLocalAudioTracks(devices: Object[]) {
|
||||
export function createLocalAudioTracks(devices: Object[], timeout: ?number) {
|
||||
return Promise.all(
|
||||
devices.map(async ({ deviceId, label }) => {
|
||||
let jitsiTrack = null;
|
||||
let hasError = false;
|
||||
|
||||
try {
|
||||
jitsiTrack = await createLocalTrack('audio', deviceId);
|
||||
jitsiTrack = await createLocalTrack('audio', deviceId, timeout);
|
||||
} catch (err) {
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/* global JitsiMeetJS */
|
||||
|
||||
import DeviceSelectionPopup from './components/web/DeviceSelectionPopup';
|
||||
|
||||
let deviceSelectionPopup;
|
||||
|
||||
window.init = i18next => {
|
||||
JitsiMeetJS.init();
|
||||
deviceSelectionPopup = new DeviceSelectionPopup(i18next);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', () => deviceSelectionPopup.close());
|
||||
@@ -1,7 +1,4 @@
|
||||
// @flow
|
||||
|
||||
import * as StackBlur from 'stackblur-canvas';
|
||||
|
||||
import {
|
||||
CLEAR_TIMEOUT,
|
||||
TIMEOUT_TICK,
|
||||
@@ -9,21 +6,27 @@ import {
|
||||
timerWorkerScript
|
||||
} from './TimerWorker';
|
||||
|
||||
const segmentationWidth = 256;
|
||||
const segmentationHeight = 144;
|
||||
const segmentationPixelCount = segmentationWidth * segmentationHeight;
|
||||
const blurValue = '25px';
|
||||
|
||||
/**
|
||||
* Represents a modified MediaStream that adds blur to video background.
|
||||
* <tt>JitsiStreamBlurEffect</tt> does the processing of the original
|
||||
* video stream.
|
||||
*/
|
||||
export default class JitsiStreamBlurEffect {
|
||||
_bpModel: Object;
|
||||
_model: Object;
|
||||
_inputVideoElement: HTMLVideoElement;
|
||||
_inputVideoCanvasElement: HTMLCanvasElement;
|
||||
_onMaskFrameTimer: Function;
|
||||
_maskFrameTimerWorker: Worker;
|
||||
_maskInProgress: boolean;
|
||||
_outputCanvasElement: HTMLCanvasElement;
|
||||
_outputCanvasCtx: Object;
|
||||
_segmentationMaskCtx: Object;
|
||||
_segmentationMask: Object;
|
||||
_segmentationMaskCanvas: Object;
|
||||
_renderMask: Function;
|
||||
_segmentationData: Object;
|
||||
isEnabled: Function;
|
||||
startEffect: Function;
|
||||
stopEffect: Function;
|
||||
@@ -35,7 +38,7 @@ export default class JitsiStreamBlurEffect {
|
||||
* @param {BodyPix} bpModel - BodyPix model.
|
||||
*/
|
||||
constructor(bpModel: Object) {
|
||||
this._bpModel = bpModel;
|
||||
this._model = bpModel;
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onMaskFrameTimer = this._onMaskFrameTimer.bind(this);
|
||||
@@ -44,7 +47,6 @@ export default class JitsiStreamBlurEffect {
|
||||
this._outputCanvasElement = document.createElement('canvas');
|
||||
this._outputCanvasElement.getContext('2d');
|
||||
this._inputVideoElement = document.createElement('video');
|
||||
this._inputVideoCanvasElement = document.createElement('canvas');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,62 +62,109 @@ export default class JitsiStreamBlurEffect {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the run post processing.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
runPostProcessing() {
|
||||
this._outputCanvasCtx.globalCompositeOperation = 'copy';
|
||||
|
||||
// Draw segmentation mask.
|
||||
this._outputCanvasCtx.filter = `blur(${blurValue})`;
|
||||
this._outputCanvasCtx.drawImage(
|
||||
this._segmentationMaskCanvas,
|
||||
0,
|
||||
0,
|
||||
segmentationWidth,
|
||||
segmentationHeight,
|
||||
0,
|
||||
0,
|
||||
this._inputVideoElement.width,
|
||||
this._inputVideoElement.height
|
||||
);
|
||||
|
||||
this._outputCanvasCtx.globalCompositeOperation = 'source-in';
|
||||
this._outputCanvasCtx.filter = 'none';
|
||||
this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);
|
||||
|
||||
this._outputCanvasCtx.globalCompositeOperation = 'destination-over';
|
||||
this._outputCanvasCtx.filter = `blur(${blurValue})`; // FIXME Does not work on Safari.
|
||||
this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the run Tensorflow Interference.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
runInference() {
|
||||
this._model._runInference();
|
||||
const outputMemoryOffset = this._model._getOutputMemoryOffset() / 4;
|
||||
|
||||
for (let i = 0; i < segmentationPixelCount; i++) {
|
||||
const background = this._model.HEAPF32[outputMemoryOffset + (i * 2)];
|
||||
const person = this._model.HEAPF32[outputMemoryOffset + (i * 2) + 1];
|
||||
const shift = Math.max(background, person);
|
||||
const backgroundExp = Math.exp(background - shift);
|
||||
const personExp = Math.exp(person - shift);
|
||||
|
||||
// Sets only the alpha component of each pixel.
|
||||
this._segmentationMask.data[(i * 4) + 3] = (255 * personExp) / (backgroundExp + personExp);
|
||||
}
|
||||
this._segmentationMaskCtx.putImageData(this._segmentationMask, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop function to render the background mask.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
async _renderMask() {
|
||||
if (!this._maskInProgress) {
|
||||
this._maskInProgress = true;
|
||||
this._bpModel.segmentPerson(this._inputVideoElement, {
|
||||
internalResolution: 'low', // resized to 0.5 times of the original resolution before inference
|
||||
maxDetections: 1, // max. number of person poses to detect per image
|
||||
segmentationThreshold: 0.7, // represents probability that a pixel belongs to a person
|
||||
flipHorizontal: false,
|
||||
scoreThreshold: 0.2
|
||||
}).then(data => {
|
||||
this._segmentationData = data;
|
||||
this._maskInProgress = false;
|
||||
});
|
||||
}
|
||||
const inputCanvasCtx = this._inputVideoCanvasElement.getContext('2d');
|
||||
_renderMask() {
|
||||
this.resizeSource();
|
||||
this.runInference();
|
||||
this.runPostProcessing();
|
||||
|
||||
inputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);
|
||||
|
||||
const currentFrame = inputCanvasCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
this._inputVideoCanvasElement.width,
|
||||
this._inputVideoCanvasElement.height
|
||||
);
|
||||
|
||||
if (this._segmentationData) {
|
||||
const blurData = new ImageData(currentFrame.data.slice(), currentFrame.width, currentFrame.height);
|
||||
|
||||
StackBlur.imageDataRGB(blurData, 0, 0, currentFrame.width, currentFrame.height, 12);
|
||||
|
||||
for (let x = 0; x < this._outputCanvasElement.width; x++) {
|
||||
for (let y = 0; y < this._outputCanvasElement.height; y++) {
|
||||
const n = (y * this._outputCanvasElement.width) + x;
|
||||
|
||||
if (this._segmentationData.data[n] === 0) {
|
||||
currentFrame.data[n * 4] = blurData.data[n * 4];
|
||||
currentFrame.data[(n * 4) + 1] = blurData.data[(n * 4) + 1];
|
||||
currentFrame.data[(n * 4) + 2] = blurData.data[(n * 4) + 2];
|
||||
currentFrame.data[(n * 4) + 3] = blurData.data[(n * 4) + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._outputCanvasElement.getContext('2d').putImageData(currentFrame, 0, 0);
|
||||
this._maskFrameTimerWorker.postMessage({
|
||||
id: SET_TIMEOUT,
|
||||
timeMs: 1000 / 30
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the resize source process.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
resizeSource() {
|
||||
this._segmentationMaskCtx.drawImage(
|
||||
this._inputVideoElement,
|
||||
0,
|
||||
0,
|
||||
this._inputVideoElement.width,
|
||||
this._inputVideoElement.height,
|
||||
0,
|
||||
0,
|
||||
segmentationWidth,
|
||||
segmentationHeight
|
||||
);
|
||||
|
||||
const imageData = this._segmentationMaskCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
segmentationWidth,
|
||||
segmentationHeight
|
||||
);
|
||||
const inputMemoryOffset = this._model._getInputMemoryOffset() / 4;
|
||||
|
||||
for (let i = 0; i < segmentationPixelCount; i++) {
|
||||
this._model.HEAPF32[inputMemoryOffset + (i * 3)] = imageData.data[i * 4] / 255;
|
||||
this._model.HEAPF32[inputMemoryOffset + (i * 3) + 1] = imageData.data[(i * 4) + 1] / 255;
|
||||
this._model.HEAPF32[inputMemoryOffset + (i * 3) + 2] = imageData.data[(i * 4) + 2] / 255;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the local track supports this effect.
|
||||
*
|
||||
@@ -136,15 +185,18 @@ export default class JitsiStreamBlurEffect {
|
||||
startEffect(stream: MediaStream) {
|
||||
this._maskFrameTimerWorker = new Worker(timerWorkerScript, { name: 'Blur effect worker' });
|
||||
this._maskFrameTimerWorker.onmessage = this._onMaskFrameTimer;
|
||||
|
||||
const firstVideoTrack = stream.getVideoTracks()[0];
|
||||
const { height, frameRate, width }
|
||||
= firstVideoTrack.getSettings ? firstVideoTrack.getSettings() : firstVideoTrack.getConstraints();
|
||||
|
||||
this._segmentationMask = new ImageData(segmentationWidth, segmentationHeight);
|
||||
this._segmentationMaskCanvas = document.createElement('canvas');
|
||||
this._segmentationMaskCanvas.width = segmentationWidth;
|
||||
this._segmentationMaskCanvas.height = segmentationHeight;
|
||||
this._segmentationMaskCtx = this._segmentationMaskCanvas.getContext('2d');
|
||||
this._outputCanvasElement.width = parseInt(width, 10);
|
||||
this._outputCanvasElement.height = parseInt(height, 10);
|
||||
this._inputVideoCanvasElement.width = parseInt(width, 10);
|
||||
this._inputVideoCanvasElement.height = parseInt(height, 10);
|
||||
this._outputCanvasCtx = this._outputCanvasElement.getContext('2d');
|
||||
this._inputVideoElement.width = parseInt(width, 10);
|
||||
this._inputVideoElement.height = parseInt(height, 10);
|
||||
this._inputVideoElement.autoplay = true;
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import * as bodyPix from '@tensorflow-models/body-pix';
|
||||
import * as wasmCheck from 'wasm-check';
|
||||
|
||||
import JitsiStreamBlurEffect from './JitsiStreamBlurEffect';
|
||||
import createTFLiteModule from './vendor/tflite/tflite';
|
||||
import createTFLiteSIMDModule from './vendor/tflite/tflite-simd';
|
||||
|
||||
const models = {
|
||||
'96': 'libs/segm_lite_v681.tflite',
|
||||
'144': 'libs/segm_full_v679.tflite'
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new instance of JitsiStreamBlurEffect. This loads the bodyPix model that is used to
|
||||
@@ -14,15 +21,24 @@ export async function createBlurEffect() {
|
||||
if (!MediaStreamTrack.prototype.getSettings && !MediaStreamTrack.prototype.getConstraints) {
|
||||
throw new Error('JitsiStreamBlurEffect not supported!');
|
||||
}
|
||||
let tflite;
|
||||
|
||||
// An output stride of 16 and a multiplier of 0.5 are used for improved
|
||||
// performance on a larger range of CPUs.
|
||||
const bpModel = await bodyPix.load({
|
||||
architecture: 'MobileNetV1',
|
||||
outputStride: 16,
|
||||
multiplier: 0.50,
|
||||
quantBytes: 2
|
||||
});
|
||||
if (wasmCheck.feature.simd) {
|
||||
tflite = await createTFLiteSIMDModule();
|
||||
} else {
|
||||
tflite = await createTFLiteModule();
|
||||
}
|
||||
|
||||
return new JitsiStreamBlurEffect(bpModel);
|
||||
const modelBufferOffset = tflite._getModelBufferMemoryOffset();
|
||||
const modelResponse = await fetch(
|
||||
models['144']
|
||||
);
|
||||
|
||||
const model = await modelResponse.arrayBuffer();
|
||||
|
||||
tflite.HEAPU8.set(new Uint8Array(model), modelBufferOffset);
|
||||
|
||||
tflite._loadModel(model.byteLength);
|
||||
|
||||
return new JitsiStreamBlurEffect(tflite);
|
||||
}
|
||||
|
||||
24
react/features/stream-effects/blur/vendor/README.md
vendored
Normal file
24
react/features/stream-effects/blur/vendor/README.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Virtual Background on stream effects
|
||||
|
||||
> Inspired from https://ai.googleblog.com/2020/10/background-features-in-google-meet.html and https://github.com/Volcomix/virtual-background.git
|
||||
|
||||
#### Canvas 2D + CPU
|
||||
|
||||
This rendering pipeline is pretty much the same as for BodyPix. It relies on Canvas compositing properties to blend rendering layers according to the segmentation mask.
|
||||
|
||||
Interactions with TFLite inference tool are executed on CPU to convert from UInt8 to Float32 for the model input and to apply softmax on the model output.
|
||||
|
||||
The framerate is higher and the quality looks better than BodyPix
|
||||
|
||||
#### SIMD and non-SIMD
|
||||
|
||||
How to test on SIMD:
|
||||
1. Go to chrome://flags/
|
||||
2. Search for SIMD flag
|
||||
3. Enable WebAssembly SIMD support(Enables support for the WebAssembly SIMD proposal).
|
||||
4. Reopen Google Chrome
|
||||
|
||||
More details:
|
||||
- [WebAssembly](https://webassembly.org/)
|
||||
- [WebAssembly SIMD](https://github.com/WebAssembly/simd)
|
||||
- [TFLite](https://blog.tensorflow.org/2020/07/accelerating-tensorflow-lite-xnnpack-integration.html)
|
||||
BIN
react/features/stream-effects/blur/vendor/models/segm_full_v679.tflite
vendored
Normal file
BIN
react/features/stream-effects/blur/vendor/models/segm_full_v679.tflite
vendored
Normal file
Binary file not shown.
BIN
react/features/stream-effects/blur/vendor/models/segm_lite_v681.tflite
vendored
Normal file
BIN
react/features/stream-effects/blur/vendor/models/segm_lite_v681.tflite
vendored
Normal file
Binary file not shown.
21
react/features/stream-effects/blur/vendor/tflite/tflite-simd.js
vendored
Normal file
21
react/features/stream-effects/blur/vendor/tflite/tflite-simd.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
react/features/stream-effects/blur/vendor/tflite/tflite-simd.wasm
vendored
Executable file
BIN
react/features/stream-effects/blur/vendor/tflite/tflite-simd.wasm
vendored
Executable file
Binary file not shown.
21
react/features/stream-effects/blur/vendor/tflite/tflite.js
vendored
Normal file
21
react/features/stream-effects/blur/vendor/tflite/tflite.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
react/features/stream-effects/blur/vendor/tflite/tflite.wasm
vendored
Executable file
BIN
react/features/stream-effects/blur/vendor/tflite/tflite.wasm
vendored
Executable file
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../analytics';
|
||||
import { getFeatureFlag, HELP_BUTTON_ENABLED } from '../../base/flags';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { IconHelp } from '../../base/icons';
|
||||
import { connect } from '../../base/redux';
|
||||
@@ -45,7 +46,8 @@ class HelpButton extends AbstractButton<Props, *> {
|
||||
*/
|
||||
function _mapStateToProps(state: Object) {
|
||||
const { userDocumentationURL } = state['features/base/config'].deploymentUrls || {};
|
||||
const visible = typeof userDocumentationURL === 'string';
|
||||
const enabled = getFeatureFlag(state, HELP_BUTTON_ENABLED, true);
|
||||
const visible = typeof userDocumentationURL === 'string' && enabled;
|
||||
|
||||
return {
|
||||
_userDocumentationURL: userDocumentationURL,
|
||||
|
||||
@@ -7,24 +7,22 @@ import { IconArrowDown } from '../../../base/icons';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
|
||||
import { getMediaPermissionPromptVisibility } from '../../../overlay';
|
||||
import { AudioSettingsPopup, toggleAudioSettings } from '../../../settings';
|
||||
import { isAudioSettingsButtonDisabled } from '../../functions';
|
||||
import AudioMuteButton from '../AudioMuteButton';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates whether audio permissions have been granted or denied.
|
||||
*/
|
||||
hasPermissions: boolean,
|
||||
|
||||
/**
|
||||
* Click handler for the small icon. Opens audio options.
|
||||
*/
|
||||
onAudioOptionsClick: Function,
|
||||
|
||||
/**
|
||||
* Whether the permission prompt is visible or not.
|
||||
* Useful for enabling the button on permission grant.
|
||||
*/
|
||||
permissionPromptVisibility: boolean,
|
||||
|
||||
/**
|
||||
* If the button should be disabled.
|
||||
*/
|
||||
@@ -34,83 +32,15 @@ type Props = {
|
||||
* Flag controlling the visibility of the button.
|
||||
* AudioSettings popup is disabled on mobile browsers.
|
||||
*/
|
||||
visible: boolean,
|
||||
visible: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* If there are permissions for audio devices.
|
||||
*/
|
||||
hasPermissions: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Button used for audio & audio settings.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class AudioSettingsButton extends Component<Props, State> {
|
||||
_isMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AudioSettingsButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._isMounted = true;
|
||||
this.state = {
|
||||
hasPermissions: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates device permissions.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _updatePermissions() {
|
||||
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
|
||||
'audio',
|
||||
);
|
||||
|
||||
this._isMounted && this.setState({
|
||||
hasPermissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updatePermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.permissionPromptVisibility !== prevProps.permissionPromptVisibility) {
|
||||
this._updatePermissions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
class AudioSettingsButton extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
@@ -118,8 +48,8 @@ class AudioSettingsButton extends Component<Props, State> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { isDisabled, onAudioOptionsClick, visible } = this.props;
|
||||
const settingsDisabled = !this.state.hasPermissions
|
||||
const { hasPermissions, isDisabled, onAudioOptionsClick, visible } = this.props;
|
||||
const settingsDisabled = !hasPermissions
|
||||
|| isDisabled
|
||||
|| !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
|
||||
|
||||
@@ -143,9 +73,11 @@ class AudioSettingsButton extends Component<Props, State> {
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const { permissions = {} } = state['features/base/devices'];
|
||||
|
||||
return {
|
||||
hasPermissions: permissions.audio,
|
||||
isDisabled: isAudioSettingsButtonDisabled(state),
|
||||
permissionPromptVisibility: getMediaPermissionPromptVisibility(state),
|
||||
visible: !isMobileBrowser()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import * as wasmCheck from 'wasm-check';
|
||||
|
||||
import {
|
||||
ACTION_SHORTCUT_TRIGGERED,
|
||||
@@ -1069,7 +1070,7 @@ class Toolbox extends Component<Props, State> {
|
||||
&& <VideoBlurButton
|
||||
key = 'videobackgroundblur'
|
||||
showLabel = { true }
|
||||
visible = { !_screensharing } />,
|
||||
visible = { !_screensharing && wasmCheck.feature.simd } />,
|
||||
this._shouldShowButton('settings')
|
||||
&& <SettingsButton
|
||||
key = 'settings'
|
||||
|
||||
@@ -4,11 +4,9 @@ import React, { Component } from 'react';
|
||||
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { IconArrowDown } from '../../../base/icons';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
|
||||
import { getLocalJitsiVideoTrack } from '../../../base/tracks';
|
||||
import { getMediaPermissionPromptVisibility } from '../../../overlay';
|
||||
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
|
||||
import { isVideoSettingsButtonDisabled } from '../../functions';
|
||||
import VideoMuteButton from '../VideoMuteButton';
|
||||
@@ -21,10 +19,9 @@ type Props = {
|
||||
onVideoOptionsClick: Function,
|
||||
|
||||
/**
|
||||
* Whether the permission prompt is visible or not.
|
||||
* Useful for enabling the button on initial permission grant.
|
||||
* Indicates whether video permissions have been granted or denied.
|
||||
*/
|
||||
permissionPromptVisibility: boolean,
|
||||
hasPermissions: boolean,
|
||||
|
||||
/**
|
||||
* Whether there is a video track or not.
|
||||
@@ -42,15 +39,7 @@ type Props = {
|
||||
* as mobile devices do not support capture of more than one
|
||||
* camera at a time.
|
||||
*/
|
||||
visible: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Whether the app has video permissions or not.
|
||||
*/
|
||||
hasPermissions: boolean,
|
||||
visible: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -58,23 +47,7 @@ type State = {
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class VideoSettingsButton extends Component<Props, State> {
|
||||
_isMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code VideoSettingsButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._isMounted = true;
|
||||
this.state = {
|
||||
hasPermissions: false
|
||||
};
|
||||
}
|
||||
class VideoSettingsButton extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Returns true if the settings icon is disabled.
|
||||
@@ -82,53 +55,9 @@ class VideoSettingsButton extends Component<Props, State> {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isIconDisabled() {
|
||||
const { hasVideoTrack, isDisabled } = this.props;
|
||||
const { hasPermissions, hasVideoTrack, isDisabled } = this.props;
|
||||
|
||||
return (!this.state.hasPermissions || isDisabled) && !hasVideoTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates device permissions.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _updatePermissions() {
|
||||
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
|
||||
'video',
|
||||
);
|
||||
|
||||
this._isMounted && this.setState({
|
||||
hasPermissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updatePermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.permissionPromptVisibility !== prevProps.permissionPromptVisibility) {
|
||||
this._updatePermissions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
return (!hasPermissions || isDisabled) && !hasVideoTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,10 +88,12 @@ class VideoSettingsButton extends Component<Props, State> {
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const { permissions = {} } = state['features/base/devices'];
|
||||
|
||||
return {
|
||||
hasPermissions: permissions.video,
|
||||
hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)),
|
||||
isDisabled: isVideoSettingsButtonDisabled(state),
|
||||
permissionPromptVisibility: getMediaPermissionPromptVisibility(state),
|
||||
visible: !isMobileBrowser()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
/* global $, config, JitsiMeetJS */
|
||||
import 'jquery';
|
||||
import { parseURLParams } from '../react/features/base/util/parseURLParams';
|
||||
import { parseURIString } from '../react/features/base/util/uri';
|
||||
import { setConfigFromURLParams } from '../../react/features/base/config/functions';
|
||||
import { parseURLParams } from '../../react/features/base/util/parseURLParams';
|
||||
import { parseURIString } from '../../react/features/base/util/uri';
|
||||
|
||||
setConfigFromURLParams(config, {}, {}, window.location);
|
||||
|
||||
const params = parseURLParams(window.location, false, 'hash');
|
||||
const { isHuman = false } = params;
|
||||
const {
|
||||
localAudio = params['config.startWithAudioMuted'] !== true,
|
||||
localVideo = params['config.startWithVideoMuted'] !== true,
|
||||
localAudio = config.startWithAudioMuted !== true,
|
||||
localVideo = config.startWithVideoMuted !== true,
|
||||
remoteVideo = isHuman,
|
||||
remoteAudio = isHuman
|
||||
remoteAudio = isHuman,
|
||||
autoPlayVideo = config.testing.noAutoPlayVideo !== true,
|
||||
|
||||
// Whether to create local audio even if muted
|
||||
autoCreateLocalAudio = config.testing.noAutoLocalAudio !== true
|
||||
} = params;
|
||||
|
||||
const { room: roomName } = parseURIString(window.location.toString());
|
||||
|
||||
let connection = null;
|
||||
|
||||
let isJoined = false;
|
||||
|
||||
let room = null;
|
||||
|
||||
let numParticipants = 1;
|
||||
@@ -25,6 +30,8 @@ let numParticipants = 1;
|
||||
let localTracks = [];
|
||||
const remoteTracks = {};
|
||||
|
||||
let maxFrameHeight = 0;
|
||||
|
||||
window.APP = {
|
||||
conference: {
|
||||
getStats() {
|
||||
@@ -56,16 +63,38 @@ window.APP = {
|
||||
localAudio,
|
||||
localVideo,
|
||||
remoteVideo,
|
||||
remoteAudio
|
||||
remoteAudio,
|
||||
autoPlayVideo
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple emulation of jitsi-meet's screen layout behavior
|
||||
*/
|
||||
function updateMaxFrameHeight() {
|
||||
let newMaxFrameHeight;
|
||||
|
||||
if (numParticipants <= 2) {
|
||||
newMaxFrameHeight = 720;
|
||||
} else if (numParticipants <= 4) {
|
||||
newMaxFrameHeight = 360;
|
||||
} else {
|
||||
newMaxFrameHeight = 180;
|
||||
}
|
||||
|
||||
if (room && maxFrameHeight !== newMaxFrameHeight) {
|
||||
maxFrameHeight = newMaxFrameHeight;
|
||||
room.setReceiverVideoConstraint(maxFrameHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function setNumberOfParticipants() {
|
||||
$('#participants').text(numParticipants);
|
||||
updateMaxFrameHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,16 +105,18 @@ function onLocalTracks(tracks = []) {
|
||||
localTracks = tracks;
|
||||
for (let i = 0; i < localTracks.length; i++) {
|
||||
if (localTracks[i].getType() === 'video') {
|
||||
$('body').append(`<video autoplay='1' id='localVideo${i}' />`);
|
||||
$('body').append(`<video ${autoPlayVideo ? 'autoplay="1" ' : ''}id='localVideo${i}' />`);
|
||||
localTracks[i].attach($(`#localVideo${i}`)[0]);
|
||||
} else {
|
||||
if (!localAudio) {
|
||||
localTracks[i].mute();
|
||||
}
|
||||
|
||||
$('body').append(
|
||||
`<audio autoplay='1' muted='true' id='localAudio${i}' />`);
|
||||
localTracks[i].attach($(`#localAudio${i}`)[0]);
|
||||
}
|
||||
if (isJoined) {
|
||||
room.addTrack(localTracks[i]);
|
||||
}
|
||||
room.addTrack(localTracks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +149,28 @@ function onRemoteTrack(track) {
|
||||
* That function is executed when the conference is joined
|
||||
*/
|
||||
function onConferenceJoined() {
|
||||
isJoined = true;
|
||||
for (let i = 0; i < localTracks.length; i++) {
|
||||
room.addTrack(localTracks[i]);
|
||||
}
|
||||
console.log('Conference joined');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles start muted events, when audio and/or video are muted due to
|
||||
* startAudioMuted or startVideoMuted policy.
|
||||
*/
|
||||
function onStartMuted() {
|
||||
// Give it some time, as it may be currently in the process of muting
|
||||
setTimeout(() => {
|
||||
const localAudioTrack = room.getLocalAudioTrack();
|
||||
|
||||
if (localAudio && localAudioTrack && localAudioTrack.isMuted()) {
|
||||
localAudioTrack.unmute();
|
||||
}
|
||||
|
||||
const localVideoTrack = room.getLocalVideoTrack();
|
||||
|
||||
if (localVideo && localVideoTrack && localVideoTrack.isMuted()) {
|
||||
localVideoTrack.unmute();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,7 +199,8 @@ function onUserLeft(id) {
|
||||
* That function is called when connection is established successfully
|
||||
*/
|
||||
function onConnectionSuccess() {
|
||||
room = connection.initJitsiConference(roomName, config);
|
||||
room = connection.initJitsiConference(roomName.toLowerCase(), config);
|
||||
room.on(JitsiMeetJS.events.conference.STARTED_MUTED, onStartMuted);
|
||||
room.on(JitsiMeetJS.events.conference.TRACK_ADDED, onRemoteTrack);
|
||||
room.on(JitsiMeetJS.events.conference.CONFERENCE_JOINED, onConferenceJoined);
|
||||
room.on(JitsiMeetJS.events.conference.USER_JOINED, id => {
|
||||
@@ -159,7 +209,31 @@ function onConnectionSuccess() {
|
||||
remoteTracks[id] = [];
|
||||
});
|
||||
room.on(JitsiMeetJS.events.conference.USER_LEFT, onUserLeft);
|
||||
room.join();
|
||||
|
||||
const devices = [];
|
||||
|
||||
if (localVideo) {
|
||||
devices.push('video');
|
||||
}
|
||||
|
||||
if (autoCreateLocalAudio) {
|
||||
devices.push('audio');
|
||||
}
|
||||
|
||||
if (devices.length > 0) {
|
||||
JitsiMeetJS.createLocalTracks({ devices })
|
||||
.then(onLocalTracks)
|
||||
.then(() => {
|
||||
room.join();
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
} else {
|
||||
room.join();
|
||||
}
|
||||
|
||||
updateMaxFrameHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,26 +277,13 @@ JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
|
||||
|
||||
JitsiMeetJS.init(config);
|
||||
|
||||
config.serviceUrl = config.bosh = `${config.websocket || config.bosh}?room=${roomName.toLowerCase()}`;
|
||||
if (config.websocketKeepAliveUrl) {
|
||||
config.websocketKeepAliveUrl += `?room=${roomName.toLowerCase()}`;
|
||||
}
|
||||
|
||||
connection = new JitsiMeetJS.JitsiConnection(null, null, config);
|
||||
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED, onConnectionSuccess);
|
||||
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_FAILED, onConnectionFailed);
|
||||
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED, disconnect);
|
||||
connection.connect();
|
||||
|
||||
const devices = [];
|
||||
|
||||
if (localVideo) {
|
||||
devices.push('video');
|
||||
}
|
||||
if (localAudio) {
|
||||
devices.push('audio');
|
||||
}
|
||||
if (devices.length > 0) {
|
||||
JitsiMeetJS.createLocalTracks({ devices })
|
||||
.then(onLocalTracks)
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
"author": "",
|
||||
"readmeFilename": "../README.md",
|
||||
"dependencies": {
|
||||
"jquery": "3.4.0"
|
||||
"jquery": "3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.5.5",
|
||||
@@ -31,18 +31,19 @@
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-loader": "8.0.4",
|
||||
"eslint": "5.6.1",
|
||||
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#1.0.1",
|
||||
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#1.0.3",
|
||||
"eslint-plugin-flowtype": "2.50.3",
|
||||
"eslint-plugin-import": "2.14.0",
|
||||
"eslint-plugin-import": "2.20.2",
|
||||
"eslint-plugin-jsdoc": "3.8.0",
|
||||
"expose-loader": "0.7.5",
|
||||
"flow-bin": "0.104.0",
|
||||
"imports-loader": "0.7.1",
|
||||
"lodash": "4.17.20",
|
||||
"string-replace-loader": "2.1.1",
|
||||
"style-loader": "0.19.0",
|
||||
"webpack": "4.27.1",
|
||||
"webpack": "4.43.0",
|
||||
"webpack-bundle-analyzer": "3.4.1",
|
||||
"webpack-cli": "3.1.2"
|
||||
"webpack-cli": "3.3.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0",
|
||||
@@ -1,21 +0,0 @@
|
||||
<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<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">
|
||||
<!--#include virtual="/base.html" -->
|
||||
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
|
||||
<!--#include virtual="/title.html" -->
|
||||
<script><!--#include virtual="/interface_config.js" --></script>
|
||||
<script>
|
||||
window.config = {};
|
||||
</script>
|
||||
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
|
||||
<script src="libs/device_selection_popup_bundle.min.js?v=1"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="react"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -196,12 +196,6 @@ module.exports = [
|
||||
},
|
||||
performance: getPerformanceHints(4 * 1024 * 1024)
|
||||
}),
|
||||
Object.assign({}, config, {
|
||||
entry: {
|
||||
'device_selection_popup_bundle': './react/features/settings/popup.js'
|
||||
},
|
||||
performance: getPerformanceHints(750 * 1024)
|
||||
}),
|
||||
Object.assign({}, config, {
|
||||
entry: {
|
||||
'alwaysontop': './react/features/always-on-top/index.js'
|
||||
|
||||
Reference in New Issue
Block a user