Compare commits

...

41 Commits

Author SHA1 Message Date
Hristo Terezov
295b3d9afc ref(Filmstrip): Use Thumbnail component. 2021-02-23 18:49:01 -06:00
Jaya Allamsetty
c11a94f7d7 feat: Add 'useNewBandwidthAllocationStrategy' to config.js. 2021-02-18 14:30:39 -05:00
Jaya Allamsetty
bfd093b0ba chore(deps) lib-jitsi-meet@latest
* feat: Use the new bridge signaling format.

c534f74884...beaff3dd02
2021-02-18 14:30:39 -05:00
Saúl Ibarra Corretgé
861935c9d7 fix(blur) fix model paths 2021-02-18 15:57:01 +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
damencho
f4d0ec1bb4 fix(load-test): Uses websocket if available and adds room param to the connection url. 2021-02-09 15:48:06 -06:00
tmoldovan8x8
ef6b641802 bugfix(ios) changes the participantInfo completion handler reference to strong.
When the method was called from Swift they were collected before calling them.
2021-02-09 18:03:25 +02:00
Saúl Ibarra Corretgé
579acbc570 feat(embed-meeting) add autoplay permission to iframe sample 2021-02-09 08:01:51 -06:00
niteshletxsoft
bca9a12df1 feat(external_ap) add api call to get live stream url 2021-02-09 12:43:38 +01:00
Shoolpani Dubey
a57b967f2e fix(external_api) add autoplay capabilities to created iframe
Should fix https://github.com/jitsi/jitsi-meet/issues/7037
2021-02-09 11:22:42 +01:00
Hristo Terezov
299927fcad docs: Add comment for initial GUM timeout values. 2021-02-08 15:53:38 -06:00
Hristo Terezov
7dc899ace1 ref(DeviceSelectionPopup): remove. 2021-02-08 15:53:38 -06:00
Hristo Terezov
a6c6cd6c56 fix: Add GUM timeout & improve device permissions 2021-02-08 15:53:38 -06:00
Hristo Terezov
7dc45c28a2 fix(AudioSlider): removed when volume is 0 2021-02-08 15:25:17 -06:00
Hristo Terezov
a215f9706a chore(deps) lib-jitsi-meet@latest
* fix(GUM): improve permissions logic.
* feat(GUM): timeout.
* ref: Remove pinEndpoint. (#1440)

30c8795770...84357ce1a8
2021-02-08 14:52:25 -06:00
Jonathan Lennox
4beca0d5dd Set receiver video constraint in load-test script, as-if tile view. (#8567) 2021-02-08 13:35:03 -05:00
Vlad Piersec
bfc4b2ac6f feat(vpaas): Send billing id to prosody 2021-02-08 12:49:55 +02:00
kazan417
53bdaa7928 fix(lang) update Russian translation 2021-02-07 12:44:20 +01:00
Tobias Kneidl
ba18b12024 add module proxy_wstunnel 2021-02-06 22:49:12 -06:00
Tobias Kneidl
a1438f1f21 change websocket url from http:// to ws:// 2021-02-06 22:49:04 -06:00
Julian1203
c856c20513 Update main-de.json
Small improvements (again)
2021-02-06 07:27:55 -06:00
Julian1203
cf92c964b4 Update main-de.json
Small improvements (again)
2021-02-05 22:15:51 -06:00
Julian1203
e69529867e Update main-de.json
Small improvements for the gender-neutral version and other small improvements.
2021-02-05 16:15:39 -06:00
Jaya Allamsetty
fd313c1af7 fix(tests): Add more checks so that test don't error out. 2021-02-05 16:30:58 -05:00
Jonathan Lennox
73c3feb8fa Improve load-test script. (#8563)
* In load-test, merge URL params into config.

* Honor config.testing.noAutoPlayVideo in load-test.
2021-02-05 16:00:28 -05:00
Дамян Минков
67a01364d3 Move load-test to resources (#8560)
* fix: Move load-test to resources.

* squash: Updates load-test dependencies.

* squash: Fix load-test build.
2021-02-05 09:12:45 -06:00
176 changed files with 3469 additions and 3990 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 = .
@@ -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:

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

1
app.js
View File

@@ -1,7 +1,6 @@
/* application specific logic */
import 'jquery';
import 'jquery-contextmenu';
import 'jQuery-Impromptu';
import 'olm';

View File

@@ -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 => {
@@ -1333,9 +1365,6 @@ export default {
.then(() => {
this.localVideo = newTrack;
this._setSharingScreen(newTrack);
if (newTrack) {
APP.UI.addLocalVideoStream(newTrack);
}
this.setVideoMuteStatus(this.isLocalVideoMuted());
})
.then(resolve)
@@ -2295,7 +2324,11 @@ export default {
// There is no guarantee another event will trigger the update
// immediately and in all situations, for example because a remote
// participant is having connection trouble so no status changes.
APP.UI.updateAllVideos();
const displayedUserId = APP.UI.getLargeVideoID();
if (displayedUserId) {
APP.UI.updateLargeVideo(displayedUserId, true);
}
});
APP.UI.addListener(

View File

@@ -240,6 +240,12 @@ var config = {
// 90: 2
// },
// Provides a way to translate the legacy bridge signaling messages, 'LastNChangedEvent',
// 'SelectedEndpointsChangedEvent' and 'ReceiverVideoConstraint' into the new 'ReceiverVideoConstraints' message
// that invokes the new bandwidth allocation algorithm in the bridge which is described here
// - https://github.com/jitsi/jitsi-videobridge/blob/master/doc/allocation.md.
// useNewBandwidthAllocationStrategy: false,
// Specify the settings for video quality optimizations on the client.
// videoQuality: {
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified

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

@@ -110,6 +110,6 @@ ul.popupmenu {
margin: -16px -24px;
}
span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
span.localvideomenu:hover ul.popupmenu, span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
display:block !important;
}

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

@@ -400,7 +400,9 @@
}
}
.local-video-menu-trigger,
.remote-video-menu-trigger,
.localvideomenu,
.remotevideomenu
{
display: inline-block;
@@ -418,6 +420,7 @@
cursor: hand;
}
}
.local-video-menu-trigger,
.remote-video-menu-trigger {
margin-top: 7px;
}
@@ -480,14 +483,6 @@
z-index: $reloadZ; /*The reload button should appear on top of the header!*/
}
.audiolevel {
display: inline-block;
position: absolute;
z-index: $zindex0;
border-radius:1px;
pointer-events: none;
}
#dominantSpeaker {
visibility: hidden;
width: 300px;

View File

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

View File

@@ -19,7 +19,7 @@
0 0 3px $videoThumbnailSelected;
}
.remotevideomenu > .icon-menu {
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
display: none;
}
@@ -32,7 +32,7 @@
box-shadow: inset 0 0 3px $videoThumbnailHovered,
0 0 3px $videoThumbnailHovered;
.remotevideomenu > .icon-menu {
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
display: inline-block;
}
}

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.
@@ -48,6 +43,7 @@
* specifically the various status icons.
*/
.remotevideomenu,
.localvideomenu,
.videocontainer__toptoolbar {
z-index: auto;
}

View File

@@ -51,6 +51,7 @@
* and tooltips from getting a new location context due to translate3d.
*/
.connection-indicator,
.local-video-menu-trigger,
.remote-video-menu-trigger,
.indicator-icon-container {
transform: translate3d(0, 0, 0);
@@ -68,7 +69,9 @@
* Move the remote video menu trigger to the bottom left of the video
* thumbnail.
*/
.localvideomenu,
.remotevideomenu,
.local-video-menu-trigger,
.remote-video-menu-trigger {
bottom: 0;
left: 0;
@@ -76,6 +79,7 @@
right: auto;
}
.local-video-menu-trigger,
.remote-video-menu-trigger {
margin-bottom: 7px;
margin-left: $remoteVideoMenuIconMargin;

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

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

View File

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

View File

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

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

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

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

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

@@ -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": "Защищенная, полнофункциональная и совершенно бесплатная система видеоконференций"
}

View File

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

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';
@@ -30,8 +31,8 @@ import {
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession } from '../../react/features/recording/functions';
import { muteAllParticipants } from '../../react/features/remote-video-menu/actions';
import { toggleTileView } from '../../react/features/video-layout';
import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality';
import { getJitsiMeetTransport } from '../transport';
@@ -342,13 +343,16 @@ function initCommands() {
if (!isChatOpen) {
APP.UI.toggleChat();
}
APP.store.dispatch(setPrivateMessageRecipient(participant));
APP.store.dispatch(openChat(participant));
} else {
logger.error('No participant found for the given participantId');
}
},
'cancel-private-chat': () => {
APP.store.dispatch(setPrivateMessageRecipient());
},
'kick-participant': participantId => {
APP.store.dispatch(kickParticipant(participantId));
}
};
transport.on('event', ({ data, name }) => {
@@ -441,6 +445,23 @@ function initCommands() {
});
break;
}
case 'get-livestream-url': {
const state = APP.store.getState();
const conference = getCurrentConference(state);
let livestreamUrl;
if (conference) {
const activeSession = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
livestreamUrl = activeSession?.liveStreamViewURL;
} else {
logger.error('Conference is not defined');
}
callback({
livestreamUrl
});
break;
}
default:
return false;
}

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

View File

@@ -115,7 +115,6 @@ UI.start = function() {
// Set the defaults for prompt dialogs.
$.prompt.setDefaults({ persistent: false });
VideoLayout.init(eventEmitter);
VideoLayout.initLargeVideo();
// Do not animate the video area on UI start (second argument passed into
@@ -135,7 +134,6 @@ UI.start = function() {
if (config.iAmRecorder) {
// in case of iAmSipGateway keep local video visible
if (!config.iAmSipGateway) {
VideoLayout.setLocalVideoVisible(false);
APP.store.dispatch(setNotificationsEnabled(false));
}
@@ -179,14 +177,6 @@ UI.unbindEvents = () => {
$(window).off('resize');
};
/**
* Show local video stream on UI.
* @param {JitsiTrack} track stream to show
*/
UI.addLocalVideoStream = track => {
VideoLayout.changeLocalVideo(track);
};
/**
* Setup and show Etherpad.
* @param {string} name etherpad id
@@ -227,14 +217,6 @@ UI.addUser = function(user) {
}
};
/**
* Update videotype for specified user.
* @param {string} id user id
* @param {string} newVideoType new videotype
*/
UI.onPeerVideoTypeChanged
= (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
/**
* Updates the user status.
*
@@ -289,19 +271,14 @@ UI.setAudioMuted = function(id) {
* Sets muted video state for participant
*/
UI.setVideoMuted = function(id) {
VideoLayout.onVideoMute(id);
VideoLayout._updateLargeVideoIfDisplayed(id, true);
if (APP.conference.isLocalId(id)) {
APP.conference.updateVideoIconEnabled();
}
};
/**
* Triggers an update of remote video and large video displays so they may pick
* up any state changes that have occurred elsewhere.
*
* @returns {void}
*/
UI.updateAllVideos = () => VideoLayout.updateAllVideos();
UI.updateLargeVideo = (id, forceUpdate) => VideoLayout.updateLargeVideo(id, forceUpdate);
/**
* Adds a listener that would be notified on the given type of event.
@@ -340,8 +317,6 @@ UI.removeListener = function(type, listener) {
*/
UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
UI.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber);
// Used by torture.
UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));

View File

@@ -1,67 +0,0 @@
/* global $, APP */
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { Thumbnail } from '../../../react/features/filmstrip';
import SmallVideo from '../videolayout/SmallVideo';
/* eslint-enable no-unused-vars */
/**
*
*/
export default class SharedVideoThumb extends SmallVideo {
/**
*
* @param {*} participant
*/
constructor(participant) {
super();
this.id = participant.id;
this.isLocal = false;
this.url = participant.id;
this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container);
this.renderThumbnail();
this._setThumbnailSize();
this.bindHoverHandler();
this.container.onclick = this._onContainerClick;
}
/**
*
* @param {*} spanId
*/
createContainer(spanId) {
const container = document.createElement('span');
container.id = spanId;
container.className = 'videocontainer';
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return container;
}
/**
* Renders the thumbnail.
*/
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
}

