Compare commits

...

17 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
9f4c036174 fix(blur) fix model paths 2021-02-18 15:36:37 +01:00
Дамян Минков
8fcaea9e3d feat(load-test): Load test startmuted (#8629)
* feat(load-test): Senders unmute themselves if muted by policy.

* feat(load-test): Adds option to skip creating local audio track.

We currently create local audio track even when starting audio muted. Adding the option to control that can load test that for clients or signalling.
2021-02-17 10:28:01 -06:00
Pawel Domas
e0aab11f98 fix: TypeError: Cannot read property 'isAudioTrack' of undefined
When there's no jitsiTrack set on the base/tracks entry it means
a track is being created (get user media is in progress).
2021-02-17 09:25:09 -06:00
Tudor D. Pop
946339a52e feat(blur) replace BodyPix with TFLite
Use the Google Meet model and SIMD optimized WASM.
2021-02-17 16:03:33 +01:00
tmoldovan8x8
f71e8a9982 feat(mobile) adds actions and events for the chat 2021-02-17 16:26:40 +02:00
chipechop
af6080f173 fix(lang) update Italian translation 2021-02-17 13:25:23 +01:00
Jaya Allamsetty
b1080340ec chore(deps) lib-jitsi-meet@latest
* ref(QualityController): Split send and receive video constraints handling.
* fix: Save guards _features to be always empty and nver undefined. (#1493)

d1f0ab4d5a...c534f74884
2021-02-16 11:16:08 -05:00
damencho
684d121159 fix(load-test): Fixes keepalive url when using load-test. 2021-02-12 13:03:29 -06:00
Saúl Ibarra Corretgé
6740b0861e fix(rn,lobby) make sure the enable dialog follows the theme color 2021-02-12 16:40:56 +01:00
tmoldovan8x8
65c56669c4 feat refactors the chat flow so it has open and close functions 2021-02-12 13:18:16 +02:00
Дамян Минков
2cd43ba2e4 fix(load-test): Always create local audio track. (#8612)
* fix(load-test): Always create local audio track.

When audio mutes will mute the track. Also fixes previous change where we do not add any of the tracks to the room.

* squash: Fix lint errors.
2021-02-11 17:25:46 -06:00
Hristo Terezov
fec2641730 fix(popups): covered by labels. 2021-02-11 16:18:56 -06:00
damencho
04ee423257 fix(load-test): Create local tracks and then join.
Behaves now as the main client, and skips few unnecessary presences.
2021-02-11 15:14:31 -06:00
Hristo Terezov
460e137ee4 chore(deps) lib-jitsi-meet@latest
* fix(GUM-permissions): cache permissions on init.
* feat: Reuse billingId from localstorage as jitsiMeetId.
* fix(example) simplify
* feat(docs) mvoe API documentatrion to the handbook

84357ce1a8...d1f0ab4d5a
2021-02-11 13:56:46 -06:00
hmuresan
1a789130a3 feat (external_api) add command for kick participant 2021-02-11 07:33:26 -06:00
Nando Thomassen
16b00dc2af feat(flags) add feature flag for help button
Introduces a new feature flag ('help.enabled') and uses that to
determine the visibility of the 'Help' button in a call.
2021-02-10 22:34:13 +01:00
Saúl Ibarra Corretgé
c3a41b8cf3 fix(avatar) refactor preloading to avoid CORS issues
Fixes: https://github.com/jitsi/jitsi-meet/issues/8510

This basically reverts
a3fb996ff0
while retaining the same properties that prompted it's original intent, namely
avoiding sending the Referrer header.
2021-02-10 14:32:56 +01:00
59 changed files with 726 additions and 387 deletions

View File

@@ -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.

View File

@@ -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 = .
@@ -26,7 +28,7 @@ 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)
@@ -82,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) ; \
@@ -91,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:

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -4,7 +4,7 @@
top: 30px;
right: 30px;
transition: right 0.5s;
z-index: $filmstripVideosZ + 1;
z-index: $labelsZ;
.circular-label {
align-items: center;

View File

@@ -47,4 +47,5 @@
border-radius: 3px;
margin: -16px -24px;
padding: 16px 24px;
z-index: $popoverZ;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -6,7 +6,6 @@
}
.horizontal-filmstrip .filmstrip {
position: absolute;
bottom: 0;
right: 0;
padding: 10px 5px;

View File

@@ -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 {

View File

@@ -22,11 +22,6 @@
display: none;
}
#remoteConnectionMessage,
.watermark {
z-index: $filmstripVideosZ + 1;
}
/**
* The follow styling uses !important to override inline styles set with
* javascript.

View File

@@ -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;
}

View File

@@ -111,7 +111,3 @@
display: none;
}
}
#videoResolutionLabel {
z-index: $zindex3 + 1;
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
@@ -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];
}
@@ -164,4 +173,24 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[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

View File

@@ -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

View File

@@ -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
/**

View File

@@ -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

View File

@@ -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",

View File

@@ -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 }) => {

View File

@@ -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',

14
package-lock.json generated
View File

@@ -10343,8 +10343,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#84357ce1a8d06c67a502709877130520aaf71592",
"from": "github:jitsi/lib-jitsi-meet#84357ce1a8d06c67a502709877130520aaf71592",
"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",

View File

@@ -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#84357ce1a8d06c67a502709877130520aaf71592",
"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"

View File

@@ -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).

View File

@@ -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;
});
}

View File

@@ -320,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
);
}

View File

@@ -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';

View File

@@ -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.
*

View File

@@ -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
};
}

View File

@@ -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());
}
};
}

View File

@@ -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));
}
}
/**

View File

@@ -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));

View File

@@ -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));

View File

@@ -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);

View File

@@ -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));

View File

@@ -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));

View File

@@ -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.

View File

@@ -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
};
}

View File

@@ -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
};
}

View File

@@ -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>

View File

@@ -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
});
}
);
}
/**

View File

@@ -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);

View File

@@ -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));

View File

@@ -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;

View File

@@ -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);
}

View 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)

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

View File

@@ -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'

View File

@@ -13,15 +13,16 @@ const {
localVideo = config.startWithVideoMuted !== true,
remoteVideo = isHuman,
remoteAudio = isHuman,
autoPlayVideo = config.testing.noAutoPlayVideo !== true
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;
@@ -107,13 +108,15 @@ function onLocalTracks(tracks = []) {
$('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]);
}
}
@@ -146,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);
}
/**
@@ -179,6 +200,7 @@ function onUserLeft(id) {
*/
function onConnectionSuccess() {
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 => {
@@ -187,7 +209,30 @@ 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();
}
@@ -233,27 +278,12 @@ 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;
});
}