View File

@@ -25,129 +25,6 @@ const Filmstrip = {
*/
getVerticalFilmstripWidth() {
return isFilmstripVisible(APP.store) ? getVerticalFilmstripVisibleAreaWidth() : 0;
},
/**
* Resizes thumbnails for tile view.
*
* @param {number} width - The new width of the thumbnails.
* @param {number} height - The new height of the thumbnails.
* @param {boolean} forceUpdate
* @returns {void}
*/
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
const thumbs = this._getThumbs(!forceUpdate);
if (thumbs.localThumb) {
thumbs.localThumb.css({
'padding-top': '',
height: `${height}px`,
'min-height': `${height}px`,
'min-width': `${width}px`,
width: `${width}px`
});
}
if (thumbs.remoteThumbs) {
thumbs.remoteThumbs.css({
'padding-top': '',
height: `${height}px`,
'min-height': `${height}px`,
'min-width': `${width}px`,
width: `${width}px`
});
}
},
/**
* Resizes thumbnails for horizontal view.
*
* @param {Object} dimensions - The new dimensions of the thumbnails.
* @param {boolean} forceUpdate
* @returns {void}
*/
resizeThumbnailsForHorizontalView({ local = {}, remote = {} }, forceUpdate = false) {
const thumbs = this._getThumbs(!forceUpdate);
if (thumbs.localThumb) {
const { height, width } = local;
thumbs.localThumb.css({
height: `${height}px`,
'min-height': `${height}px`,
'min-width': `${width}px`,
width: `${width}px`
});
}
if (thumbs.remoteThumbs) {
const { height, width } = remote;
thumbs.remoteThumbs.css({
height: `${height}px`,
'min-height': `${height}px`,
'min-width': `${width}px`,
width: `${width}px`
});
}
},
/**
* Resizes thumbnails for vertical view.
*
* @returns {void}
*/
resizeThumbnailsForVerticalView() {
const thumbs = this._getThumbs(true);
if (thumbs.localThumb) {
const heightToWidthPercent = 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
thumbs.localThumb.css({
'padding-top': `${heightToWidthPercent}%`,
width: '',
height: '',
'min-width': '',
'min-height': ''
});
}
if (thumbs.remoteThumbs) {
const heightToWidthPercent = 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO;
thumbs.remoteThumbs.css({
'padding-top': `${heightToWidthPercent}%`,
width: '',
height: '',
'min-width': '',
'min-height': ''
});
}
},
/**
* Returns thumbnails of the filmstrip
* @param onlyVisible
* @returns {object} thumbnails
*/
_getThumbs(onlyVisible = false) {
let selector = 'span';
if (onlyVisible) {
selector += ':visible';
}
const localThumb = $('#localVideoContainer');
const remoteThumbs = $('#filmstripRemoteVideosContainer').children(selector);
// Exclude the local video container if it has been hidden.
if (localThumb.hasClass('hidden')) {
return { remoteThumbs };
}
return { remoteThumbs,
localThumb };
}
};

View File

@@ -22,7 +22,6 @@ import {
import { PresenceLabel } from '../../../react/features/presence-status';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import { createDeferred } from '../../util/helpers';
import AudioLevels from '../audio_levels/AudioLevels';
@@ -51,21 +50,19 @@ export default class LargeVideoManager {
/**
*
*/
constructor(emitter) {
constructor() {
/**
* The map of <tt>LargeContainer</tt>s where the key is the video
* container type.
* @type {Object.<string, LargeContainer>}
*/
this.containers = {};
this.eventEmitter = emitter;
this.state = VIDEO_CONTAINER_TYPE;
// FIXME: We are passing resizeContainer as parameter which is calling
// Container.resize. Probably there's better way to implement this.
this.videoContainer = new VideoContainer(
() => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
this.videoContainer = new VideoContainer(() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
// use the same video container to handle desktop tracks
@@ -300,7 +297,6 @@ export default class LargeVideoManager {
// after everything is done check again if there are any pending
// new streams.
this.updateInProcess = false;
this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
this.scheduleLargeVideoUpdate();
});
}

View File

@@ -1,213 +0,0 @@
/* global $, config, APP */
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VideoTrack } from '../../../react/features/base/media';
import { updateSettings } from '../../../react/features/base/settings';
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import SmallVideo from './SmallVideo';
/**
*
*/
export default class LocalVideo extends SmallVideo {
/**
*
* @param {*} emitter
* @param {*} streamEndedCallback
*/
constructor(emitter, streamEndedCallback) {
super();
this.videoSpanId = 'localVideoContainer';
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
this.$container = $(this.container);
this.isLocal = true;
this._setThumbnailSize();
this.updateDOMLocation();
this.renderThumbnail();
this.localVideoId = null;
this.bindHoverHandler();
if (!config.disableLocalVideoFlip) {
this._buildContextMenu();
}
this.emitter = emitter;
Object.defineProperty(this, 'id', {
get() {
return APP.conference.getMyUserId();
}
});
this.initBrowserSpecificProperties();
this.container.onclick = this._onContainerClick;
}
/**
*
*/
createContainer() {
const containerSpan = document.createElement('span');
containerSpan.classList.add('videocontainer');
containerSpan.id = this.videoSpanId;
return containerSpan;
}
/**
* Renders the thumbnail.
*/
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
/**
*
* @param {*} stream
*/
changeVideo(stream) {
this.localVideoId = `localVideo_${stream.getId()}`;
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
const settings = APP.store.getState()['features/base/settings'];
this._enableDisableContextMenu(isVideo);
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
this._notifyOfStreamEnded();
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
}
/**
* Notify any subscribers of the local video stream ending.
*
* @private
* @returns {void}
*/
_notifyOfStreamEnded() {
if (this.streamEndedCallback) {
this.streamEndedCallback(this.id);
}
}
/**
* Shows or hides the local video container.
* @param {boolean} true to make the local video container visible, false
* otherwise
*/
setVisible(visible) {
// We toggle the hidden class as an indication to other interested parties
// that this container has been hidden on purpose.
this.$container.toggleClass('hidden');
// We still show/hide it as we need to overwrite the style property if we
// want our action to take effect. Toggling the display property through
// the above css class didn't succeed in overwriting the style.
if (visible) {
this.$container.show();
} else {
this.$container.hide();
}
}
/**
* Sets the flipX state of the video.
* @param val {boolean} true for flipped otherwise false;
*/
setFlipX(val) {
this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
if (!this.localVideoId) {
return;
}
if (val) {
this.selectVideoElement().addClass('flipVideoX');
} else {
this.selectVideoElement().removeClass('flipVideoX');
}
}
/**
* Builds the context menu for the local video.
*/
_buildContextMenu() {
$.contextMenu({
selector: `#${this.videoSpanId}`,
zIndex: 10000,
items: {
flip: {
name: 'Flip',
callback: () => {
const { store } = APP;
const val = !store.getState()['features/base/settings']
.localFlipX;
this.setFlipX(val);
store.dispatch(updateSettings({
localFlipX: val
}));
}
}
},
events: {
show(options) {
options.items.flip.name
= APP.translation.generateTranslationHTML(
'videothumbnail.flip');
}
}
});
}
/**
* Enables or disables the context menu for the local video.
* @param enable {boolean} true for enable, false for disable
*/
_enableDisableContextMenu(enable) {
if (this.$container.contextMenu) {
this.$container.contextMenu(enable);
}
}
/**
* Places the {@code LocalVideo} in the DOM based on the current video layout.
*
* @returns {void}
*/
updateDOMLocation() {
if (!this.container) {
return;
}
if (this.container.parentElement) {
this.container.parentElement.removeChild(this.container);
}
const appendTarget = shouldDisplayTileView(APP.store.getState())
? document.getElementById('localVideoTileViewContainer')
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
}
}

View File

@@ -1,240 +0,0 @@
/* global $, APP, config */
/* eslint-disable no-unused-vars */
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import Logger from 'jitsi-meet-logger';
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import { getParticipantById } from '../../../react/features/base/participants';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
import { PresenceLabel } from '../../../react/features/presence-status';
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
/* eslint-enable no-unused-vars */
import UIUtils from '../util/UIUtil';
import SmallVideo from './SmallVideo';
const logger = Logger.getLogger(__filename);
/**
* List of container events that we are going to process, will be added as listener to the
* container for every event in the list. The latest event will be stored in redux.
*/
const containerEvents = [
'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart',
'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting'
];
/**
*
* @param {*} spanId
*/
function createContainer(spanId) {
const container = document.createElement('span');
container.id = spanId;
container.className = 'videocontainer';
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return container;
}
/**
*
*/
export default class RemoteVideo extends SmallVideo {
/**
* Creates new instance of the <tt>RemoteVideo</tt>.
* @param user {JitsiParticipant} the user for whom remote video instance will
* be created.
* @constructor
*/
constructor(user) {
super();
this.user = user;
this.id = user.getId();
this.videoSpanId = `participant_${this.id}`;
this.addRemoteVideoContainer();
this.bindHoverHandler();
this.flipX = false;
this.isLocal = false;
/**
* The flag is set to <tt>true</tt> after the 'canplay' event has been
* triggered on the current video element. It goes back to <tt>false</tt>
* when the stream is removed. It is used to determine whether the video
* playback has ever started.
* @type {boolean}
*/
this._canPlayEventReceived = false;
this.container.onclick = this._onContainerClick;
}
/**
*
*/
addRemoteVideoContainer() {
this.container = createContainer(this.videoSpanId);
this.$container = $(this.container);
this.renderThumbnail();
this._setThumbnailSize();
this.initBrowserSpecificProperties();
return this.container;
}
/**
* Renders the thumbnail.
*/
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
/**
* Removes the remote stream element corresponding to the given stream and
* parent container.
*
* @param stream the MediaStream
* @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
*/
removeRemoteStreamElement(stream) {
if (!this.container) {
return false;
}
const isVideo = stream.isVideoTrack();
const elementID = `remoteVideo_${stream.getId()}`;
const select = $(`#${elementID}`);
select.remove();
if (isVideo) {
this._canPlayEventReceived = false;
}
logger.info(`Video removed ${this.id}`, select);
this.updateView();
}
/**
* The remote video is considered "playable" once the can play event has been received.
*
* @inheritdoc
* @override
*/
isVideoPlayable() {
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
}
/**
* @inheritDoc
*/
updateView() {
this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
super.updateView();
}
/**
* Removes RemoteVideo from the page.
*/
remove() {
ReactDOM.unmountComponentAtNode(this.container);
super.remove();
}
/**
*
* @param {*} streamElement
* @param {*} stream
*/
waitForPlayback(streamElement, stream) {
$(streamElement).hide();
const webRtcStream = stream.getOriginalStream();
const isVideo = stream.isVideoTrack();
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
return;
}
const listener = () => {
this._canPlayEventReceived = true;
logger.info(`${this.id} video is now active`, streamElement);
if (streamElement) {
$(streamElement).show();
}
streamElement.removeEventListener('canplay', listener);
// Refresh to show the video
this.updateView();
};
streamElement.addEventListener('canplay', listener);
}
/**
*
* @param {*} stream
*/
addRemoteStreamElement(stream) {
if (!this.container) {
logger.debug('Not attaching remote stream due to no container');
return;
}
const isVideo = stream.isVideoTrack();
if (!stream.getOriginalStream()) {
logger.debug('Remote video stream has no original stream');
return;
}
let streamElement = document.createElement('video');
streamElement.autoplay = !config.testing?.noAutoPlayVideo;
streamElement.id = `remoteVideo_${stream.getId()}`;
// Put new stream element always in front
streamElement = UIUtils.prependChild(this.container, streamElement);
this.waitForPlayback(streamElement, stream);
stream.attach(streamElement);
if (isVideo && isTestModeEnabled(APP.store.getState())) {
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
containerEvents.forEach(event => {
streamElement.addEventListener(event, cb.bind(this, event));
});
}
}
}

View File

@@ -1,509 +0,0 @@
/* global $, APP, interfaceConfig */
/* eslint-disable no-unused-vars */
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import Logger from 'jitsi-meet-logger';
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator';
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
import { i18next } from '../../../react/features/base/i18n';
import { MEDIA_TYPE } from '../../../react/features/base/media';
import {
getLocalParticipant,
getParticipantById,
getParticipantCount,
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import {
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
isLocalTrackMuted,
isRemoteTrackMuted
} from '../../../react/features/base/tracks';
import { ConnectionIndicator } from '../../../react/features/connection-indicator';
import { DisplayName } from '../../../react/features/display-name';
import {
DominantSpeakerIndicator,
RaisedHandIndicator,
StatusIndicators,
isVideoPlayable
} from '../../../react/features/filmstrip';
import {
LAYOUTS,
getCurrentLayout,
setTileView,
shouldDisplayTileView
} from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = Logger.getLogger(__filename);
/**
* Display mode constant used when video is being displayed on the small video.
* @type {number}
* @constant
*/
const DISPLAY_VIDEO = 0;
/**
* Display mode constant used when the user's avatar is being displayed on
* the small video.
* @type {number}
* @constant
*/
const DISPLAY_AVATAR = 1;
/**
* Display mode constant used when neither video nor avatar is being displayed
* on the small video. And we just show the display name.
* @type {number}
* @constant
*/
const DISPLAY_BLACKNESS_WITH_NAME = 2;
/**
* Display mode constant used when video is displayed and display name
* at the same time.
* @type {number}
* @constant
*/
const DISPLAY_VIDEO_WITH_NAME = 3;
/**
* Display mode constant used when neither video nor avatar is being displayed
* on the small video. And we just show the display name.
* @type {number}
* @constant
*/
const DISPLAY_AVATAR_WITH_NAME = 4;
/**
*
*/
export default class SmallVideo {
/**
* Constructor.
*/
constructor() {
this.videoIsHovered = false;
this.videoType = undefined;
// Bind event handlers so they are only bound once for every instance.
this.updateView = this.updateView.bind(this);
this._onContainerClick = this._onContainerClick.bind(this);
}
/**
* Returns the identifier of this small video.
*
* @returns the identifier of this small video
*/
getId() {
return this.id;
}
/**
* Indicates if this small video is currently visible.
*
* @return <tt>true</tt> if this small video isn't currently visible and
* <tt>false</tt> - otherwise.
*/
isVisible() {
return this.$container.is(':visible');
}
/**
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
*/
bindHoverHandler() {
// Add hover handler
this.$container.hover(
() => {
this.videoIsHovered = true;
this.renderThumbnail(true);
this.updateView();
},
() => {
this.videoIsHovered = false;
this.renderThumbnail(false);
this.updateView();
}
);
}
/**
* Renders the thumbnail.
*/
renderThumbnail() {
// Should be implemented by in subclasses.
}
/**
* This is an especially interesting function. A naive reader might think that
* it returns this SmallVideo's "video" element. But it is much more exciting.
* It first finds this video's parent element using jquery, then uses a utility
* from lib-jitsi-meet to extract the video element from it (with two more
* jquery calls), and finally uses jquery again to encapsulate the video element
* in an array. This last step allows (some might prefer "forces") users of
* this function to access the video element via the 0th element of the returned
* array (after checking its length of course!).
*/
selectVideoElement() {
return $($(this.container).find('video')[0]);
}
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
*
* @param isFocused indicates if the thumbnail should be focused/pinned or not
*/
focus(isFocused) {
const focusedCssClass = 'videoContainerFocused';
const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
if (!isFocused && isFocusClassEnabled) {
this.$container.removeClass(focusedCssClass);
} else if (isFocused && !isFocusClassEnabled) {
this.$container.addClass(focusedCssClass);
}
}
/**
*
*/
hasVideo() {
return this.selectVideoElement().length !== 0;
}
/**
* Checks whether the user associated with this <tt>SmallVideo</tt> is currently
* being displayed on the "large video".
*
* @return {boolean} <tt>true</tt> if the user is displayed on the large video
* or <tt>false</tt> otherwise.
*/
isCurrentlyOnLargeVideo() {
return APP.store.getState()['features/large-video']?.participantId === this.id;
}
/**
* Checks whether there is a playable video stream available for the user
* associated with this <tt>SmallVideo</tt>.
*
* @return {boolean} <tt>true</tt> if there is a playable video stream available
* or <tt>false</tt> otherwise.
*/
isVideoPlayable() {
return isVideoPlayable(APP.store.getState(), this.id);
}
/**
* Determines what should be display on the thumbnail.
*
* @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
* or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
*/
selectDisplayMode(input) {
if (!input.tileViewActive && input.isScreenSharing) {
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
} else if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
// Display name is always and only displayed when user is on the stage
return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
} else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
// check hovering and change state to video with name
return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
}
// check hovering and change state to avatar with name
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
}
/**
* Computes information that determine the display mode.
*
* @returns {Object}
*/
computeDisplayModeInput() {
let isScreenSharing = false;
let connectionStatus;
const state = APP.store.getState();
const id = this.id;
const participant = getParticipantById(state, id);
const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks'];
const videoTrack
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
isScreenSharing = videoTrack?.videoType === 'desktop';
connectionStatus = participant.connectionStatus;
}
return {
isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
isHovered: this._isHovered(),
isAudioOnly: APP.conference.isAudioOnly(),
tileViewActive: shouldDisplayTileView(state),
isVideoPlayable: this.isVideoPlayable(),
hasVideo: Boolean(this.selectVideoElement().length),
connectionStatus,
canPlayEventReceived: this._canPlayEventReceived,
videoStream: Boolean(videoTrack),
isScreenSharing,
videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
};
}
/**
* Checks whether current video is considered hovered. Currently it is hovered
* if the mouse is over the video, or if the connection
* indicator is shown(hovered).
* @private
*/
_isHovered() {
return this.videoIsHovered;
}
/**
* Updates the css classes of the thumbnail based on the current state.
*/
updateView() {
this.$container.removeClass((index, classNames) =>
classNames.split(' ').filter(name => name.startsWith('display-')));
const oldDisplayMode = this.displayMode;
let displayModeString = '';
const displayModeInput = this.computeDisplayModeInput();
// Determine whether video, avatar or blackness should be displayed
this.displayMode = this.selectDisplayMode(displayModeInput);
switch (this.displayMode) {
case DISPLAY_AVATAR_WITH_NAME:
displayModeString = 'avatar-with-name';
this.$container.addClass('display-avatar-with-name');
break;
case DISPLAY_BLACKNESS_WITH_NAME:
displayModeString = 'blackness-with-name';
this.$container.addClass('display-name-on-black');
break;
case DISPLAY_VIDEO:
displayModeString = 'video';
this.$container.addClass('display-video');
break;
case DISPLAY_VIDEO_WITH_NAME:
displayModeString = 'video-with-name';
this.$container.addClass('display-name-on-video');
break;
case DISPLAY_AVATAR:
default:
displayModeString = 'avatar';
this.$container.addClass('display-avatar-only');
break;
}
if (this.displayMode !== oldDisplayMode) {
logger.debug(`Displaying ${displayModeString} for ${this.id}, data: [${JSON.stringify(displayModeInput)}]`);
}
if (this.displayMode !== DISPLAY_VIDEO
&& this.displayMode !== DISPLAY_VIDEO_WITH_NAME
&& displayModeInput.tileViewActive
&& displayModeInput.isScreenSharing
&& !displayModeInput.isAudioOnly) {
// send the event
sendAnalytics(createScreenSharingIssueEvent({
source: 'thumbnail',
...displayModeInput
}));
}
}
/**
* Shows or hides the dominant speaker indicator.
* @param show whether to show or hide.
*/
showDominantSpeakerIndicator(show) {
// Don't create and show dominant speaker indicator if
// DISABLE_DOMINANT_SPEAKER_INDICATOR is true
if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
return;
}
if (!this.container) {
logger.warn(`Unable to set dominant speaker indicator - ${this.videoSpanId} does not exist`);
return;
}
this.$container.toggleClass('active-speaker', show);
}
/**
* Initalizes any browser specific properties. Currently sets the overflow
* property for Qt browsers on Windows to hidden, thus fixing the following
* problem:
* Some browsers don't have full support of the object-fit property for the
* video element and when we set video object-fit to "cover" the video
* actually overflows the boundaries of its container, so it's important
* to indicate that the "overflow" should be hidden.
*
* Setting this property for all browsers will result in broken audio levels,
* which makes this a temporary solution, before reworking audio levels.
*/
initBrowserSpecificProperties() {
const userAgent = window.navigator.userAgent;
if (userAgent.indexOf('QtWebEngine') > -1
&& (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1)) {
this.$container.css('overflow', 'hidden');
}
}
/**
* Cleans up components on {@code SmallVideo} and removes itself from the DOM.
*
* @returns {void}
*/
remove() {
logger.log('Remove thumbnail', this.id);
this._unmountThumbnail();
// Remove whole container
if (this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
}
/**
* Helper function for re-rendering multiple react components of the small
* video.
*
* @returns {void}
*/
rerender() {
this.updateView();
}
/**
* Callback invoked when the thumbnail is clicked and potentially trigger
* pinning of the participant.
*
* @param {MouseEvent} event - The click event to intercept.
* @private
* @returns {void}
*/
_onContainerClick(event) {
const triggerPin = this._shouldTriggerPin(event);
if (event.stopPropagation && triggerPin) {
event.stopPropagation();
event.preventDefault();
}
if (triggerPin) {
this.togglePin();
}
return false;
}
/**
* Returns whether or not a click event is targeted at certain elements which
* should not trigger a pin.
*
* @param {MouseEvent} event - The click event to intercept.
* @private
* @returns {boolean}
*/
_shouldTriggerPin(event) {
// TODO Checking the classes is a workround to allow events to bubble into
// the DisplayName component if it was clicked. React's synthetic events
// will fire after jQuery handlers execute, so stop propogation at this
// point will prevent DisplayName from getting click events. This workaround
// should be removeable once LocalVideo is a React Component because then
// the components share the same eventing system.
const $source = $(event.target || event.srcElement);
return $source.parents('.displayNameContainer').length === 0
&& $source.parents('.popover').length === 0
&& !event.target.classList.contains('popover');
}
/**
* Pins the participant displayed by this thumbnail or unpins if already pinned.
*
* @returns {void}
*/
togglePin() {
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
APP.store.dispatch(pinParticipant(participantIdToPin));
}
/**
* Unmounts the thumbnail.
*/
_unmountThumbnail() {
ReactDOM.unmountComponentAtNode(this.container);
}
/**
* Sets the size of the thumbnail.
*/
_setThumbnailSize() {
const layout = getCurrentLayout(APP.store.getState());
const heightToWidthPercent = 100
/ (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO);
switch (layout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
this.$container.css('padding-top', `${heightToWidthPercent}%`);
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const state = APP.store.getState();
const { local, remote } = state['features/filmstrip'].horizontalViewDimensions;
const size = this.isLocal ? local : remote;
if (typeof size !== 'undefined') {
const { height, width } = size;
this.$container.css({
height: `${height}px`,
'min-height': `${height}px`,
'min-width': `${width}px`,
width: `${width}px`
});
}
break;
}
case LAYOUTS.TILE_VIEW: {
const state = APP.store.getState();
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
if (typeof thumbnailSize !== 'undefined') {
const { height, width } = thumbnailSize;
this.$container.css({
height: `${height}px`,
'min-height': `${height}px`,
'min-width': `${width}px`,
width: `${width}px`
});
}
break;
}
}
}
}

View File

@@ -9,7 +9,6 @@ import { isTestModeEnabled } from '../../../react/features/base/testing';
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
import Filmstrip from './Filmstrip';
@@ -187,16 +186,13 @@ export class VideoContainer extends LargeContainer {
* Creates new VideoContainer instance.
* @param resizeContainer {Function} function that takes care of the size
* of the video container.
* @param emitter {EventEmitter} the event emitter that will be used by
* this instance.
*/
constructor(resizeContainer, emitter) {
constructor(resizeContainer) {
super();
this.stream = null;
this.userId = null;
this.videoType = null;
this.localFlipX = true;
this.emitter = emitter;
this.resizeContainer = resizeContainer;
/**
@@ -534,7 +530,6 @@ export class VideoContainer extends LargeContainer {
this.$avatar.css('visibility', show ? 'visible' : 'hidden');
this.avatarDisplayed = show;
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE, show);
APP.API.notifyLargeVideoVisibilityChanged(show);
}

View File

@@ -4,88 +4,29 @@ import Logger from 'jitsi-meet-logger';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
import {
getLocalParticipant as getLocalParticipantFromStore,
getPinnedParticipant,
getParticipantById,
pinParticipant
getParticipantById
} from '../../../react/features/base/participants';
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
import UIEvents from '../../../service/UI/UIEvents';
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
import LargeVideoManager from './LargeVideoManager';
import LocalVideo from './LocalVideo';
import RemoteVideo from './RemoteVideo';
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
const logger = Logger.getLogger(__filename);
const remoteVideos = {};
let localVideoThumbnail = null;
let eventEmitter = null;
let largeVideo;
/**
* flipX state of the localVideo
*/
let localFlipX = null;
/**
* Handler for local flip X changed event.
* @param {Object} val
*/
function onLocalFlipXChanged(val) {
localFlipX = val;
if (largeVideo) {
largeVideo.onLocalFlipXChange(val);
}
}
/**
* Returns an array of all thumbnails in the filmstrip.
*
* @private
* @returns {Array}
*/
function getAllThumbnails() {
return [
...localVideoThumbnail ? [ localVideoThumbnail ] : [],
...Object.values(remoteVideos)
];
}
/**
* Private helper to get the redux representation of the local participant.
*
* @private
* @returns {Object}
*/
function getLocalParticipant() {
return getLocalParticipantFromStore(APP.store.getState());
}
const VideoLayout = {
init(emitter) {
eventEmitter = emitter;
localVideoThumbnail = new LocalVideo(
emitter,
this._updateLargeVideoIfDisplayed.bind(this));
this.registerListeners();
},
/**
* Registering listeners for UI events in Video layout component.
*
* @returns {void}
* Handler for local flip X changed event.
*/
registerListeners() {
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
onLocalFlipXChanged);
onLocalFlipXChanged() {
if (largeVideo) {
const { store } = APP;
const { localFlipX } = store.getState()['features/base/settings'];
largeVideo.onLocalFlipXChange(localFlipX);
}
},
/**
@@ -95,13 +36,16 @@ const VideoLayout = {
*/
reset() {
this._resetLargeVideo();
this._resetFilmstrip();
},
initLargeVideo() {
this._resetLargeVideo();
largeVideo = new LargeVideoManager(eventEmitter);
largeVideo = new LargeVideoManager();
const { store } = APP;
const { localFlipX } = store.getState()['features/base/settings'];
if (localFlipX) {
largeVideo.onLocalFlipXChange(localFlipX);
}
@@ -120,55 +64,6 @@ const VideoLayout = {
}
},
changeLocalVideo(stream) {
const localId = getLocalParticipant().id;
this.onVideoTypeChanged(localId, stream.videoType);
localVideoThumbnail.changeVideo(stream);
this._updateLargeVideoIfDisplayed(localId);
},
/**
* Shows/hides local video.
* @param {boolean} true to make the local video visible, false - otherwise
*/
setLocalVideoVisible(visible) {
localVideoThumbnail.setVisible(visible);
},
onRemoteStreamAdded(stream) {
const id = stream.getParticipantId();
const remoteVideo = remoteVideos[id];
logger.debug(`Received a new ${stream.getType()} stream for ${id}`);
if (!remoteVideo) {
logger.debug('No remote video element to add stream');
return;
}
remoteVideo.addRemoteStreamElement(stream);
this.onVideoMute(id);
remoteVideo.updateView();
},
onRemoteStreamRemoved(stream) {
const id = stream.getParticipantId();
const remoteVideo = remoteVideos[id];
// Remote stream may be removed after participant left the conference.
if (remoteVideo) {
remoteVideo.removeRemoteStreamElement(stream);
remoteVideo.updateView();
}
this.updateVideoMutedForNoTracks(id);
},
/**
* FIXME get rid of this method once muted indicator are reactified (by
* making sure that user with no tracks is displayed as muted )
@@ -180,7 +75,7 @@ const VideoLayout = {
const participant = APP.conference.getParticipantById(participantId);
if (participant && !participant.getTracksByMediaType('video').length) {
APP.UI.setVideoMuted(participantId);
VideoLayout._updateLargeVideoIfDisplayed(participantId, true);
}
},
@@ -202,110 +97,12 @@ const VideoLayout = {
return videoTrack?.videoType;
},
isPinned(id) {
return id === this.getPinnedId();
},
getPinnedId() {
const { id } = getPinnedParticipant(APP.store.getState()) || {};
return id || null;
},
/**
* Triggers a thumbnail to pin or unpin itself.
*
* @param {number} videoNumber - The index of the video to toggle pin on.
* @private
*/
togglePin(videoNumber) {
const videos = getAllThumbnails();
const videoView = videos[videoNumber];
videoView && videoView.togglePin();
},
/**
* Callback invoked to update display when the pin participant has changed.
*
* @paramn {string|null} pinnedParticipantID - The participant ID of the
* participant that is pinned or null if no one is pinned.
* @returns {void}
*/
onPinChange(pinnedParticipantID) {
getAllThumbnails().forEach(thumbnail =>
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
},
/**
* Creates a participant container for the given id.
*
* @param {Object} participant - The redux representation of a remote
* participant.
* @returns {void}
*/
addRemoteParticipantContainer(participant) {
if (!participant || participant.local) {
return;
} else if (participant.isFakeParticipant) {
const sharedVideoThumb = new SharedVideoThumb(participant);
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
return;
}
const id = participant.id;
const jitsiParticipant = APP.conference.getParticipantById(id);
const remoteVideo = new RemoteVideo(jitsiParticipant);
this.addRemoteVideoContainer(id, remoteVideo);
this.updateVideoMutedForNoTracks(id);
},
/**
* Adds remote video container for the given id and <tt>SmallVideo</tt>.
*
* @param {string} the id of the video to add
* @param {SmallVideo} smallVideo the small video instance to add as a
* remote video
*/
addRemoteVideoContainer(id, remoteVideo) {
remoteVideos[id] = remoteVideo;
// Initialize the view
remoteVideo.updateView();
},
/**
* On video muted event.
*/
onVideoMute(id) {
if (APP.conference.isLocalId(id)) {
localVideoThumbnail && localVideoThumbnail.updateView();
} else {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateView();
}
}
// large video will show avatar instead of muted stream
this._updateLargeVideoIfDisplayed(id, true);
},
/**
* On dominant speaker changed event.
*
* @param {string} id - The participant ID of the new dominant speaker.
* @returns {void}
*/
onDominantSpeakerChanged(id) {
getAllThumbnails().forEach(thumbnail =>
thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
},
/**
* Shows/hides warning about a user's connectivity issues.
*
@@ -321,12 +118,6 @@ const VideoLayout = {
// We have to trigger full large video update to transition from
// avatar to video on connectivity restored.
this._updateLargeVideoIfDisplayed(id, true);
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateView();
}
},
/**
@@ -339,58 +130,14 @@ const VideoLayout = {
*/
onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
if (endpointsLeavingLastN) {
endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
endpointsLeavingLastN.forEach(this._updateLargeVideoIfDisplayed, this);
}
if (endpointsEnteringLastN) {
endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
endpointsEnteringLastN.forEach(this._updateLargeVideoIfDisplayed, this);
}
},
/**
* Updates remote video by id if it exists.
* @param {string} id of the remote video
* @private
*/
_updateRemoteVideo(id) {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateView();
this._updateLargeVideoIfDisplayed(id);
}
},
removeParticipantContainer(id) {
// Unlock large video
if (this.getPinnedId() === id) {
logger.info('Focused video owner has left the conference');
APP.store.dispatch(pinParticipant(null));
}
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
// Remove remote video
logger.info(`Removing remote video: ${id}`);
delete remoteVideos[id];
remoteVideo.remove();
} else {
logger.warn(`No remote video for ${id}`);
}
},
onVideoTypeChanged(id, newVideoType) {
const remoteVideo = remoteVideos[id];
if (!remoteVideo) {
return;
}
logger.info('Peer video type changed: ', id, newVideoType);
remoteVideo.updateView();
},
/**
* Resizes the video area.
*/
@@ -401,15 +148,6 @@ const VideoLayout = {
}
},
getSmallVideo(id) {
if (APP.conference.isLocalId(id)) {
return localVideoThumbnail;
}
return remoteVideos[id];
},
changeUserAvatar(id, avatarUrl) {
if (this.isCurrentlyOnLarge(id)) {
largeVideo.updateAvatar(avatarUrl);
@@ -432,24 +170,6 @@ const VideoLayout = {
return largeVideo && largeVideo.id === id;
},
/**
* Triggers an update of remote video and large video displays so they may
* pick up any state changes that have occurred elsewhere.
*
* @returns {void}
*/
updateAllVideos() {
const displayedUserId = this.getLargeVideoID();
if (displayedUserId) {
this.updateLargeVideo(displayedUserId, true);
}
Object.keys(remoteVideos).forEach(video => {
remoteVideos[video].updateView();
});
},
updateLargeVideo(id, forceUpdate) {
if (!largeVideo) {
return;
@@ -510,13 +230,6 @@ const VideoLayout = {
return Promise.resolve();
}
const currentId = largeVideo.id;
let oldSmallVideo;
if (currentId) {
oldSmallVideo = this.getSmallVideo(currentId);
}
let containerTypeToShow = type;
// if we are hiding a container and there is focusedVideo
@@ -533,12 +246,7 @@ const VideoLayout = {
}
}
return largeVideo.showContainer(containerTypeToShow)
.then(() => {
if (oldSmallVideo) {
oldSmallVideo && oldSmallVideo.updateView();
}
});
return largeVideo.showContainer(containerTypeToShow);
},
isLargeContainerTypeVisible(type) {
@@ -561,14 +269,6 @@ const VideoLayout = {
return largeVideo;
},
/**
* Sets the flipX state of the local video.
* @param {boolean} true for flipped otherwise false;
*/
setLocalFlipX(val) {
this.localFlipX = val;
},
/**
* Returns the wrapper jquery selector for the largeVideo
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
@@ -577,15 +277,6 @@ const VideoLayout = {
return this.getCurrentlyOnLargeContainer().$wrapper;
},
/**
* Returns the number of remove video ids.
*
* @returns {number} The number of remote videos.
*/
getRemoteVideosCount() {
return Object.keys(remoteVideos).length;
},
/**
* Helper method to invoke when the video layout has changed and elements
* have to be re-arranged and resized.
@@ -593,12 +284,7 @@ const VideoLayout = {
* @returns {void}
*/
refreshLayout() {
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
VideoLayout.resizeVideoArea();
// Rerender the thumbnails since they are dependant on the layout because of the tooltip positioning.
localVideoThumbnail && localVideoThumbnail.rerender();
Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender());
},
/**
@@ -615,26 +301,6 @@ const VideoLayout = {
largeVideo = null;
},
/**
* Cleans up filmstrip state. While a separate {@code Filmstrip} exists, its
* implementation is mainly for querying and manipulating the DOM while
* state mostly remains in {@code VideoLayout}.
*
* @private
* @returns {void}
*/
_resetFilmstrip() {
Object.keys(remoteVideos).forEach(remoteVideoId => {
this.removeParticipantContainer(remoteVideoId);
delete remoteVideos[remoteVideoId];
});
if (localVideoThumbnail) {
localVideoThumbnail.remove();
localVideoThumbnail = null;
}
},
/**
* Triggers an update of large video if the passed in participant is
* currently displayed on large video.

View File

@@ -9,6 +9,7 @@ import {
sendAnalytics
} from '../../react/features/analytics';
import { toggleDialog } from '../../react/features/base/dialog';
import { clickOnVideo } from '../../react/features/filmstrip/actions';
import { KeyboardShortcutsDialog }
from '../../react/features/keyboard-shortcuts';
import { SpeakerStats } from '../../react/features/speaker-stats';
@@ -54,7 +55,7 @@ const KeyboardShortcut = {
if (_shortcuts.has(key)) {
_shortcuts.get(key).function(e);
} else if (!isNaN(num) && num >= 0 && num <= 9) {
APP.UI.clickOnVideo(num);
APP.store.dispatch(clickOnVideo(num));
}
}

19
package-lock.json generated
View File

@@ -10168,11 +10168,6 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"jquery-contextmenu": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/jquery-contextmenu/-/jquery-contextmenu-2.4.5.tgz",
"integrity": "sha1-5lrOBg2M2tTQ5d94FdDXA55RdFA="
},
"jquery-i18next": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz",
@@ -10343,8 +10338,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#77978f0e621f463ed606fe94dda25ff18f1d5f64",
"from": "github:jitsi/lib-jitsi-meet#77978f0e621f463ed606fe94dda25ff18f1d5f64",
"version": "github:jitsi/lib-jitsi-meet#beaff3dd02e9e99da2f61c2a9ed5ca3a34d51dcf",
"from": "github:jitsi/lib-jitsi-meet#beaff3dd02e9e99da2f61c2a9ed5ca3a34d51dcf",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",
@@ -15491,11 +15486,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 +17061,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

@@ -52,11 +52,10 @@
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
"jquery": "3.5.1",
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#77978f0e621f463ed606fe94dda25ff18f1d5f64",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#beaff3dd02e9e99da2f61c2a9ed5ca3a34d51dcf",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -95,10 +94,10 @@
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rtcstats": "github:jitsi/rtcstats#v6.2.0",
"stackblur-canvas": "2.3.0",
"styled-components": "3.4.9",
"util": "0.12.1",
"uuid": "3.1.0",
"wasm-check": "2.0.1",
"windows-iana": "^3.1.0",
"xmldom": "0.1.27",
"zxcvbn": "4.4.2"

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -11,7 +11,7 @@ import { Video } from './_';
/**
* The type of the React {@code Component} props of {@link AbstractVideoTrack}.
*/
export type Props = {
export type Props = {|
/**
* The Redux dispatch function.
@@ -46,7 +46,7 @@ export type Props = {
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
*/
zoomEnabled?: boolean
};
|};
/**
* Implements a React {@link Component} that renders video element for a

View File

@@ -5,7 +5,7 @@ import React, { Component } from 'react';
/**
* The type of the React {@code Component} props of {@link Video}.
*/
type Props = {
type Props = {|
/**
* CSS classes to add to the video element.
@@ -38,8 +38,98 @@ type Props = {
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
playsinline: boolean
};
playsinline: boolean,
/**
* onAbort event handler.
*/
onAbort?: ?Function,
/**
* onCanPlay event handler.
*/
onCanPlay?: ?Function,
/**
* onCanPlayThrough event handler.
*/
onCanPlayThrough?: ?Function,
/**
* onEmptied event handler.
*/
onEmptied?: ?Function,
/**
* onEnded event handler.
*/
onEnded?: ?Function,
/**
* onError event handler.
*/
onError?: ?Function,
/**
* onLoadedData event handler.
*/
onLoadedData?: ?Function,
/**
* onLoadedMetadata event handler.
*/
onLoadedMetadata?: ?Function,
/**
* onLoadStart event handler.
*/
onLoadStart?: ?Function,
/**
* onPause event handler.
*/
onPause?: ?Function,
/**
* onPlay event handler.
*/
onPlay?: ?Function,
/**
* onPlaying event handler.
*/
onPlaying?: ?Function,
/**
* Not used on web.
*/
onPress?: Function,
/**
* onRateChange event handler.
*/
onRateChange?: ?Function,
/**
* onStalled event handler.
*/
onStalled?: ?Function,
/**
* onSuspend event handler.
*/
onSuspend?: ?Function,
/**
* onWaiting event handler.
*/
onWaiting?: ?Function,
/**
* A styles that will be applied on the video element.
*/
style?: Object
|};
/**
* Component that renders a video element for a passed in video track.
@@ -139,6 +229,10 @@ class Video extends Component<Props> {
this._attachTrack(nextProps.videoTrack);
}
if (this.props.style !== nextProps.style || this.props.className !== nextProps.className) {
return true;
}
return false;
}
@@ -149,13 +243,28 @@ class Video extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const {
autoPlay,
className,
id,
// eslint-disable-next-line no-unused-vars
onVideoPlaying,
// eslint-disable-next-line no-unused-vars
videoTrack,
playsinline,
style,
...eventHandlers
} = this.props;
return (
<video
autoPlay = { this.props.autoPlay }
className = { this.props.className }
id = { this.props.id }
playsInline = { this.props.playsinline }
ref = { this._setVideoElement } />
autoPlay = { autoPlay }
className = { className }
id = { id }
playsInline = { playsinline }
ref = { this._setVideoElement }
style = { style }
{ ...eventHandlers } />
);
}

View File

@@ -11,7 +11,7 @@ import Video from './Video';
/**
* The type of the React {@code Component} props of {@link VideoTrack}.
*/
type Props = AbstractVideoTrackProps & {
type Props = AbstractVideoTrackProps & {|
/**
* CSS classes to add to the video element.
@@ -29,8 +29,93 @@ type Props = AbstractVideoTrackProps & {
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
_noAutoPlayVideo: boolean
};
_noAutoPlayVideo: boolean,
/**
* onAbort event handler.
*/
onAbort?: ?Function,
/**
* onCanPlay event handler.
*/
onCanPlay?: ?Function,
/**
* onCanPlayThrough event handler.
*/
onCanPlayThrough?: ?Function,
/**
* onEmptied event handler.
*/
onEmptied?: ?Function,
/**
* onEnded event handler.
*/
onEnded?: ?Function,
/**
* onError event handler.
*/
onError?: ?Function,
/**
* onLoadedData event handler.
*/
onLoadedData?: ?Function,
/**
* onLoadedMetadata event handler.
*/
onLoadedMetadata?: ?Function,
/**
* onLoadStart event handler.
*/
onLoadStart?: ?Function,
/**
* onPause event handler.
*/
onPause?: ?Function,
/**
* onPlay event handler.
*/
onPlay?: ?Function,
/**
* onPlaying event handler.
*/
onPlaying?: ?Function,
/**
* onRateChange event handler.
*/
onRateChange?: ?Function,
/**
* onStalled event handler.
*/
onStalled?: ?Function,
/**
* onSuspend event handler.
*/
onSuspend?: ?Function,
/**
* onWaiting event handler.
*/
onWaiting?: ?Function,
/**
* A styles that will be applied on the video element.
*/
style: Object
|};
/**
* Component that renders a video element for a passed in video track and
@@ -57,13 +142,30 @@ class VideoTrack extends AbstractVideoTrack<Props> {
* @returns {ReactElement}
*/
render() {
const {
_noAutoPlayVideo,
className,
// eslint-disable-next-line no-unused-vars
dispatch,
id,
videoTrack,
style,
waitForVideoStarted, // eslint-disable-line no-unused-vars
zOrder, // eslint-disable-line no-unused-vars
zoomEnabled, // eslint-disable-line no-unused-vars
...eventHandlers
} = this.props;
return (
<Video
autoPlay = { !this.props._noAutoPlayVideo }
className = { this.props.className }
id = { this.props.id }
autoPlay = { !_noAutoPlayVideo }
className = { className }
id = { id }
onVideoPlaying = { this._onVideoPlaying }
videoTrack = { this.props.videoTrack } />
style = { style }
videoTrack = { videoTrack }
{ ...eventHandlers } />
);
}

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

@@ -1,6 +1,7 @@
// @flow
import _ from 'lodash';
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import { PREJOIN_INITIALIZED } from '../../prejoin/actionTypes';
import { APP_WILL_MOUNT } from '../app';
import { setAudioOnly } from '../audio-only';
@@ -14,6 +15,7 @@ import { SETTINGS_UPDATED } from './actionTypes';
import { updateSettings } from './actions';
import { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
/**
* The middleware of the feature base/settings. Distributes changes to the state
* of base/settings to the states of other features computed from the state of
@@ -36,6 +38,7 @@ MiddlewareRegistry.register(store => next => action => {
case SETTINGS_UPDATED:
_maybeHandleCallIntegrationChange(action);
_maybeSetAudioOnly(store, action);
_maybeSetLocalFlipX(store, action);
_updateLocalParticipant(store, action);
_maybeCrashReportingChange(action);
break;
@@ -121,6 +124,21 @@ function _maybeSetAudioOnly(
}
}
/**
* Updates {@code localFlipX} flag for {@code VideoLayout} if it's updated in the settings.
*
* @param {Store} store - The redux store.
* @param {Object} action - The redux action.
* @private
* @returns {void}
*/
function _maybeSetLocalFlipX(store, { settings: { localFlipX } }) {
if (typeof localFlipX === 'boolean') {
// TODO: This needs to be removed once the large video is Reactified.
VideoLayout.onLocalFlipXChanged();
}
}
/**
* Updates the display name to the one in JWT if there is one.
*

View File

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

View File

@@ -1,3 +1,4 @@
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import {
createTrackMutedEvent,
sendAnalytics
@@ -398,8 +399,10 @@ export function trackAdded(track) {
noDataFromSourceNotificationInfo = { timeout };
}
}
track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => VideoLayout._updateLargeVideoIfDisplayed(participantId));
} else {
participantId = track.getParticipantId();
isReceivingData = true;

View File

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

View File

@@ -163,7 +163,6 @@ MiddlewareRegistry.register(store => next => action => {
} else {
APP.UI.setVideoMuted(participantID);
}
APP.UI.onPeerVideoTypeChanged(participantID, jitsiTrack.videoType);
} else if (jitsiTrack.isLocal()) {
APP.conference.setAudioMuteStatus(muted);
} else {

View File

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

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

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

View File

@@ -145,6 +145,18 @@ type Props = {
transport: Array<Object>
};
/**
* Click handler.
*
* @param {SyntheticEvent} event - The click event.
* @returns {void}
*/
function onClick(event) {
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
// needs to be stopped.
event.stopPropagation();
}
/**
* React {@code Component} for displaying connection statistics.
*
@@ -161,7 +173,9 @@ class ConnectionStatsTable extends Component<Props> {
const { isLocalVideo, enableSaveLogs } = this.props;
return (
<div className = 'connection-info'>
<div
className = 'connection-info'
onClick = { onClick }>
{ this._renderStatistics() }
<div className = 'connection-actions'>
{ isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './functions';

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
// @flow
import { pinParticipant } from '../base/participants';
import { toState } from '../base/redux';
import { CHAT_SIZE } from '../chat/constants';
@@ -69,4 +70,20 @@ export function setHorizontalViewDimensions(clientHeight: number = 0) {
};
}
/**
* Emulates a click on the n-th video.
*
* @param {number} n - Number that identifies the video.
* @returns {Function}
*/
export function clickOnVideo(n: number) {
return (dispatch: Function, getState: Function) => {
const participants = getState()['features/base/participants'];
const nThParticipant = participants[n];
const { id, pinned } = nThParticipant;
dispatch(pinParticipant(pinned ? null : id));
};
}
export * from './actions.native';

View File

@@ -20,9 +20,9 @@ import { StyleType } from '../../../base/styles';
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import { DisplayNameLabel } from '../../../display-name';
import { RemoteVideoMenu } from '../../../remote-video-menu';
import ConnectionStatusComponent from '../../../remote-video-menu/components/native/ConnectionStatusComponent';
import { toggleToolboxVisible } from '../../../toolbox/actions.native';
import { RemoteVideoMenu } from '../../../video-menu';
import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
import AudioMutedIndicator from './AudioMutedIndicator';
import DominantSpeakerIndicator from './DominantSpeakerIndicator';

View File

@@ -10,12 +10,15 @@ import {
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { isButtonEnabled } from '../../../toolbox/functions.web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions';
import Thumbnail from './Thumbnail';
declare var APP: Object;
declare var interfaceConfig: Object;
@@ -54,6 +57,11 @@ type Props = {
*/
_hideToolbar: boolean,
/**
* The participants in the call.
*/
_participants: Array<Object>,
/**
* The number of rows in tile view.
*/
@@ -132,18 +140,15 @@ class Filmstrip extends Component <Props> {
* @returns {ReactElement}
*/
render() {
// Note: Appending of {@code RemoteVideo} views is handled through
// VideoLayout. The views do not get blown away on render() because
// ReactDOMComponent is only aware of the given JSX and not new appended
// DOM. As such, when updateDOMProperties gets called, only attributes
// will get updated without replacing the DOM. If the known DOM gets
// modified, then the views will get blown away.
const filmstripStyle = { };
const filmstripRemoteVideosContainerStyle = {};
let remoteVideoContainerClassName = 'remote-videos-container';
const { _currentLayout, _participants } = this.props;
const remoteParticipants = _participants.filter(p => !p.local);
const localParticipant = getLocalParticipant(_participants);
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
switch (this.props._currentLayout) {
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
// Adding 18px for the 2px margins, 2px borders on the left and right and 5px padding on the left and right.
// Also adding 7px for the scrollbar.
@@ -185,7 +190,13 @@ class Filmstrip extends Component <Props> {
<div
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
<div id = 'filmstripLocalVideoThumbnail' />
<div id = 'filmstripLocalVideoThumbnail'>
{
!tileViewActive && <Thumbnail
key = 'local'
participantID = { localParticipant.id } />
}
</div>
</div>
<div
className = { remoteVideosWrapperClassName }
@@ -199,7 +210,21 @@ class Filmstrip extends Component <Props> {
className = { remoteVideoContainerClassName }
id = 'filmstripRemoteVideosContainer'
style = { filmstripRemoteVideosContainerStyle }>
<div id = 'localVideoTileViewContainer' />
{
remoteParticipants.map(
p => (
<Thumbnail
key = { `remote_${p.id}` }
participantID = { p.id } />
))
}
<div id = 'localVideoTileViewContainer'>
{
tileViewActive && <Thumbnail
key = 'local'
participantID = { localParticipant.id } />
}
</div>
</div>
</div>
</div>
@@ -306,6 +331,7 @@ function _mapStateToProps(state) {
_filmstripWidth: filmstripWidth,
_hideScrollbar: Boolean(iAmSipGateway),
_hideToolbar: Boolean(iAmSipGateway),
_participants: state['features/base/participants'],
_rows: gridDimensions.rows,
_videosClassName: videosClassName,
_visible: visible

View File

@@ -1,8 +1,8 @@
// @flow
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React, { Component } from 'react';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
import { AudioLevelIndicator } from '../../../audio-level-indicator';
import { Avatar } from '../../../base/avatar';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
@@ -11,42 +11,73 @@ import AudioTrack from '../../../base/media/components/web/AudioTrack';
import {
getLocalParticipant,
getParticipantById,
getParticipantCount
getParticipantCount,
pinParticipant
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import { isTestModeEnabled } from '../../../base/testing';
import {
getLocalAudioTrack,
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
updateLastTrackVideoMediaEvent
} from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import { DisplayName } from '../../../display-name';
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
import { PresenceLabel } from '../../../presence-status';
import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_MODE_TO_STRING,
DISPLAY_VIDEO,
DISPLAY_VIDEO_WITH_NAME,
VIDEO_TEST_EVENTS
} from '../../constants';
import { isVideoPlayable, computeDisplayMode } from '../../functions';
import { getDisplayModeInput } from '../../functions.web';
import logger from '../../logger';
const JitsiTrackEvents = JitsiMeetJS.events.track;
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} state of {@link Thumbnail}.
*/
type State = {
export type State = {|
/**
* The current audio level value for the Thumbnail.
*/
audioLevel: number,
/**
* Indicates that the canplay event has been received.
*/
canPlayEventReceived: boolean,
/**
* The current display mode of the thumbnail.
*/
displayMode: number,
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: boolean,
/**
* The current volume setting for the Thumbnail.
*/
volume: ?number
};
|};
/**
* The type of the React {@code Component} props of {@link Thumbnail}.
*/
type Props = {
export type Props = {|
/**
* The audio track related to the participant.
@@ -73,11 +104,21 @@ type Props = {
*/
_defaultLocalDisplayName: string,
/**
* Indicates whether the local video flip feature is disabled or not.
*/
_disableLocalVideoFlip: boolean,
/**
* Indicates whether the profile functionality is disabled.
*/
_disableProfile: boolean,
/**
* The display mode of the thumbnail.
*/
_displayMode: number,
/**
* The height of the Thumbnail.
*/
@@ -88,16 +129,51 @@ type Props = {
*/
_heightToWidthPercent: number,
/**
* Indicates whether the thumbnail should be hidden or not.
*/
_isHidden: boolean,
/**
* Indicates whether audio only mode is enabled.
*/
_isAudioOnly: boolean,
/**
* Indicates whether the participant associated with the thumbnail is displayed on the large video.
*/
_isCurrentlyOnLargeVideo: boolean,
/**
* Indicates whether the participant is screen sharing.
*/
_isScreenSharing: boolean,
/**
* Indicates whether the video associated with the thumbnail is playable.
*/
_isVideoPlayable: boolean,
/**
* Disable/enable the dominant speaker indicator.
*/
_isDominantSpeakerDisabled: boolean,
/**
* Indicates whether testing mode is enabled.
*/
_isTestModeEnabled: boolean,
/**
* The size of the icon of indicators.
*/
_indicatorIconSize: number,
/**
* The current local video flip setting.
*/
_localFlipX: boolean,
/**
* An object with information about the participant related to the thumbnaul.
*/
@@ -128,16 +204,23 @@ type Props = {
*/
dispatch: Function,
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: ?boolean,
/**
* The ID of the participant related to the thumbnail.
*/
participantID: ?string
};
|};
/**
* Click handler.
*
* @param {SyntheticEvent} event - The click event.
* @returns {void}
*/
function onClick(event) {
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
// needs to be stopped.
event.stopPropagation();
}
/**
* Implements a thumbnail.
@@ -146,6 +229,8 @@ type Props = {
*/
class Thumbnail extends Component<Props, State> {
_onCanPlay: () => void;
/**
* Initializes a new Thumbnail instance.
*
@@ -155,14 +240,26 @@ class Thumbnail extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
const state = {
audioLevel: 0,
volume: undefined
canPlayEventReceived: false,
isHovered: false,
volume: undefined,
displayMode: DISPLAY_VIDEO
};
this.state = {
...state,
displayMode: computeDisplayMode(getDisplayModeInput(props, state))
};
this._updateAudioLevel = this._updateAudioLevel.bind(this);
this._onCanPlay = this._onCanPlay.bind(this);
this._onClick = this._onClick.bind(this);
this._onVolumeChange = this._onVolumeChange.bind(this);
this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
this._onMouseEnter = this._onMouseEnter.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
}
/**
@@ -173,6 +270,7 @@ class Thumbnail extends Component<Props, State> {
*/
componentDidMount() {
this._listenForAudioUpdates();
this._onDisplayModeChanged();
}
/**
@@ -182,14 +280,86 @@ class Thumbnail extends Component<Props, State> {
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps: Props) {
componentDidUpdate(prevProps: Props, prevState: State) {
if (prevProps._audioTrack !== this.props._audioTrack) {
this._stopListeningForAudioUpdates(prevProps._audioTrack);
this._listenForAudioUpdates();
this._updateAudioLevel(0);
}
if (prevState.displayMode !== this.state.displayMode) {
this._onDisplayModeChanged();
}
}
/**
* Handles display mode changes.
*
* @returns {void}
*/
_onDisplayModeChanged() {
const input = getDisplayModeInput(this.props, this.state);
const displayModeString = DISPLAY_MODE_TO_STRING[this.state.displayMode];
const id = this.props._participant?.id;
this._maybeSendScreenSharingIssueEvents(input);
logger.debug(`Displaying ${displayModeString} for ${id}, data: [${JSON.stringify(input)}]`);
}
/**
* Sends screen sharing issue event if an issue is detected.
*
* @param {Object} input - The input used to compute the thumbnail display mode.
* @returns {void}
*/
_maybeSendScreenSharingIssueEvents(input) {
const {
_currentLayout,
_isAudioOnly,
_isScreenSharing
} = this.props;
const { displayMode } = this.state;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
if (displayMode !== DISPLAY_VIDEO
&& displayMode !== DISPLAY_VIDEO_WITH_NAME
&& tileViewActive
&& _isScreenSharing
&& !_isAudioOnly) {
sendAnalytics(createScreenSharingIssueEvent({
source: 'thumbnail',
...input
}));
}
}
/**
* Implements React's {@link Component#getDerivedStateFromProps()}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: Props, prevState: State) {
if (!props._videoTrack && prevState.canPlayEventReceived) {
return {
...prevState,
canPlayEventReceived: false
};
}
const newDisplayMode = computeDisplayMode(getDisplayModeInput(props, prevState));
if (newDisplayMode !== prevState.displayMode) {
return {
...prevState,
displayMode: newDisplayMode
};
}
return null;
}
/**
* Unsubscribe from audio level updates.
*
@@ -253,8 +423,17 @@ class Thumbnail extends Component<Props, State> {
* @returns {Object} - The styles for the thumbnail.
*/
_getStyles(): Object {
const { _height, _heightToWidthPercent, _currentLayout } = this.props;
let styles;
const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
const userAgent = window.navigator.userAgent;
const isOverflowHidden = userAgent.indexOf('QtWebEngine') > -1
&& (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1);
let styles: {
thumbnail: Object,
avatar: Object
} = {
thumbnail: {},
avatar: {}
};
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
@@ -262,7 +441,13 @@ class Thumbnail extends Component<Props, State> {
const avatarSize = _height / 2;
styles = {
avatarContainer: {
thumbnail: {
height: `${_height}px`,
minHeight: `${_height}px`,
minWidth: `${_width}px`,
width: `${_width}px`
},
avatar: {
height: `${avatarSize}px`,
width: `${avatarSize}px`
}
@@ -271,7 +456,10 @@ class Thumbnail extends Component<Props, State> {
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
styles = {
avatarContainer: {
thumbnail: {
paddingTop: `${_heightToWidthPercent}%`
},
avatar: {
height: '50%',
width: `${_heightToWidthPercent / 2}%`
}
@@ -280,9 +468,53 @@ class Thumbnail extends Component<Props, State> {
}
}
if (_isHidden) {
styles.thumbnail.display = 'none';
}
if (isOverflowHidden) {
styles.thumbnail.overflow = 'hidden';
}
return styles;
}
_onClick: () => void;
/**
* On click handler.
*
* @returns {void}
*/
_onClick() {
const { _participant, dispatch } = this.props;
const { id, pinned } = _participant;
dispatch(pinParticipant(pinned ? null : id));
}
_onMouseEnter: () => void;
/**
* Mouse enter handler.
*
* @returns {void}
*/
_onMouseEnter() {
this.setState({ isHovered: true });
}
_onMouseLeave: () => void;
/**
* Mouse leave handler.
*
* @returns {void}
*/
_onMouseLeave() {
this.setState({ isHovered: false });
}
/**
* Renders a fake participant (youtube video) thumbnail.
*
@@ -292,9 +524,17 @@ class Thumbnail extends Component<Props, State> {
_renderFakeParticipant() {
const { _participant } = this.props;
const { id } = _participant;
const styles = this._getStyles();
const containerClassName = this._getContainerClassName();
return (
<>
<span
className = { containerClassName }
id = 'sharedVideoContainer'
onClick = { this._onClick }
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
style = { styles.thumbnail }>
<img
className = 'sharedVideoAvatar'
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
@@ -303,7 +543,7 @@ class Thumbnail extends Component<Props, State> {
elementID = 'sharedVideoContainer_name'
participantID = { id } />
</div>
</>
</span>
);
}
@@ -320,9 +560,9 @@ class Thumbnail extends Component<Props, State> {
_isDominantSpeakerDisabled,
_indicatorIconSize: iconSize,
_participant,
_participantCount,
isHovered
_participantCount
} = this.props;
const { isHovered } = this.state;
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
const { id, local = false, dominantSpeaker = false } = _participant;
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
@@ -344,43 +584,41 @@ class Thumbnail extends Component<Props, State> {
return (
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ !_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { true }
iconSize = { iconSize }
isLocalVideo = { local }
participantId = { id }
statsPopoverPosition = { statsPopoverPosition } />
}
<RaisedHandIndicator
{ !_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { true }
iconSize = { iconSize }
isLocalVideo = { local }
participantId = { id }
statsPopoverPosition = { statsPopoverPosition } />
}
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
&& <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
&& <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
}
</AtlasKitThemeProvider>
}
</div>);
}
/**
* Renders the avatar.
*
* @param {Object} styles - The styles that will be applied to the avatar.
* @returns {ReactElement}
*/
_renderAvatar() {
_renderAvatar(styles) {
const { _participant } = this.props;
const { id } = _participant;
const styles = this._getStyles();
return (
<div
className = 'avatar-container'
style = { styles.avatarContainer }>
style = { styles }>
<Avatar
className = 'userAvatar'
participantId = { id } />
@@ -388,6 +626,38 @@ class Thumbnail extends Component<Props, State> {
);
}
/**
* Returns the container class name.
*
* @returns {string} - The class name that will be used for the container.
*/
_getContainerClassName() {
let className = 'videocontainer';
const { displayMode } = this.state;
const { _isAudioOnly, _isDominantSpeakerDisabled, _isHidden, _participant } = this.props;
const isRemoteParticipant = !_participant?.local && !_participant?.isFakeParticipant;
className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
if (_participant?.pinned) {
className += ' videoContainerFocused';
}
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
className += ' active-speaker';
}
if (_isHidden) {
className += ' hidden';
}
if (isRemoteParticipant && _isAudioOnly) {
className += ' audio-only';
}
return className;
}
/**
* Renders the local participant's thumbnail.
*
@@ -396,19 +666,33 @@ class Thumbnail extends Component<Props, State> {
_renderLocalParticipant() {
const {
_defaultLocalDisplayName,
_disableLocalVideoFlip,
_isScreenSharing,
_localFlipX,
_disableProfile,
_participant,
_videoTrack
} = this.props;
const { id } = _participant || {};
const { audioLevel } = this.state;
const styles = this._getStyles();
const containerClassName = this._getContainerClassName();
const videoTrackClassName
= !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
return (
<>
<span
className = { containerClassName }
id = 'localVideoContainer'
onClick = { this._onClick }
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
style = { styles.thumbnail }>
<div className = 'videocontainer__background' />
<span id = 'localVideoWrapper'>
<VideoTrack
className = { videoTrackClassName }
id = 'localVideo_container'
videoTrack = { _videoTrack } />
</span>
@@ -419,21 +703,60 @@ class Thumbnail extends Component<Props, State> {
{ this._renderTopIndicators() }
</div>
<div className = 'videocontainer__hoverOverlay' />
<div className = 'displayNameContainer'>
<div
className = 'displayNameContainer'
onClick = { onClick }>
<DisplayName
allowEditing = { !_disableProfile }
displayNameSuffix = { _defaultLocalDisplayName }
elementID = 'localDisplayName'
participantID = { id } />
</div>
{ this._renderAvatar() }
{ this._renderAvatar(styles.avatar) }
<span className = 'localvideomenu'>
<LocalVideoMenuTriggerButton />
</span>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</>
</span>
);
}
/**
* Canplay event listener.
*
* @param {SyntheticEvent} event - The event.
* @returns {void}
*/
_onCanPlay(event) {
this.setState({ canPlayEventReceived: true });
const {
_isTestModeEnabled,
_videoTrack
} = this.props;
if (_videoTrack && _isTestModeEnabled) {
this._onTestingEvent(event);
}
}
/**
* Event handler for testing events.
*
* @param {SyntheticEvent} event - The event.
* @returns {void}
*/
_onTestingEvent(event) {
const {
_videoTrack,
dispatch
} = this.props;
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
}
/**
* Renders a remote participant's 'thumbnail.
@@ -443,29 +766,58 @@ class Thumbnail extends Component<Props, State> {
_renderRemoteParticipant() {
const {
_audioTrack,
_isTestModeEnabled,
_participant,
_startSilent
_startSilent,
_videoTrack
} = this.props;
const { id } = _participant;
const { audioLevel, volume } = this.state;
const { audioLevel, canPlayEventReceived, volume } = this.state;
const styles = this._getStyles();
const containerClassName = this._getContainerClassName();
// hide volume when in silent mode
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
const jitsiTrack = _audioTrack?.jitsiTrack;
const audioTrackId = jitsiTrack && jitsiTrack.getId();
const jitsiAudioTrack = _audioTrack?.jitsiTrack;
const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
const videoEventListeners = {};
if (_videoTrack && _isTestModeEnabled) {
Object.keys(VIDEO_TEST_EVENTS).forEach(attribute => {
videoEventListeners[attribute] = this._onTestingEvent;
});
}
videoEventListeners.onCanPlay = this._onCanPlay;
const videoElementStyle = canPlayEventReceived ? null : {
display: 'none'
};
return (
<>
<span
className = { containerClassName }
id = { `participant_${id}` }
onClick = { this._onClick }
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
style = { styles.thumbnail }>
{
_audioTrack
? <AudioTrack
audioTrack = { _audioTrack }
id = { `remoteAudio_${audioTrackId || ''}` }
muted = { _startSilent }
onInitialVolumeSet = { this._onInitialVolumeSet }
volume = { this.state.volume } />
: null
_videoTrack && <VideoTrack
id = { `remoteVideo_${videoTrackId || ''}` }
style = { videoElementStyle }
videoTrack = { _videoTrack }
{ ...videoEventListeners } />
}
{
_audioTrack && <AudioTrack
audioTrack = { _audioTrack }
id = { `remoteAudio_${audioTrackId || ''}` }
muted = { _startSilent }
onInitialVolumeSet = { this._onInitialVolumeSet }
volume = { volume } />
}
<div className = 'videocontainer__background' />
<div className = 'videocontainer__toptoolbar'>
@@ -480,24 +832,22 @@ class Thumbnail extends Component<Props, State> {
elementID = { `participant_${id}_name` }
participantID = { id } />
</div>
{ this._renderAvatar() }
{ this._renderAvatar(styles.avatar) }
<div className = 'presence-label-container'>
<PresenceLabel
className = 'presence-label'
participantID = { id } />
</div>
<span className = 'remotevideomenu'>
<AtlasKitThemeProvider mode = 'dark'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { volume }
onVolumeChange = { onVolumeChange }
participantID = { id } />
</AtlasKitThemeProvider>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { volume }
onVolumeChange = { onVolumeChange }
participantID = { id } />
</span>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</>
</span>
);
}
@@ -527,7 +877,6 @@ class Thumbnail extends Component<Props, State> {
this.setState({ volume: value });
}
/**
* Implements React's {@link Component#render()}.
*
@@ -568,17 +917,24 @@ function _mapStateToProps(state, ownProps): Object {
// Only the local participant won't have id for the time when the conference is not yet joined.
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
const { id } = participant;
const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks'];
const _videoTrack = isLocal
? getLocalVideoTrack(state['features/base/tracks'])
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const _audioTrack = isLocal
? getLocalAudioTrack(state['features/base/tracks'])
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = getCurrentLayout(state);
let size = {};
const { startSilent, disableProfile = false } = state['features/base/config'];
const {
startSilent,
disableLocalVideoFlip,
disableProfile = false,
iAmRecorder,
iAmSipGateway
} = state['features/base/config'];
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
const { localFlipX } = state['features/base/settings'];
switch (_currentLayout) {
@@ -617,16 +973,23 @@ function _mapStateToProps(state, ownProps): Object {
}
}
return {
_audioTrack,
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
_currentLayout,
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
_disableProfile: disableProfile,
_isHidden: isLocal && iAmRecorder && !iAmSipGateway,
_isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
_isScreenSharing: _videoTrack?.videoType === 'desktop',
_isTestModeEnabled: isTestModeEnabled(state),
_isVideoPlayable: isVideoPlayable(state, id),
_indicatorIconSize: NORMAL,
_localFlipX: Boolean(localFlipX),
_participant: participant,
_participantCount: getParticipantCount(state),
_startSilent: Boolean(startSilent),

View File

@@ -47,3 +47,83 @@ export const DEFAULT_MAX_COLUMNS = 5;
* An extended number of columns for tile view.
*/
export const ABSOLUTE_MAX_COLUMNS = 7;
/**
* Map of attributes of the video element to the corresponding video event that we are going to process.
* Will be adding a listener for every event in the list. The latest event will be stored in redux. This is
* currently used by torture only.
*/
export const VIDEO_TEST_EVENTS = {
'onAbort': 'abort',
'onCanPlay': 'canplay',
'onCanPlayThrough': 'canplaytrough',
'onEmptied': 'emptied',
'onEnded': 'ended',
'onError': 'error',
'onLoadedData': 'loadeddata',
'onLoadedMetadata': 'loadedmetadata',
'onLoadStart': 'loadstart',
'onPause': 'pause',
'onPlay': 'play',
'onPlaying': 'playing',
'onRateChange': 'ratechange',
'onStalled': 'stalled',
'onSuspend': 'suspend',
'onWaiting': 'waiting'
};
/**
* Display mode constant used when video is being displayed on the small video.
* @type {number}
* @constant
*/
export const DISPLAY_VIDEO = 0;
/**
* Display mode constant used when the user's avatar is being displayed on
* the small video.
* @type {number}
* @constant
*/
export const DISPLAY_AVATAR = 1;
/**
* Display mode constant used when neither video nor avatar is being displayed
* on the small video. And we just show the display name.
* @type {number}
* @constant
*/
export const DISPLAY_BLACKNESS_WITH_NAME = 2;
/**
* Display mode constant used when video is displayed and display name
* at the same time.
* @type {number}
* @constant
*/
export const DISPLAY_VIDEO_WITH_NAME = 3;
/**
* Display mode constant used when neither video nor avatar is being displayed
* on the small video. And we just show the display name.
* @type {number}
* @constant
*/
export const DISPLAY_AVATAR_WITH_NAME = 4;
/**
* Maps the display modes to class name that will be applied on the thumbnail container.
* @type {Array<string>}
* @constant
*/
export const DISPLAY_MODE_TO_CLASS_NAME = [ 'display-video', 'display-avatar-only', 'display-name-on-black',
'display-name-on-video', 'display-avatar-with-name' ];
/**
* Maps the display modes to string.
* @type {Array<string>}
* @constant
*/
export const DISPLAY_MODE_TO_STRING = [ 'video', 'avatar', 'blackness-with-name',
'video-with-name', 'avatar-with-name' ];

View File

@@ -15,8 +15,19 @@ import {
isLocalTrackMuted,
isRemoteTrackMuted
} from '../base/tracks/functions';
import { LAYOUTS } from '../video-layout';
import { ASPECT_RATIO_BREAKPOINT, SQUARE_TILE_ASPECT_RATIO, TILE_ASPECT_RATIO } from './constants';
import type { Props, State } from './components/web/Thumbnail';
import {
ASPECT_RATIO_BREAKPOINT,
DISPLAY_AVATAR,
DISPLAY_AVATAR_WITH_NAME,
DISPLAY_BLACKNESS_WITH_NAME,
DISPLAY_VIDEO,
DISPLAY_VIDEO_WITH_NAME,
SQUARE_TILE_ASPECT_RATIO,
TILE_ASPECT_RATIO
} from './constants';
declare var interfaceConfig: Object;
@@ -176,3 +187,71 @@ export function getVerticalFilmstripVisibleAreaWidth() {
return Math.min(filmstripMaxWidth, window.innerWidth);
}
/**
* Extracts information for props and state needed to compute the display mode.
*
* @param {Props} props - The component's props.
* @param {State} state - The component's state.
* @returns {Object}
*/
export function getDisplayModeInput(props: Props, state: State) {
const {
_currentLayout,
_isAudioOnly,
_isCurrentlyOnLargeVideo,
_isScreenSharing,
_isVideoPlayable,
_participant,
_videoTrack
} = props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const { canPlayEventReceived, isHovered } = state;
return {
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
isHovered,
isAudioOnly: _isAudioOnly,
tileViewActive,
isVideoPlayable: _isVideoPlayable,
connectionStatus: _participant?.connectionStatus,
canPlayEventReceived,
videoStream: Boolean(_videoTrack),
isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
isScreenSharing: _isScreenSharing,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
/**
* Computes information that determine the display mode.
*
* @param {Object} input - Obejct containing all necessary information for determining the display mode for
* the thumbnail.
* @returns {number} - One of <tt>DISPLAY_VIDEO</tt>, <tt>DISPLAY_AVATAR</tt> or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
*/
export function computeDisplayMode(input: Object) {
const {
isAudioOnly,
isCurrentlyOnLargeVideo,
isScreenSharing,
canPlayEventReceived,
isHovered,
isRemoteParticipant,
tileViewActive
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
// Display name is always and only displayed when user is on the stage
return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
} else if (adjustedIsVideoPlayable && !isAudioOnly) {
// check hovering and change state to video with name
return isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
}
// check hovering and change state to avatar with name
return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
}

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../base/logging/functions';
export default getLogger('features/filmstrip');

View File

@@ -1,15 +1,12 @@
// @flow
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
import { MiddlewareRegistry } from '../base/redux';
import { CLIENT_RESIZED } from '../base/responsive-ui';
import {
getCurrentLayout,
LAYOUTS,
shouldDisplayTileView
LAYOUTS
} from '../video-layout';
import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
import './subscriber.web';
@@ -48,29 +45,6 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
}
case SET_TILE_VIEW_DIMENSIONS: {
const state = store.getState();
if (shouldDisplayTileView(state)) {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
// Once the thumbnails are reactified this should be moved there too.
Filmstrip.resizeThumbnailsForTileView(width, height, true);
}
break;
}
case SET_HORIZONTAL_VIEW_DIMENSIONS: {
const state = store.getState();
if (getCurrentLayout(state) === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
const { horizontalViewDimensions = {} } = state['features/filmstrip'];
// Once the thumbnails are reactified this should be moved there too.
Filmstrip.resizeThumbnailsForHorizontalView(horizontalViewDimensions, true);
}
break;
}
}
return result;

View File

@@ -1,7 +1,5 @@
// @flow
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { StateListenerRegistry, equals } from '../base/redux';
import { setFilmstripVisible } from '../filmstrip/actions';
import { setOverflowDrawer } from '../toolbox/actions.web';
@@ -71,32 +69,9 @@ StateListenerRegistry.register(
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
// Once the thumbnails are reactified this should be moved there too.
Filmstrip.resizeThumbnailsForVerticalView();
break;
}
});
/**
* Handles on stage participant updates.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/large-video'].participantId,
/* listener */ (participantId, store, oldParticipantId) => {
const newThumbnail = VideoLayout.getSmallVideo(participantId);
const oldThumbnail = VideoLayout.getSmallVideo(oldParticipantId);
if (newThumbnail) {
newThumbnail.updateView();
}
if (oldThumbnail) {
oldThumbnail.updateView();
}
}
);
/**
* Listens for changes in the chat state to calculate the dimensions of the tile view grid and the tiles.
*/

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,16 +27,30 @@ 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 { muteLocal } from '../../remote-video-menu/actions';
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
import { openChat } from '../../chat/actions';
import { sendMessage, setPrivateMessageRecipient, closeChat } from '../../chat/actions.any';
import { muteLocal } from '../../video-menu/actions';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
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
});
}
);
}
/**

Some files were not shown because too many files have changed in this diff Show More