Merge pull request #14704 from antobinary/merge-25-develop

chore: Merge 2.5 into develop
This commit is contained in:
Anton Georgiev 2022-03-30 14:21:35 -04:00 committed by GitHub
commit d66c162669
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 8410 additions and 3949 deletions

View File

@ -11,7 +11,7 @@ stages:
# define which docker image to use for builds
default:
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-03-16-bbb-25-jvm-11
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-03-29-bbb-25-meteor-261
# This stage uses git to find out since when each package has been unmodified.
# it then checks an API endpoint on the package server to find out for which of

View File

@ -63,6 +63,8 @@ public class ApiParams {
public static final String WEBCAMS_ONLY_FOR_MODERATOR = "webcamsOnlyForModerator";
public static final String MEETING_CAMERA_CAP = "meetingCameraCap";
public static final String USER_CAMERA_CAP = "userCameraCap";
public static final String MEETING_EXPIRE_IF_NO_USER_JOINED_IN_MINUTES = "meetingExpireIfNoUserJoinedInMinutes";
public static final String MEETING_EXPIRE_WHEN_LAST_USER_LEFT_IN_MINUTES = "meetingExpireWhenLastUserLeftInMinutes";
public static final String WELCOME = "welcome";
public static final String HTML5_INSTANCE_ID = "html5InstanceId";
public static final String ROLE = "role";

View File

@ -412,7 +412,7 @@ public class MeetingService implements MessageListener {
formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(),
m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getMeetingLayout(), m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getModeratorOnlyMessage(),
m.getDialNumber(), m.getMaxUsers(),
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getmeetingExpireWhenLastUserLeftInMinutes(),
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getMeetingExpireWhenLastUserLeftInMinutes(),
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),

View File

@ -118,8 +118,8 @@ public class ParamsProcessorUtil {
private Long maxPresentationFileUpload = 30000000L; // 30MB
private Integer clientLogoutTimerInMinutes = 0;
private Integer meetingExpireIfNoUserJoinedInMinutes = 5;
private Integer meetingExpireWhenLastUserLeftInMinutes = 1;
private Integer defaultMeetingExpireIfNoUserJoinedInMinutes = 5;
private Integer defaultMeetingExpireWhenLastUserLeftInMinutes = 1;
private Integer userInactivityInspectTimerInMinutes = 120;
private Integer userInactivityThresholdInMinutes = 30;
private Integer userActivitySignResponseDelayInMinutes = 5;
@ -560,6 +560,24 @@ public class ParamsProcessorUtil {
}
}
Integer meetingExpireIfNoUserJoinedInMinutes = defaultMeetingExpireIfNoUserJoinedInMinutes;
if (!StringUtils.isEmpty(params.get(ApiParams.MEETING_EXPIRE_IF_NO_USER_JOINED_IN_MINUTES))) {
try {
meetingExpireIfNoUserJoinedInMinutes = Integer.parseInt(params.get(ApiParams.MEETING_EXPIRE_IF_NO_USER_JOINED_IN_MINUTES));
} catch (NumberFormatException e) {
log.warn("Invalid param [meetingExpireIfNoUserJoinedInMinutes] for meeting=[{}]", internalMeetingId);
}
}
Integer meetingExpireWhenLastUserLeftInMinutes = defaultMeetingExpireWhenLastUserLeftInMinutes;
if (!StringUtils.isEmpty(params.get(ApiParams.MEETING_EXPIRE_WHEN_LAST_USER_LEFT_IN_MINUTES))) {
try {
meetingExpireWhenLastUserLeftInMinutes = Integer.parseInt(params.get(ApiParams.MEETING_EXPIRE_WHEN_LAST_USER_LEFT_IN_MINUTES));
} catch (NumberFormatException e) {
log.warn("Invalid param [meetingExpireWhenLastUserLeftInMinutes] for meeting=[{}]", internalMeetingId);
}
}
boolean endWhenNoModerator = defaultEndWhenNoModerator;
if (!StringUtils.isEmpty(params.get(ApiParams.END_WHEN_NO_MODERATOR))) {
try {
@ -1134,15 +1152,11 @@ public class ParamsProcessorUtil {
}
public void setMeetingExpireWhenLastUserLeftInMinutes(Integer value) {
meetingExpireWhenLastUserLeftInMinutes = value;
}
public Integer getmeetingExpireWhenLastUserLeftInMinutes() {
return meetingExpireWhenLastUserLeftInMinutes;
defaultMeetingExpireWhenLastUserLeftInMinutes = value;
}
public void setMeetingExpireIfNoUserJoinedInMinutes(Integer value) {
meetingExpireIfNoUserJoinedInMinutes = value;
defaultMeetingExpireIfNoUserJoinedInMinutes = value;
}
public Integer getUserInactivityInspectTimerInMinutes() {

View File

@ -668,7 +668,7 @@ public class Meeting {
meetingExpireWhenLastUserLeftInMinutes = value;
}
public Integer getmeetingExpireWhenLastUserLeftInMinutes() {
public Integer getMeetingExpireWhenLastUserLeftInMinutes() {
return meetingExpireWhenLastUserLeftInMinutes;
}

View File

@ -1 +1 @@
git clone --branch v3.2.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
git clone --branch v3.4.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.5.0-alpha.5
BIGBLUEBUTTON_RELEASE=2.5.0-alpha.6

View File

@ -716,7 +716,7 @@ check_configuration() {
if [ ! -f $file ]; then
echo "# Error: File not found: $file"
else
if cat $file | grep -v redis.pass | grep -v redisPassword | grep -q "^[^=]*=[ ]*$"; then
if cat $file | grep -v redis.pass | grep -v redisPassword | grep -v ^# | grep -q "^[^=]*=[ ]*$"; then
echo "# The following properties in $file have no value:"
echo "# $(grep '^[^=#]*=[ ]*$' $file | grep -v redis.pass | grep -v redisPassword | sed 's/=//g')"
fi
@ -912,7 +912,7 @@ check_state() {
RUNNING_APPS="${RUNNING_APPS} Nginx"
fi
if ! netstat -ant | grep '8090' > /dev/null; then
if ! ss -ant | grep '8090' > /dev/null; then
print_header
NOT_RUNNING_APPS="${NOT_RUNNING_APPS} ${TOMCAT_USER} or grails"
else
@ -1025,7 +1025,7 @@ check_state() {
# Check if the user is running their own bbb-web
#
if grep -q 8888 /usr/share/bigbluebutton/nginx/web.nginx; then
if ! netstat -ant | grep '8888' > /dev/null; then
if ! ss -ant | grep '8888' > /dev/null; then
echo "# Warning: There is no application server listening to port 8888."
echo
fi
@ -1257,7 +1257,7 @@ check_state() {
echo "#"
fi
FREESWITCH_SIP=$(netstat -anlt | grep :5066 | grep -v tcp6 | grep LISTEN | sed 's/ [ ]*/ /g' | cut -d' ' -f4 | sed 's/:5066//g')
FREESWITCH_SIP=$(ss -anlt4 | grep :5066 | grep -v tcp6 | grep LISTEN | sed 's/ [ ]*/ /g' | cut -d' ' -f4 | sed 's/:5066//g')
KURENTO_SIP=$(echo "$KURENTO_CONFIG" | yq r - freeswitch.sip_ip)
if [ ! -z "$FREESWITCH_SIP" ]; then
@ -1845,11 +1845,11 @@ if [ $CLEAN ]; then
fi
if [ $NETWORK ]; then
netstat -ant | egrep ":80|:443\ " | egrep -v ":::|0.0.0.0" > /tmp/t_net
ss -ant | egrep ":80|:443\ " | egrep -v ":::|0.0.0.0" > /tmp/t_net
REMOTE=$(cat /tmp/t_net | cut -c 45-68 | cut -d ":" -f1 | sort | uniq)
if [ "$REMOTE" != "" ]; then
echo -e "netstat\t\t\t80\t443"
echo -e "ss\t\t\t80\t443"
for IP in $REMOTE ; do
PORT_80=$(cat /tmp/t_net | grep :80 | cut -c 45-68 | cut -d ":" -f1 | grep $IP | wc -l )
PORT_443=$(cat /tmp/t_net | grep :443 | cut -c 45-68 | cut -d ":" -f1 | grep $IP | wc -l )

View File

@ -5,23 +5,21 @@
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.13.0
mongo@1.14.6
reactive-var@1.0.11
standard-minifier-css@1.7.4
standard-minifier-js@2.7.1
standard-minifier-js@2.8.0
es5-shim@4.8.0
ecmascript@0.16.0
ecmascript@0.16.1
shell-server@0.5.0
static-html@1.3.2
react-meteor-data
http@1.4.2
session@1.2.0
tracker@1.2.0
check@1.3.1
rocketchat:streamer
cfs:reactive-list
meteortesting:mocha
lmieulet:meteor-coverage

View File

@ -1 +1 @@
METEOR@2.5
METEOR@2.6.1

View File

@ -1,6 +1,6 @@
allow-deny@1.1.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.7.0
babel-compiler@7.8.1
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
@ -9,16 +9,14 @@ boilerplate-generator@1.7.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
cfs:reactive-list@0.0.9
check@1.3.1
ddp@1.4.0
ddp-client@2.5.0
ddp-common@1.4.0
ddp-server@2.5.0
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.7.2
ecmascript@0.16.0
ecmascript@0.16.1
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
@ -29,35 +27,35 @@ geojson-utils@1.0.10
hot-code-push@1.0.4
html-tools@1.1.2
htmljs@1.1.1
http@1.4.4
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
launch-screen@1.3.0
lmieulet:meteor-coverage@3.2.0
lmieulet:meteor-coverage@4.1.0
logging@1.3.1
meteor@1.10.0
meteor-base@1.5.1
meteortesting:browser-tests@1.3.4
meteortesting:mocha@2.0.2
meteortesting:mocha-core@8.0.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2
minifier-css@1.6.0
minifier-js@2.7.1
minimongo@1.7.0
minifier-js@2.7.3
minimongo@1.8.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.7
modules@0.17.0
modules@0.18.0
modules-runtime@0.12.0
mongo@1.13.0
mongo@1.14.6
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.8
npm-mongo@3.9.1
npm-mongo@4.3.1
ordered-dict@1.1.0
promise@0.12.0
random@1.2.0
react-fast-refresh@0.2.0
react-meteor-data@0.2.16
react-fast-refresh@0.2.2
react-meteor-data@2.4.0
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.1
@ -69,12 +67,12 @@ shell-server@0.5.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
standard-minifier-css@1.7.4
standard-minifier-js@2.7.1
standard-minifier-js@2.8.0
static-html@1.3.2
templating-tools@1.2.1
tmeasday:check-npm-versions@0.3.2
tracker@1.2.0
typescript@4.4.1
underscore@1.0.10
url@1.3.2
webapp@1.13.0
webapp@1.13.1
webapp-hashing@1.1.0

View File

@ -79,7 +79,7 @@ sudo systemctl daemon-reload
echo 'before stopping bbb-html5:'
ps -ef | grep node-
sudo netstat -netlp | grep -i node
sudo ss -netlp | grep -i node
echo 'before stopping bbb-html5:'
echo '_____________'
@ -88,7 +88,7 @@ sudo systemctl stop bbb-html5
sleep 5s
echo 'after stopping bbb-html5:'
ps -ef | grep node-
sudo netstat -netlp | grep -i node
sudo ss -netlp | grep -i node
echo 'after stopping bbb-html5:'
echo '_____________'
@ -97,6 +97,6 @@ sudo systemctl start bbb-html5
sleep 10s
echo 'after:...'
ps -ef | grep node-
sudo netstat -netlp | grep -i node
sudo ss -netlp | grep -i node
echo 'after:'
echo '_____________'

View File

@ -1,4 +1,4 @@
import { HTTP } from 'meteor/http';
import axios from 'axios';
import { check } from 'meteor/check';
import Presentations from '/imports/api/presentations';
import Logger from '/imports/startup/server/logger';
@ -9,7 +9,8 @@ import setCurrentPresentation from './setCurrentPresentation';
const getSlideText = async (url) => {
let content = '';
try {
content = await HTTP.get(url).content;
const request = await axios(url);
content = request.data;
} catch (error) {
Logger.error(`No file found. ${error}`);
}

View File

@ -8,12 +8,16 @@ import ClientConnections from '/imports/startup/server/ClientConnections';
import UsersPersistentData from '/imports/api/users-persistent-data';
import VoiceUsers from '/imports/api/voice-users/';
const clearAllSessions = (sessionUserId) => {
const disconnectUser = (meetingId, userId) => {
const sessionUserId = `${meetingId}--${userId}`;
ClientConnections.removeClientConnection(sessionUserId);
const serverSessions = Meteor.server.sessions;
const interable = serverSessions.values();
for (const session of interable) {
if (session.userId === sessionUserId) {
Logger.info(`Removed session id=${userId} meeting=${meetingId}`);
session.close();
}
}
@ -24,14 +28,14 @@ export default function removeUser(meetingId, userId) {
check(userId, String);
try {
const selector = {
meetingId,
userId,
};
// we don't want to fully process the redis message in frontend
// since the backend is supposed to update Mongo
if ((process.env.BBB_HTML5_ROLE !== 'frontend')) {
const selector = {
meetingId,
userId,
};
setloggedOutStatus(userId, meetingId, true);
VideoStreams.remove({ meetingId, userId });
@ -49,9 +53,19 @@ export default function removeUser(meetingId, userId) {
}
if (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend') {
const sessionUserId = `${meetingId}--${userId}`;
ClientConnections.removeClientConnection(sessionUserId);
clearAllSessions(sessionUserId);
//Wait for user removal and then kill user connections and sessions
const queryCurrentUser = Users.find(selector);
if (queryCurrentUser.count() === 0) {
disconnectUser(meetingId, userId);
} else {
let queryUserObserver = queryCurrentUser.observeChanges({
removed() {
disconnectUser(meetingId, userId);
queryUserObserver.stop();
},
});
}
}
Logger.info(`Removed user id=${userId} meeting=${meetingId}`);

View File

@ -228,7 +228,6 @@ class Base extends Component {
if (approved && loading) this.updateLoadingState(false);
if (prevProps.ejected || ejected) {
console.log(' if (prevProps.ejected || ejected) {');
Session.set('codeError', '403');
Session.set('isMeetingEnded', true);
}

View File

@ -1,7 +1,6 @@
import styled from 'styled-components';
import {
smPaddingX,
smPaddingY,
mdPaddingX,
mdPaddingY,
} from '/imports/ui/stylesheets/styled-components/general';
@ -26,10 +25,9 @@ const MessageListWrapper = styled.div`
overflow-y: auto;
padding-left: ${smPaddingX};
margin-left: calc(-1 * ${mdPaddingX});
padding-right: ${smPaddingY};
padding-right: ${smPaddingX};
margin-right: calc(-1 * ${mdPaddingY});
padding-bottom: ${mdPaddingX};
margin-bottom: calc(-1 * ${mdPaddingX});
z-index: 2;
[dir="rtl"] & {
padding-right: ${mdPaddingX};

View File

@ -117,7 +117,10 @@ class BBBMenu extends React.Component {
<div
onClick={(e) => {
e.persist();
this.opts.autoFocus = !(['mouse', 'touch'].includes(e.nativeEvent.pointerType));
const firefoxInputSource = !([1, 5].includes(e.nativeEvent.mozInputSource)); // 1 = mouse, 5 = touch (firefox only)
const chromeInputSource = !(['mouse', 'touch'].includes(e.nativeEvent.pointerType));
this.opts.autoFocus = firefoxInputSource && chromeInputSource;
this.handleClick(e);
}}
onKeyPress={(e) => {

View File

@ -139,16 +139,18 @@ const generateStateWithNewMessage = (msg, state, msgType = MESSAGE_TYPES.HISTORY
if (groupMessage.sender === stateMessages.lastSender) {
const previousMessage = msg.timestamp <= getLoginTime();
const timeWindowKey = keyName + '-' + stateMessages.chatIndexes[keyName];
const read = previousMessage ? true : !!removedMessagesReadState[groupMessage.id];
messageGroups[timeWindowKey] = {
...groupMessage,
lastTimestamp: msg.timestamp,
read: previousMessage ? true : false,
read,
content: [
...groupMessage.content,
{ id: msg.id, text: msg.message, time: msg.timestamp }
],
};
if (!previousMessage && groupMessage.sender !== Auth.userID) {
if (!read && groupMessage.sender !== Auth.userID) {
stateMessages.unreadTimeWindows.add(timeWindowKey);
}
}

View File

@ -75,10 +75,10 @@ const intlMessages = defineMessages({
id: 'app.lock-viewers.locked',
description: 'locked element label',
},
unlockedLabel: {
id: 'app.lock-viewers.unlocked',
description: 'unlocked element label',
},
hideCursorsLabel: {
id: "app.lock-viewers.hideViewersCursor",
description: 'label for other viewers cursor',
}
});
const propTypes = {
@ -126,12 +126,9 @@ class LockViewersComponent extends Component {
displayLockStatus(status) {
const { intl } = this.props;
return (
<Styled.ToggleLabel>
{status ? intl.formatMessage(intlMessages.lockedLabel)
: intl.formatMessage(intlMessages.unlockedLabel)
}
status && <Styled.ToggleLabel>
{intl.formatMessage(intlMessages.lockedLabel)}
</Styled.ToggleLabel>
);
}
@ -361,6 +358,32 @@ class LockViewersComponent extends Component {
</Styled.FormElementRight>
</Styled.Col>
</Styled.Row>
<Styled.Row>
<Styled.Col aria-hidden="true">
<Styled.FormElement>
<Styled.Label>
{intl.formatMessage(intlMessages.hideCursorsLabel)}
</Styled.Label>
</Styled.FormElement>
</Styled.Col>
<Styled.Col>
<Styled.FormElementRight>
{this.displayLockStatus(lockSettingsProps.hideViewersCursor)}
<Toggle
icons={false}
defaultChecked={lockSettingsProps.hideViewersCursor}
onChange={() => {
this.toggleLockSettings('hideViewersCursor');
}}
ariaLabel={intl.formatMessage(intlMessages.hideCursorsLabel)}
showToggleLabel={showToggleLabel}
invertColors={invertColors}
data-test="hideViewersCursor"
/>
</Styled.FormElementRight>
</Styled.Col>
</Styled.Row>
</Styled.Form>
</Styled.Container>
<Styled.Footer>

View File

@ -24,6 +24,7 @@ const lockContextContainer = (component) => withTracker(() => {
lockSetting.userLocks.userPrivateChat = userIsLocked && lockSettings.disablePrivateChat;
lockSetting.userLocks.userPublicChat = userIsLocked && lockSettings.disablePublicChat;
lockSetting.userLocks.userLockedLayout = userIsLocked && lockSettings.lockedLayout;
lockSetting.userLocks.hideViewersCursor = userIsLocked && lockSettings.hideViewersCursor;
return lockSetting;
})(withLockContext(component));

View File

@ -28,6 +28,10 @@ const intlDisableMessages = defineMessages({
id: 'app.userList.userOptions.hideUserList',
description: 'label to hide user list notification',
},
hideViewersCursor: {
id: 'app.userList.userOptions.hideViewersCursor',
description: 'label to show viewer cursors notification',
},
onlyModeratorWebcam: {
id: 'app.userList.userOptions.webcamsOnlyForModerator',
description: 'label to disable all webcams except for the moderators cam',
@ -59,6 +63,10 @@ const intlEnableMessages = defineMessages({
id: 'app.userList.userOptions.showUserList',
description: 'label to show user list notification',
},
hideViewersCursor: {
id: 'app.userList.userOptions.showViewersCursor',
description: 'label to hide viewer cursors notification',
},
onlyModeratorWebcam: {
id: 'app.userList.userOptions.enableOnlyModeratorWebcam',
description: 'label to enable all webcams except for the moderators cam',
@ -88,11 +96,11 @@ class LockViewersNotifyComponent extends Component {
const rejectedKeys = ['setBy', 'lockedLayout'];
const disabledSettings = Object.keys(lockSettings)
.filter(key => prevLockSettings[key] !== lockSettings[key]
.filter((key) => prevLockSettings[key] !== lockSettings[key]
&& lockSettings[key]
&& !rejectedKeys.includes(key));
const enableSettings = Object.keys(lockSettings)
.filter(key => prevLockSettings[key] !== lockSettings[key]
.filter((key) => prevLockSettings[key] !== lockSettings[key]
&& !lockSettings[key]
&& !rejectedKeys.includes(key));

View File

@ -725,7 +725,6 @@ class Presentation extends PureComponent {
fullscreenElementId,
layoutContextDispatch,
} = this.props;
const { isFullscreen } = this.state;
return (
<PresentationMenu
@ -733,7 +732,6 @@ class Presentation extends PureComponent {
screenshotRef={this.getSvgRef()}
elementName={intl.formatMessage(intlMessages.presentationLabel)}
elementId={fullscreenElementId}
isFullscreen={isFullscreen}
toggleSwapLayout={MediaService.toggleSwapLayout}
layoutContextDispatch={layoutContextDispatch}
/>

View File

@ -1,53 +1,70 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import CursorService from './service';
import Cursor from './component';
import React, { Component, useContext } from "react";
import PropTypes from "prop-types";
import { withTracker } from "meteor/react-meteor-data";
import Auth from "/imports/ui/services/auth";
import lockContextContainer from "/imports/ui/components/lock-viewers/context/container";
import { UsersContext } from "/imports/ui/components/components-data/users-context/context";
import CursorService from "./service";
import Cursor from "./component";
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
class CursorContainer extends Component {
render() {
const { cursorX, cursorY } = this.props;
if (cursorX > 0 && cursorY > 0) {
const CursorContainer = (props) => {
const { cursorX, cursorY, presenter, uid, isViewersCursorLocked } = props;
if (cursorX > 0 && cursorY > 0) {
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const role = users[Auth.meetingID][Auth.userID].role;
const userId = users[Auth.meetingID][Auth.userID].userId;
const showCursor =
role === ROLE_MODERATOR || presenter || (!presenter && uid === userId);
if (!isViewersCursorLocked || (isViewersCursorLocked && showCursor)) {
return (
<Cursor
cursorX={cursorX}
cursorY={cursorY}
setLabelBoxDimensions={this.setLabelBoxDimensions}
{...this.props}
{...props}
/>
);
}
return null;
}
}
return null;
};
export default lockContextContainer(
withTracker((params) => {
const { cursorId, userLocks } = params;
const isViewersCursorLocked = userLocks?.hideViewersCursor;
const cursor = CursorService.getCurrentCursor(cursorId);
if (cursor) {
const {
xPercent: cursorX,
yPercent: cursorY,
userName,
userId,
presenter,
} = cursor;
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
export default withTracker((params) => {
const { cursorId } = params;
return {
cursorX,
cursorY,
userName,
presenter: presenter,
uid: userId,
isRTL,
isViewersCursorLocked,
};
}
const cursor = CursorService.getCurrentCursor(cursorId);
if (cursor) {
const { xPercent: cursorX, yPercent: cursorY, userName } = cursor;
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
return {
cursorX,
cursorY,
userName,
isRTL,
cursorX: -1,
cursorY: -1,
userName: "",
};
}
return {
cursorX: -1,
cursorY: -1,
userName: '',
};
})(CursorContainer);
})(CursorContainer)
);
CursorContainer.propTypes = {
// Defines the 'x' coordinate for the cursor, in percentages of the slide's width

View File

@ -14,9 +14,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
import CursorWrapperService from './service';
import CursorContainer from '../container';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
const CursorWrapperContainer = ({ presenterCursorId, multiUserCursorIds, ...rest }) => (
<g>
@ -30,8 +30,7 @@ const CursorWrapperContainer = ({ presenterCursorId, multiUserCursorIds, ...rest
/>
)
: null }
{multiUserCursorIds.map(cursorId => (
{multiUserCursorIds.map((cursorId) => (
<CursorContainer
key={cursorId._id}
cursorId={cursorId._id}
@ -46,7 +45,6 @@ export default withTracker((params) => {
const { podId, whiteboardId } = params;
const cursorIds = CursorWrapperService.getCurrentCursorIds(podId, whiteboardId);
const { presenterCursorId, multiUserCursorIds } = cursorIds;
const isMultiUser = WhiteboardService.isMultiUserActive(whiteboardId);
return {
@ -56,7 +54,6 @@ export default withTracker((params) => {
};
})(CursorWrapperContainer);
CursorWrapperContainer.propTypes = {
// Defines the object which contains the id of the presenter's cursor
presenterCursorId: PropTypes.shape({

View File

@ -5,9 +5,12 @@ const getCurrentCursor = (cursorId) => {
const cursor = Cursor.findOne({ _id: cursorId });
if (cursor) {
const { userId } = cursor;
const user = Users.findOne({ userId }, { fields: { name: 1 } });
const user = Users.findOne({ userId }, { fields: { name: 1, presenter: 1, userId: 1, role: 1 } });
if (user) {
cursor.userName = user.name;
cursor.userId = user.userId;
cursor.role = user.role;
cursor.presenter = user.presenter;
return cursor;
}
}

View File

@ -10,6 +10,8 @@ const PresentationMenuContainer = (props) => {
const fullscreen = layoutSelect((i) => i.fullscreen);
const { element: currentElement, group: currentGroup } = fullscreen;
const layoutContextDispatch = layoutDispatch();
const { elementId } = props;
const isFullscreen = currentElement === elementId;
return (
<PresentationMenu
@ -17,6 +19,7 @@ const PresentationMenuContainer = (props) => {
{...{
currentElement,
currentGroup,
isFullscreen,
layoutContextDispatch,
}}
/>
@ -25,7 +28,6 @@ const PresentationMenuContainer = (props) => {
export default withTracker((props) => {
const handleToggleFullscreen = (ref) => FullscreenService.toggleFullScreen(ref);
const { isFullscreen } = props;
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
const meetingId = Auth.meetingID;
const meetingObject = Meetings.findOne({ meetingId }, { fields: { 'meetingProp.name': 1 } });
@ -34,7 +36,6 @@ export default withTracker((props) => {
...props,
handleToggleFullscreen,
isIphone,
isFullscreen,
isDropdownOpen: Session.get('dropdownOpen'),
meetingName: meetingObject.meetingProp.name,
};

View File

@ -337,6 +337,7 @@ const isMeetingLocked = (id) => {
|| lockSettings.disablePublicChat
|| lockSettings.disableNotes
|| lockSettings.hideUserList
|| lockSettings.hideViewersCursor
|| usersProp.webcamsOnlyForModerator) {
isLocked = true;
}

View File

@ -958,7 +958,7 @@ class VideoProvider extends Component {
const peer = this.webRtcPeers[stream];
this.videoTags[stream] = video;
if (peer && !peer.attached) {
if (peer && !peer.attached && peer.stream === stream) {
this.attachVideoStream(stream);
}
}

View File

@ -1,90 +1,69 @@
import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import browserInfo from '/imports/utils/browserInfo';
import { Meteor } from 'meteor/meteor';
import React, { useEffect, useRef, useState } from 'react';
import { injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import BBBMenu from '/imports/ui/components/common/menu/component';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import FullscreenButtonContainer from '/imports/ui/components/common/fullscreen-button/container';
import Styled from './styles';
import VideoService from '../../service';
import ViewActions from '/imports/ui/components/video-provider/video-list/video-list-item/view-actions/component';
import UserActions from '/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component';
import UserStatus from '/imports/ui/components/video-provider/video-list/video-list-item/user-status/component';
import PinArea from '/imports/ui/components/video-provider/video-list/video-list-item/pin-area/component';
import {
isStreamStateUnhealthy,
subscribeToStreamStateChange,
unsubscribeFromStreamStateChange,
} from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
import { ACTIONS } from '/imports/ui/components/layout/enums';
import Settings from '/imports/ui/services/settings';
import Styled from './styles';
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
const { isSafari } = browserInfo;
const FULLSCREEN_CHANGE_EVENT = isSafari ? 'webkitfullscreenchange' : 'fullscreenchange';
const VideoListItem = (props) => {
const {
name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus,
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount,
} = props;
const intlMessages = defineMessages({
focusLabel: {
id: 'app.videoDock.webcamFocusLabel',
},
focusDesc: {
id: 'app.videoDock.webcamFocusDesc',
},
unfocusLabel: {
id: 'app.videoDock.webcamUnfocusLabel',
},
unfocusDesc: {
id: 'app.videoDock.webcamUnfocusDesc',
},
pinLabel: {
id: 'app.videoDock.webcamPinLabel',
},
pinDesc: {
id: 'app.videoDock.webcamPinDesc',
},
unpinLabel: {
id: 'app.videoDock.webcamUnpinLabel',
},
unpinLabelDisabled: {
id: 'app.videoDock.webcamUnpinLabelDisabled',
},
unpinDesc: {
id: 'app.videoDock.webcamUnpinDesc',
},
mirrorLabel: {
id: 'app.videoDock.webcamMirrorLabel',
},
mirrorDesc: {
id: 'app.videoDock.webcamMirrorDesc',
},
});
const [videoIsReady, setVideoIsReady] = useState(false);
const [isStreamHealthy, setIsStreamHealthy] = useState(false);
const [isMirrored, setIsMirrored] = useState(false);
class VideoListItem extends Component {
constructor(props) {
super(props);
this.videoTag = null;
const videoTag = useRef();
const videoContainer = useRef();
this.state = {
videoIsReady: false,
isFullscreen: false,
isStreamHealthy: false,
isMirrored: VideoService.mirrorOwnWebcam(props.userId),
const shouldRenderReconnect = !isStreamHealthy && videoIsReady;
const { animations } = Settings.application;
const talking = voiceUser?.talking;
const onStreamStateChange = (e) => {
const { streamState } = e.detail;
const newHealthState = !isStreamStateUnhealthy(streamState);
e.stopPropagation();
if (newHealthState !== isStreamHealthy) {
setIsStreamHealthy(newHealthState);
}
};
const handleSetVideoIsReady = () => {
setVideoIsReady(true);
window.dispatchEvent(new Event('resize'));
/* used when re-sharing cameras after leaving a breakout room.
it is needed in cases where the user has more than one active camera
so we only share the second camera after the first
has finished loading (can't share more than one at the same time) */
Session.set('canConnect', true);
};
// component did mount
useEffect(() => {
onVideoItemMount(videoTag.current);
subscribeToStreamStateChange(cameraId, onStreamStateChange);
videoTag.current.addEventListener('loadeddata', handleSetVideoIsReady);
return () => {
videoTag.current.removeEventListener('loadeddata', handleSetVideoIsReady);
};
}, []);
this.mirrorOwnWebcam = VideoService.mirrorOwnWebcam(props.userId);
this.setVideoIsReady = this.setVideoIsReady.bind(this);
this.onFullscreenChange = this.onFullscreenChange.bind(this);
this.onStreamStateChange = this.onStreamStateChange.bind(this);
}
componentDidMount() {
const { onVideoItemMount, cameraId } = this.props;
onVideoItemMount(this.videoTag);
this.videoTag.addEventListener('loadeddata', this.setVideoIsReady);
this.videoContainer.addEventListener(FULLSCREEN_CHANGE_EVENT, this.onFullscreenChange);
subscribeToStreamStateChange(cameraId, this.onStreamStateChange);
}
componentDidUpdate() {
// component will mount
useEffect(() => {
const playElement = (elem) => {
if (elem.paused) {
elem.play().catch((error) => {
@ -100,268 +79,88 @@ class VideoListItem extends Component {
// This is here to prevent the videos from freezing when they're
// moved around the dom by react, e.g., when changing the user status
// see https://bugs.chromium.org/p/chromium/issues/detail?id=382879
if (this.videoTag) {
playElement(this.videoTag);
if (videoIsReady) {
playElement(videoTag.current);
}
}
}, [videoIsReady]);
componentWillUnmount() {
const {
cameraId,
onVideoItemUnmount,
isFullscreenContext,
layoutContextDispatch,
} = this.props;
this.videoTag.removeEventListener('loadeddata', this.setVideoIsReady);
this.videoContainer.removeEventListener(FULLSCREEN_CHANGE_EVENT, this.onFullscreenChange);
unsubscribeFromStreamStateChange(cameraId, this.onStreamStateChange);
// component will unmount
useEffect(() => () => {
unsubscribeFromStreamStateChange(cameraId, onStreamStateChange);
onVideoItemUnmount(cameraId);
}, []);
if (isFullscreenContext) {
layoutContextDispatch({
type: ACTIONS.SET_FULLSCREEN_ELEMENT,
value: {
element: '',
group: '',
},
});
}
}
onStreamStateChange(e) {
const { streamState } = e.detail;
const { isStreamHealthy } = this.state;
const newHealthState = !isStreamStateUnhealthy(streamState);
e.stopPropagation();
if (newHealthState !== isStreamHealthy) {
this.setState({ isStreamHealthy: newHealthState });
}
}
onFullscreenChange() {
const { isFullscreen } = this.state;
const serviceIsFullscreen = FullscreenService.isFullScreen(this.videoContainer);
if (isFullscreen !== serviceIsFullscreen) {
this.setState({ isFullscreen: serviceIsFullscreen });
}
}
setVideoIsReady() {
const { videoIsReady } = this.state;
if (!videoIsReady) this.setState({ videoIsReady: true });
window.dispatchEvent(new Event('resize'));
/* used when re-sharing cameras after leaving a breakout room.
it is needed in cases where the user has more than one active camera
so we only share the second camera after the first
has finished loading (can't share more than one at the same time) */
Session.set('canConnect', true);
}
getAvailableActions() {
const {
intl,
cameraId,
numOfStreams,
onHandleVideoFocus,
user,
focused,
} = this.props;
const pinned = user?.pin;
const userId = user?.userId;
const isPinnedIntlKey = !pinned ? 'pin' : 'unpin';
const isFocusedIntlKey = !focused ? 'focus' : 'unfocus';
const menuItems = [{
key: `${cameraId}-mirror`,
label: intl.formatMessage(intlMessages.mirrorLabel),
description: intl.formatMessage(intlMessages.mirrorDesc),
onClick: () => this.mirrorCamera(cameraId),
}];
if (numOfStreams > 2) {
menuItems.push({
key: `${cameraId}-focus`,
label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]),
onClick: () => onHandleVideoFocus(cameraId),
});
}
if (VideoService.isVideoPinEnabledForCurrentUser()) {
menuItems.push({
key: `${cameraId}-pin`,
label: intl.formatMessage(intlMessages[`${isPinnedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isPinnedIntlKey}Desc`]),
onClick: () => VideoService.toggleVideoPin(userId, pinned),
});
}
return menuItems;
}
mirrorCamera() {
const { isMirrored } = this.state;
this.setState({ isMirrored: !isMirrored });
}
renderFullscreenButton() {
const { name, cameraId } = this.props;
const { isFullscreen } = this.state;
if (!ALLOW_FULLSCREEN) return null;
return (
<FullscreenButtonContainer
data-test="webcamsFullscreenButton"
fullscreenRef={this.videoContainer}
elementName={name}
elementId={cameraId}
elementGroup="webcams"
isFullscreen={isFullscreen}
dark
/>
);
}
renderPinButton() {
const { user, intl } = this.props;
const pinned = user?.pin;
const userId = user?.userId;
const shouldRenderPinButton = pinned && userId;
const videoPinActionAvailable = VideoService.isVideoPinEnabledForCurrentUser();
if (!shouldRenderPinButton) return null;
return (
<Styled.PinButtonWrapper>
<Styled.PinButton
color="default"
icon={!pinned ? 'pin-video_on' : 'pin-video_off'}
size="sm"
onClick={() => VideoService.toggleVideoPin(userId, true)}
label={videoPinActionAvailable
? intl.formatMessage(intlMessages.unpinLabel)
: intl.formatMessage(intlMessages.unpinLabelDisabled)}
hideLabel
disabled={!videoPinActionAvailable}
data-test="pinVideoButton"
/>
</Styled.PinButtonWrapper>
);
}
render() {
const {
videoIsReady,
isStreamHealthy,
isMirrored,
} = this.state;
const {
name,
user,
voiceUser,
numOfStreams,
isFullscreenContext,
} = this.props;
const availableActions = this.getAvailableActions();
const enableVideoMenu = Meteor.settings.public.kurento.enableVideoMenu || false;
const shouldRenderReconnect = !isStreamHealthy && videoIsReady;
const { isFirefox } = browserInfo;
const { animations } = Settings.application;
const talking = voiceUser?.talking;
const listenOnly = voiceUser?.listenOnly;
const muted = voiceUser?.muted;
const voiceUserJoined = voiceUser?.joined;
return (
<Styled.Content
talking={talking}
fullscreen={isFullscreenContext}
data-test={talking ? 'webcamItemTalkingUser' : 'webcamItem'}
animations={animations}
>
{
!videoIsReady
&& (
<Styled.WebcamConnecting
data-test="webcamConnecting"
talking={talking}
animations={animations}
>
<Styled.LoadingText>{name}</Styled.LoadingText>
</Styled.WebcamConnecting>
)
}
{
shouldRenderReconnect
&& <Styled.Reconnecting />
}
<Styled.VideoContainer ref={(ref) => { this.videoContainer = ref; }}>
<Styled.Video
muted
data-test={this.mirrorOwnWebcam ? 'mirroredVideoContainer' : 'videoContainer'}
mirrored={isMirrored}
unhealthyStream={shouldRenderReconnect}
ref={(ref) => { this.videoTag = ref; }}
autoPlay
playsInline
/>
{videoIsReady && this.renderFullscreenButton()}
{videoIsReady && this.renderPinButton()}
</Styled.VideoContainer>
{videoIsReady
&& (
<Styled.Info>
{enableVideoMenu && availableActions.length >= 1
? (
<BBBMenu
trigger={<Styled.DropdownTrigger tabIndex={0} data-test="dropdownWebcamButton">{name}</Styled.DropdownTrigger>}
actions={this.getAvailableActions()}
opts={{
id: "default-dropdown-menu",
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getContentAnchorEl: null,
fullwidth: "true",
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformorigin: { vertical: 'bottom', horizontal: 'left' },
}}
return (
<Styled.Content
ref={videoContainer}
talking={talking}
fullscreen={isFullscreenContext}
data-test={talking ? 'webcamItemTalkingUser' : 'webcamItem'}
animations={animations}
>
{
videoIsReady
? (
<>
<Styled.TopBar>
<PinArea
user={user}
/>
)
: (
<Styled.Dropdown isFirefox={isFirefox}>
<Styled.UserName noMenu={numOfStreams < 3}>
{name}
</Styled.UserName>
</Styled.Dropdown>
)}
{muted && !listenOnly ? <Styled.Muted iconName="unmute_filled" /> : null}
{listenOnly ? <Styled.Voice iconName="listen" /> : null}
{voiceUserJoined && !muted ? <Styled.Voice iconName="unmute" /> : null}
</Styled.Info>
)}
</Styled.Content>
);
}
}
<ViewActions
videoContainer={videoContainer}
name={name}
cameraId={cameraId}
isFullscreenContext={isFullscreenContext}
layoutContextDispatch={layoutContextDispatch}
/>
</Styled.TopBar>
<Styled.BottomBar>
<UserActions
name={name}
user={user}
cameraId={cameraId}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
/>
<UserStatus
voiceUser={voiceUser}
/>
</Styled.BottomBar>
</>
)
: (
<Styled.WebcamConnecting
data-test="webcamConnecting"
talking={talking}
animations={animations}
>
<Styled.LoadingText>{name}</Styled.LoadingText>
</Styled.WebcamConnecting>
)
}
<Styled.VideoContainer>
<Styled.Video
muted
mirrored={isMirrored}
unhealthyStream={shouldRenderReconnect}
ref={videoTag}
autoPlay
playsInline
/>
</Styled.VideoContainer>
{shouldRenderReconnect && <Styled.Reconnecting />}
</Styled.Content>
);
};
export default injectIntl(VideoListItem);
VideoListItem.defaultProps = {
numOfStreams: 0,
user: null,
};
VideoListItem.propTypes = {
@ -372,11 +171,15 @@ VideoListItem.propTypes = {
formatMessage: PropTypes.func.isRequired,
}).isRequired,
onHandleVideoFocus: PropTypes.func.isRequired,
onVideoItemMount: PropTypes.func.isRequired,
onVideoItemUnmount: PropTypes.func.isRequired,
isFullscreenContext: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
user: PropTypes.shape({
pin: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
}).isRequired,
voiceUser: PropTypes.shape({
voiceUser: PropTypes.shape({
muted: PropTypes.bool.isRequired,
listenOnly: PropTypes.bool.isRequired,
talking: PropTypes.bool.isRequired,

View File

@ -34,7 +34,11 @@ export default withTracker((props) => {
voiceUser: VoiceUsers.findOne({ intId: userId },
{ fields: { muted: 1, listenOnly: 1, talking: 1 } }),
user: Users.findOne({ intId: userId },
{ fields: { pin: 1, userId: 1 } }),
{
fields: {
pin: 1, userId: 1, name: 1,
},
}),
};
})(VideoListItemContainer);

View File

@ -0,0 +1,44 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import VideoService from '/imports/ui/components/video-provider/service';
import Styled from './styles';
const intlMessages = defineMessages({
unpinLabel: {
id: 'app.videoDock.webcamUnpinLabel',
},
unpinLabelDisabled: {
id: 'app.videoDock.webcamUnpinLabelDisabled',
},
});
const PinArea = (props) => {
const intl = useIntl();
const { user } = props;
const pinned = user?.pin;
const userId = user?.userId;
const shouldRenderPinButton = pinned && userId;
const videoPinActionAvailable = VideoService.isVideoPinEnabledForCurrentUser();
if (!shouldRenderPinButton) return <Styled.PinButtonWrapper />;
return (
<Styled.PinButtonWrapper>
<Styled.PinButton
color="default"
icon={!pinned ? 'pin-video_on' : 'pin-video_off'}
size="sm"
onClick={() => VideoService.toggleVideoPin(userId, true)}
label={videoPinActionAvailable
? intl.formatMessage(intlMessages.unpinLabel)
: intl.formatMessage(intlMessages.unpinLabelDisabled)}
hideLabel
disabled={!videoPinActionAvailable}
data-test="pinVideoButton"
/>
</Styled.PinButtonWrapper>
);
};
export default PinArea;

View File

@ -0,0 +1,43 @@
import styled from 'styled-components';
import Button from '/imports/ui/components/common/button/component';
import { colorTransparent, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
const PinButton = styled(Button)`
padding: 5px;
&,
&:active,
&:hover,
&:focus {
background-color: ${colorTransparent} !important;
border: none !important;
& > i {
border: none !important;
color: ${colorWhite};
font-size: 1rem;
background-color: ${colorTransparent} !important;
}
}
`;
const PinButtonWrapper = styled.div`
background-color: rgba(0,0,0,.3);
cursor: pointer;
border: 0;
margin: 2px;
height: fit-content;
[dir="rtl"] & {
right: auto;
left :0;
}
[class*="presentationZoomControls"] & {
position: relative !important;
}
`;
export default {
PinButtonWrapper,
PinButton,
};

View File

@ -1,21 +1,10 @@
import styled from 'styled-components';
import Icon from '/imports/ui/components/common/icon/component';
import Button from '/imports/ui/components/common/button/component';
import {
colorPrimary,
colorBlack,
colorWhite,
colorOffWhite,
colorDanger,
colorSuccess,
colorTransparent,
} from '/imports/ui/stylesheets/styled-components/palette';
import { TextElipsis, DivElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
import { landscape, mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
import {
audioIndicatorWidth,
audioIndicatorFs,
} from '/imports/ui/stylesheets/styled-components/general';
import { TextElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
const Content = styled.div`
position: relative;
@ -161,149 +150,24 @@ const Video = styled.video`
`}
`;
const Info = styled.div`
const TopBar = styled.div`
position: absolute;
bottom: 1px;
left: 7px;
right: 5px;
z-index: 2;
`;
const Dropdown = styled.div`
display: flex;
outline: none !important;
width: 70%;
@media ${mediumUp} {
>[aria-expanded] {
padding: .25rem;
}
}
@media ${landscape} {
button {
width: calc(100vw - 4rem);
margin-left: 1rem;
}
}
${({ isFirefox }) => isFirefox && `
max-width: 100%;
`}
`;
const UserName = styled(TextElipsis)`
position: relative;
max-width: 75%;
// Keep the background with 0.5 opacity, but leave the text with 1
background-color: rgba(0, 0, 0, 0.5);
border-radius: 1px;
color: ${colorOffWhite};
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
${({ noMenu }) => noMenu && `
padding: 0 .5rem 0 .5rem !important;
`}
`;
const Muted = styled(Icon)`
display: inline-block;
position: absolute;
right: 7px;
bottom: 6px;
width: ${audioIndicatorWidth};
height: ${audioIndicatorWidth};
min-width: ${audioIndicatorWidth};
min-height: ${audioIndicatorWidth};
color: ${colorWhite};
border-radius: 50%;
&::before {
font-size: ${audioIndicatorFs};
}
background-color: ${colorDanger};
`;
const Voice = styled(Icon)`
display: inline-block;
position: absolute;
right: 7px;
bottom: 6px;
width: ${audioIndicatorWidth};
height: ${audioIndicatorWidth};
min-width: ${audioIndicatorWidth};
min-height: ${audioIndicatorWidth};
color: ${colorWhite};
border-radius: 50%;
&::before {
font-size: ${audioIndicatorFs};
}
background-color: ${colorSuccess};
`;
const DropdownTrigger = styled(DivElipsis)`
position: relative;
max-width: 75%;
// Keep the background with 0.5 opacity, but leave the text with 1
background-color: rgba(0, 0, 0, 0.5);
border-radius: 1px;
color: ${colorOffWhite};
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
cursor: pointer;
&::after {
content: "\\203a";
position: absolute;
transform: rotate(90deg);
top: 45%;
width: 0;
line-height: 0;
right: .45rem;
}
`;
const PinButtonWrapper = styled.div`
position: absolute;
width: 100%;
z-index: 1;
top: 0;
left: auto;
background-color: rgba(0,0,0,.3);
cursor: pointer;
border: 0;
z-index: 2;
margin: 2px;
[dir="rtl"] & {
right: auto;
left :0;
}
[class*="presentationZoomControls"] & {
position: relative !important;
}
padding: 5px;
justify-content: space-between;
`;
const PinButton = styled(Button)`
padding: 5px;
&,
&:active,
&:hover,
&:focus {
background-color: ${colorTransparent} !important;
border: none !important;
& > i {
border: none !important;
color: ${colorWhite};
font-size: 1rem;
background-color: ${colorTransparent} !important;
}
}
const BottomBar = styled.div`
position: absolute;
display: flex;
width: 100%;
z-index: 1;
bottom: 0;
padding: 1px 7px;
justify-content: space-between;
`;
export default {
@ -313,12 +177,6 @@ export default {
Reconnecting,
VideoContainer,
Video,
Info,
Dropdown,
UserName,
Muted,
Voice,
DropdownTrigger,
PinButtonWrapper,
PinButton,
};
TopBar,
BottomBar,
};

View File

@ -0,0 +1,139 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import browserInfo from '/imports/utils/browserInfo';
import VideoService from '/imports/ui/components/video-provider/service';
import BBBMenu from '/imports/ui/components/common/menu/component';
import PropTypes from 'prop-types';
import Styled from './styles';
const intlMessages = defineMessages({
focusLabel: {
id: 'app.videoDock.webcamFocusLabel',
},
focusDesc: {
id: 'app.videoDock.webcamFocusDesc',
},
unfocusLabel: {
id: 'app.videoDock.webcamUnfocusLabel',
},
unfocusDesc: {
id: 'app.videoDock.webcamUnfocusDesc',
},
pinLabel: {
id: 'app.videoDock.webcamPinLabel',
},
unpinLabel: {
id: 'app.videoDock.webcamUnpinLabel',
},
pinDesc: {
id: 'app.videoDock.webcamPinDesc',
},
unpinDesc: {
id: 'app.videoDock.webcamUnpinDesc',
},
mirrorLabel: {
id: 'app.videoDock.webcamMirrorLabel',
},
mirrorDesc: {
id: 'app.videoDock.webcamMirrorDesc',
},
});
const UserActions = (props) => {
const {
name, cameraId, numOfStreams, onHandleVideoFocus, user, focused, onHandleMirror,
} = props;
const intl = useIntl();
const enableVideoMenu = Meteor.settings.public.kurento.enableVideoMenu || false;
const { isFirefox } = browserInfo;
const getAvailableActions = () => {
const pinned = user?.pin;
const userId = user?.userId;
const isPinnedIntlKey = !pinned ? 'pin' : 'unpin';
const isFocusedIntlKey = !focused ? 'focus' : 'unfocus';
const menuItems = [{
key: `${cameraId}-mirror`,
label: intl.formatMessage(intlMessages.mirrorLabel),
description: intl.formatMessage(intlMessages.mirrorDesc),
onClick: () => onHandleMirror(),
}];
if (numOfStreams > 2) {
menuItems.push({
key: `${cameraId}-focus`,
label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]),
onClick: () => onHandleVideoFocus(cameraId),
});
}
if (VideoService.isVideoPinEnabledForCurrentUser()) {
menuItems.push({
key: `${cameraId}-pin`,
label: intl.formatMessage(intlMessages[`${isPinnedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isPinnedIntlKey}Desc`]),
onClick: () => VideoService.toggleVideoPin(userId, pinned),
});
}
return menuItems;
};
return (
<Styled.MenuWrapper>
{enableVideoMenu && getAvailableActions().length >= 1
? (
<BBBMenu
trigger={(
<Styled.DropdownTrigger
tabIndex={0}
data-test="dropdownWebcamButton"
>
{name}
</Styled.DropdownTrigger>
)}
actions={getAvailableActions()}
opts={{
id: 'default-dropdown-menu',
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getContentAnchorEl: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformorigin: { vertical: 'bottom', horizontal: 'left' },
}}
/>
)
: (
<Styled.Dropdown isFirefox={isFirefox}>
<Styled.UserName noMenu={numOfStreams < 3}>
{name}
</Styled.UserName>
</Styled.Dropdown>
)}
</Styled.MenuWrapper>
);
};
export default UserActions;
UserActions.defaultProps = {
focused: false,
};
UserActions.propTypes = {
name: PropTypes.string.isRequired,
cameraId: PropTypes.string.isRequired,
numOfStreams: PropTypes.number.isRequired,
onHandleVideoFocus: PropTypes.func.isRequired,
user: PropTypes.shape({
pin: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
}).isRequired,
focused: PropTypes.bool,
onHandleMirror: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,78 @@
import styled from 'styled-components';
import { colorOffWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { TextElipsis, DivElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
import { landscape, mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
const DropdownTrigger = styled(DivElipsis)`
position: relative;
// Keep the background with 0.5 opacity, but leave the text with 1
background-color: rgba(0, 0, 0, 0.5);
border-radius: 1px;
color: ${colorOffWhite};
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
cursor: pointer;
white-space: nowrap;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
&::after {
content: "\\203a";
position: absolute;
transform: rotate(90deg);
top: 45%;
width: 0;
line-height: 0;
right: .45rem;
}
`;
const UserName = styled(TextElipsis)`
position: relative;
max-width: 75%;
// Keep the background with 0.5 opacity, but leave the text with 1
background-color: rgba(0, 0, 0, 0.5);
border-radius: 1px;
color: ${colorOffWhite};
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
${({ noMenu }) => noMenu && `
padding: 0 .5rem 0 .5rem !important;
`}
`;
const Dropdown = styled.div`
display: flex;
outline: none !important;
width: 70%;
@media ${mediumUp} {
>[aria-expanded] {
padding: .25rem;
}
}
@media ${landscape} {
button {
width: calc(100vw - 4rem);
margin-left: 1rem;
}
}
${({ isFirefox }) => isFirefox && `
max-width: 100%;
`}
`;
const MenuWrapper = styled.div`
max-width: 75%;
`;
export default {
DropdownTrigger,
UserName,
Dropdown,
MenuWrapper,
};

View File

@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
const UserStatus = (props) => {
const { voiceUser } = props;
const listenOnly = voiceUser?.listenOnly;
const muted = voiceUser?.muted;
const voiceUserJoined = voiceUser?.joined;
return (
<>
{(muted && !listenOnly) && <Styled.Muted iconName="unmute_filled" />}
{listenOnly && <Styled.Voice iconName="listen" /> }
{(voiceUserJoined && !muted) && <Styled.Voice iconName="unmute" />}
</>
);
};
export default UserStatus;
UserStatus.defaultProps = {
};
UserStatus.propTypes = {
voiceUser: PropTypes.shape({
listenOnly: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
joined: PropTypes.bool,
}).isRequired,
};

View File

@ -0,0 +1,39 @@
import styled from 'styled-components';
import Icon from '/imports/ui/components/common/icon/component';
import { audioIndicatorFs, audioIndicatorWidth } from '/imports/ui/stylesheets/styled-components/general';
import { colorDanger, colorSuccess, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
const Voice = styled(Icon)`
width: ${audioIndicatorWidth};
height: ${audioIndicatorWidth};
min-width: ${audioIndicatorWidth};
min-height: ${audioIndicatorWidth};
color: ${colorWhite};
border-radius: 50%;
&::before {
font-size: ${audioIndicatorFs};
}
background-color: ${colorSuccess};
`;
const Muted = styled(Icon)`
width: ${audioIndicatorWidth};
height: ${audioIndicatorWidth};
min-width: ${audioIndicatorWidth};
min-height: ${audioIndicatorWidth};
color: ${colorWhite};
border-radius: 50%;
&::before {
font-size: ${audioIndicatorFs};
}
background-color: ${colorDanger};
`;
export default {
Voice,
Muted,
};

View File

@ -0,0 +1,55 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { ACTIONS } from '/imports/ui/components/layout/enums';
import FullscreenButtonContainer from '/imports/ui/components/common/fullscreen-button/container';
import Styled from './styles';
const ViewActions = (props) => {
const {
name, cameraId, videoContainer, isFullscreenContext, layoutContextDispatch,
} = props;
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
useEffect(() => () => {
// exit fullscreen when component is unmounted
if (isFullscreenContext) {
layoutContextDispatch({
type: ACTIONS.SET_FULLSCREEN_ELEMENT,
value: {
element: '',
group: '',
},
});
}
}, []);
if (!ALLOW_FULLSCREEN) return null;
return (
<Styled.FullscreenWrapper>
<FullscreenButtonContainer
data-test="webcamsFullscreenButton"
fullscreenRef={videoContainer.current}
elementName={name}
elementId={cameraId}
elementGroup="webcams"
isFullscreen={isFullscreenContext}
dark
/>
</Styled.FullscreenWrapper>
);
};
export default ViewActions;
ViewActions.propTypes = {
name: PropTypes.string.isRequired,
cameraId: PropTypes.string.isRequired,
videoContainer: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired,
isFullscreenContext: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
const FullscreenWrapper = styled.div`
position: relative;
`;
export default {
FullscreenWrapper,
};

View File

@ -29,7 +29,10 @@ const ReactiveAnnotationContainer = (props) => {
export default withTracker((params) => {
const { shapeId } = params;
const annotation = ReactiveAnnotationService.getAnnotationById(shapeId);
const unsentAnnotation = ReactiveAnnotationService.getUnsentAnnotationById(shapeId);
const isUnsentAnnotation = unsentAnnotation !== undefined;
const annotation = isUnsentAnnotation
? unsentAnnotation : ReactiveAnnotationService.getAnnotationById(shapeId);
const isViewer = Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID }, {
fields: {
role: 1,

View File

@ -1,9 +1,14 @@
import { UnsentAnnotations } from '/imports/ui/components/whiteboard/service';
import { Annotations, UnsentAnnotations } from '/imports/ui/components/whiteboard/service';
const getAnnotationById = _id => UnsentAnnotations.findOne({
const getAnnotationById = _id => Annotations.findOne({
_id,
});
const getUnsentAnnotationById = _id => UnsentAnnotations.findOne({
_id,
});
export default {
getAnnotationById,
getUnsentAnnotationById,
};

View File

@ -6,14 +6,17 @@ import { Slides } from '/imports/api/slides';
import { makeCall } from '/imports/ui/services/api';
import PresentationService from '/imports/ui/components/presentation/service';
import logger from '/imports/startup/client/logger';
import { isEqual } from 'lodash';
const Annotations = new Mongo.Collection(null);
const UnsentAnnotations = new Mongo.Collection(null);
const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations;
const DRAW_START = ANNOTATION_CONFIG.status.start;
const DRAW_UPDATE = ANNOTATION_CONFIG.status.update;
const DRAW_END = ANNOTATION_CONFIG.status.end;
const ANNOTATION_TYPE_PENCIL = 'pencil';
const ANNOTATION_TYPE_TEXT = 'text';
let annotationsStreamListener = null;
@ -24,6 +27,64 @@ const clearPreview = (annotation) => {
function clearFakeAnnotations() {
UnsentAnnotations.remove({});
Annotations.remove({ id: /-fake/g });
}
function handleAddedLiveSyncPreviewAnnotation({
meetingId, whiteboardId, userId, annotation,
}) {
const isOwn = Auth.meetingID === meetingId && Auth.userID === userId;
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation);
if (!isOwn) {
Annotations.upsert(query.selector, query.modifier);
return;
}
const fakeAnnotation = Annotations.findOne({ id: `${annotation.id}-fake` });
let fakePoints;
if (fakeAnnotation) {
fakePoints = fakeAnnotation.annotationInfo.points;
const { points: lastPoints } = annotation.annotationInfo;
if (annotation.annotationType !== 'pencil') {
Annotations.update(fakeAnnotation._id, {
$set: {
position: annotation.position,
'annotationInfo.color': isEqual(fakePoints, lastPoints) || annotation.status === DRAW_END
? annotation.annotationInfo.color : fakeAnnotation.annotationInfo.color,
},
$inc: { version: 1 }, // TODO: Remove all this version stuff
});
return;
}
}
Annotations.upsert(query.selector, query.modifier, (err) => {
if (err) {
logger.error({
logCode: 'whiteboard_annotation_upsert_error',
extraInfo: { error: err },
}, 'Error on adding an annotation');
return;
}
// Remove fake annotation for pencil on draw end
if (annotation.status === DRAW_END) {
Annotations.remove({ id: `${annotation.id}-fake` });
return;
}
if (annotation.status === DRAW_START) {
Annotations.update(fakeAnnotation._id, {
$set: {
position: annotation.position - 1,
},
$inc: { version: 1 }, // TODO: Remove all this version stuff
});
}
});
}
function handleAddedAnnotation({
@ -51,8 +112,11 @@ function handleRemovedAnnotation({
if (shapeId) {
query.id = shapeId;
}
Annotations.remove(query);
const annotationIsFake = Annotations.remove(query) === 0;
if (annotationIsFake) {
query.id = { $in: [shapeId, `${shapeId}-fake`] };
Annotations.remove(query);
}
}
export function initAnnotationsStreamListener() {
@ -82,7 +146,14 @@ export function initAnnotationsStreamListener() {
annotationsStreamListener.on('removed', handleRemovedAnnotation);
annotationsStreamListener.on('added', ({ annotations }) => {
annotations.forEach(annotation => handleAddedAnnotation(annotation));
annotations.forEach((annotation) => {
const tool = annotation.annotation.annotationType;
if (tool === ANNOTATION_TYPE_TEXT) {
handleAddedLiveSyncPreviewAnnotation(annotation);
} else {
handleAddedAnnotation(annotation);
}
});
});
});
}
@ -183,6 +254,33 @@ const sendAnnotation = (annotation) => {
}
};
const sendLiveSyncPreviewAnnotation = (annotation) => {
// Prevent sending annotations while disconnected
if (!Meteor.status().connected) return;
annotationsQueue.push(annotation);
if (!annotationsSenderIsRunning) setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin);
// skip optimistic for draw end since the smoothing is done in akka
if (annotation.status === DRAW_END) return;
const { position, ...relevantAnotation } = annotation;
const queryFake = addAnnotationQuery(
Auth.meetingID, annotation.wbId, Auth.userID,
{
...relevantAnotation,
id: `${annotation.id}-fake`,
position: Number.MAX_SAFE_INTEGER,
annotationInfo: {
...annotation.annotationInfo,
color: increaseBrightness(annotation.annotationInfo.color, 40),
},
},
);
Annotations.upsert(queryFake.selector, queryFake.modifier);
};
WhiteboardMultiUser.find({ meetingId: Auth.meetingID }).observeChanges({
changed: clearFakeAnnotations,
});
@ -288,6 +386,7 @@ export {
Annotations,
UnsentAnnotations,
sendAnnotation,
sendLiveSyncPreviewAnnotation,
clearPreview,
getMultiUser,
getMultiUserSize,

View File

@ -233,6 +233,7 @@ export default class WhiteboardOverlay extends Component {
const {
whiteboardId,
sendAnnotation,
sendLiveSyncPreviewAnnotation,
resetTextShapeSession,
setTextShapeActiveId,
contextMenuHandler,
@ -249,6 +250,7 @@ export default class WhiteboardOverlay extends Component {
normalizeThickness: this.normalizeThickness,
normalizeFont: this.normalizeFont,
sendAnnotation,
sendLiveSyncPreviewAnnotation,
resetTextShapeSession,
setTextShapeActiveId,
contextMenuHandler,
@ -290,6 +292,8 @@ WhiteboardOverlay.propTypes = {
viewBoxHeight: PropTypes.number.isRequired,
// Defines a handler to publish an annotation to the server
sendAnnotation: PropTypes.func.isRequired,
// Defines a handler to public an annotation with live preview to the server
sendLiveSyncPreviewAnnotation: PropTypes.func.isRequired,
// Defines a handler to clear a shape preview
clearPreview: PropTypes.func.isRequired,
// Defines a current whiteboard id

View File

@ -18,6 +18,7 @@ export default withTracker(() => ({
clearPreview: WhiteboardOverlayService.clearPreview,
contextMenuHandler: WhiteboardOverlayService.contextMenuHandler,
sendAnnotation: WhiteboardOverlayService.sendAnnotation,
sendLiveSyncPreviewAnnotation: WhiteboardOverlayService.sendLiveSyncPreviewAnnotation,
setTextShapeActiveId: WhiteboardOverlayService.setTextShapeActiveId,
resetTextShapeSession: WhiteboardOverlayService.resetTextShapeSession,
drawSettings: WhiteboardOverlayService.getWhiteboardToolbarValues(),

View File

@ -1,6 +1,6 @@
import Storage from '/imports/ui/services/storage/session';
import Auth from '/imports/ui/services/auth';
import { sendAnnotation, clearPreview } from '/imports/ui/components/whiteboard/service';
import { sendAnnotation, sendLiveSyncPreviewAnnotation, clearPreview } from '/imports/ui/components/whiteboard/service';
import { publishCursorUpdate } from '/imports/ui/components/cursor/service';
const DRAW_SETTINGS = 'drawSettings';
@ -56,6 +56,7 @@ const updateCursor = (payload) => {
export default {
sendAnnotation,
sendLiveSyncPreviewAnnotation,
getWhiteboardToolbarValues,
setTextShapeActiveId,
resetTextShapeSession,

View File

@ -447,6 +447,7 @@ export default class TextDrawListener extends Component {
const {
normalizeFont,
sendAnnotation,
sendLiveSyncPreviewAnnotation,
} = actions;
const {
@ -478,7 +479,7 @@ export default class TextDrawListener extends Component {
position: 0,
};
sendAnnotation(annotation, whiteboardId);
sendLiveSyncPreviewAnnotation(annotation, whiteboardId);
}
discardAnnotation() {
@ -591,6 +592,8 @@ TextDrawListener.propTypes = {
normalizeFont: PropTypes.func.isRequired,
// Defines a function which we use to publish a message to the server
sendAnnotation: PropTypes.func.isRequired,
// Defines a function to wich we use to publish a message to the server
sendLiveSyncPreviewAnnotation: PropTypes.func.isRequired,
// Defines a function which resets the current state of the text shape drawing
resetTextShapeSession: PropTypes.func.isRequired,
// Defines a function that sets a session value for the current active text shape

View File

@ -132,6 +132,8 @@
"app.userList.userOptions.savedNames.title": "List of users in meeting {0} at {1}",
"app.userList.userOptions.sortedFirstName.heading": "Sorted by first name:",
"app.userList.userOptions.sortedLastName.heading": "Sorted by last name:",
"app.userList.userOptions.hideViewersCursor": "Viewer cursors are locked",
"app.userList.userOptions.showViewersCursor": "Viewer cursors are unlocked",
"app.media.label": "Media",
"app.media.autoplayAlertDesc": "Allow Access",
"app.media.screenshare.start": "Screenshare has started",
@ -695,7 +697,7 @@
"app.lock-viewers.button.apply": "Apply",
"app.lock-viewers.button.cancel": "Cancel",
"app.lock-viewers.locked": "Locked",
"app.lock-viewers.unlocked": "Unlocked",
"app.lock-viewers.hideViewersCursor": "See other viewers cursors",
"app.guest-policy.ariaTitle": "Guest policy settings modal",
"app.guest-policy.title": "Guest policy",
"app.guest-policy.description": "Change meeting guest policy setting",
@ -952,6 +954,7 @@
"playback.button.search.aria": "Search",
"playback.button.section.aria": "Side section",
"playback.button.swap.aria": "Swap content",
"playback.button.theme.aria": "Toggle theme",
"playback.error.wrapper.aria": "Error area",
"playback.loader.wrapper.aria": "Loader area",
"playback.player.wrapper.aria": "Player area",

View File

@ -4,9 +4,10 @@
"requires": true,
"packages": {
"": {
"name": "playwright",
"devDependencies": {
"@playwright/test": "^1.18.1",
"axios": "^0.25.0",
"axios": "^0.26.1",
"dotenv": "^16.0.0",
"js-yaml": "^4.1.0",
"playwright": "^1.18.1",
@ -1060,12 +1061,12 @@
"dev": true
},
"node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.7"
"follow-redirects": "^1.14.8"
}
},
"node_modules/babel-plugin-dynamic-import-node": {
@ -2056,9 +2057,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"node_modules/ms": {
@ -3416,12 +3417,12 @@
"dev": true
},
"axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.7"
"follow-redirects": "^1.14.8"
}
},
"babel-plugin-dynamic-import-node": {
@ -4152,9 +4153,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"ms": {

View File

@ -7,7 +7,7 @@
},
"devDependencies": {
"@playwright/test": "^1.18.1",
"axios": "^0.25.0",
"axios": "^0.26.1",
"dotenv": "^16.0.0",
"js-yaml": "^4.1.0",
"playwright": "^1.18.1",

File diff suppressed because it is too large Load Diff

View File

@ -6,24 +6,22 @@
"verbose": false
},
"dependencies": {
"axios": "^0.21.4",
"babel-jest": "^27.0.6",
"axios": "^0.26.1",
"babel-jest": "^27.5.1",
"child_process": "^1.0.2",
"dotenv": "^6.0.0",
"fs-extra": "^9.1.0",
"jest": "^26.6.3",
"jest-image-snapshot": "^4.4.0",
"jest-transform-css": "^2.1.0",
"jest-transform-file": "^1.1.1",
"js-yaml": "^4.0.0",
"moment": "^2.24.0",
"puppeteer-mass-screenshots": "^1.0.14",
"puppeteer-video-recorder": "^1.0.3",
"dotenv": "^16.0.0",
"fs-extra": "^10.0.1",
"jest": "^27.5.1",
"jest-image-snapshot": "^4.5.1",
"js-yaml": "^4.1.0",
"moment": "^2.29.1",
"puppeteer-mass-screenshots": "^1.0.15",
"puppeteer-video-recorder": "^1.0.5",
"sha1": "^1.1.1",
"sleep-promise": "9.1.0",
"yaml": "^1.7.2"
"yaml": "^1.10.2"
},
"devDependencies": {
"clipboardy": "^2.1.0"
"clipboardy": "^3.0.0"
}
}

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash
IS_BBB_WEB_RUNNING=`netstat -an | grep LISTEN | grep 8090 > /dev/null && echo 1 || echo 0`
IS_BBB_WEB_RUNNING=`ss -lt | grep ":8090" > /dev/null && echo 1 || echo 0`
if [ "$IS_BBB_WEB_RUNNING" = "1" ]; then
echo "bbb-web is running, exiting"

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash
IS_BBB_WEB_RUNNING=`netstat -an | grep LISTEN | grep 8090 > /dev/null && echo 1 || echo 0`
IS_BBB_WEB_RUNNING=`ss -an | grep LISTEN | grep 8090 > /dev/null && echo 1 || echo 0`
if [ "$IS_BBB_WEB_RUNNING" = "1" ]; then
echo "bbb-web is running, exiting"

View File

@ -116,7 +116,7 @@ fi
sed -i 's/worker_connections 768/worker_connections 4000/g' /etc/nginx/nginx.conf
if grep "worker_rlimit_nofile" /etc/nginx/nginx.conf; then
if grep -q "worker_rlimit_nofile" /etc/nginx/nginx.conf; then
num=$(grep worker_rlimit_nofile /etc/nginx/nginx.conf | grep -o '[0-9]*')
if [[ "$num" -lt 10000 ]]; then
sed -i 's/worker_rlimit_nofile [0-9 ]*;/worker_rlimit_nofile 10000;/g' /etc/nginx/nginx.conf

View File

@ -12,6 +12,11 @@ case "$1" in
sed -i "s@<X-PRE-PROCESS cmd=\"set\" data=\"local_ip_v4=.*\"/>@<X-PRE-PROCESS cmd=\"set\" data=\"local_ip_v4=$IP\"/>@g" /opt/freeswitch/etc/freeswitch/vars.xml
fi
# Fix issue #14670 (we do it here to fix a previously broken install)
if grep -q "data=\"local_ip_v4=\"" /opt/freeswitch/etc/freeswitch/vars.xml; then
sed -i "s@<X-PRE-PROCESS cmd=\"set\" data=\"local_ip_v4=.*\"/>@<X-PRE-PROCESS cmd=\"set\" data=\"local_ip_v4=$IP\"/>@g" /opt/freeswitch/etc/freeswitch/vars.xml
fi
sed -n 's/,VP8//g' /opt/freeswitch/etc/freeswitch/vars.xml
SOURCE=/tmp/external.xml

View File

@ -58,9 +58,9 @@ fi
source /etc/lsb-release
# Setup specific version of node
# Set up specific version of node
if [ "$DISTRIB_CODENAME" == "focal" ]; then
node_version="14.18.1"
node_version="14.18.3"
if [[ ! -d /usr/share/node-v${node_version}-linux-x64 ]]; then
cd /usr/share
tar xfz "node-v${node_version}-linux-x64.tar.gz"

View File

@ -92,11 +92,11 @@ cp bbb-html5-frontend@.service staging/usr/lib/systemd/system
mkdir -p staging/usr/share
if [ ! -f node-v14.18.1-linux-x64.tar.gz ]; then
wget https://nodejs.org/dist/v14.18.1/node-v14.18.1-linux-x64.tar.gz
if [ ! -f node-v14.18.3-linux-x64.tar.gz ]; then
wget https://nodejs.org/dist/v14.18.3/node-v14.18.3-linux-x64.tar.gz
fi
cp node-v14.18.1-linux-x64.tar.gz staging/usr/share
cp node-v14.18.3-linux-x64.tar.gz staging/usr/share
if [ -f staging/usr/share/meteor/bundle/programs/web.browser/head.html ]; then
sed -i "s/VERSION/$(($BUILD))/" staging/usr/share/meteor/bundle/programs/web.browser/head.html

View File

@ -15,7 +15,7 @@ nohup mongod --config ./mongo-ramdisk.conf --oplogSize 8 --replSet rs0 --noauth
MONGO_OK=0
while [ "$MONGO_OK" = "0" ]; do
MONGO_OK=`netstat -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0`
MONGO_OK=`ss -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0`
sleep 1;
done;

View File

@ -9,7 +9,7 @@ echo "Starting mongoDB"
MONGO_OK=0
while [ "$MONGO_OK" = "0" ]; do
MONGO_OK=$(netstat -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0)
MONGO_OK=$(ss -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0)
sleep 1;
done;
@ -49,7 +49,7 @@ fi
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
export MONGO_URL=mongodb://127.0.1.1/meteor
export NODE_ENV=production
export NODE_VERSION=node-v14.18.1-linux-x64
export NODE_VERSION=node-v14.18.3-linux-x64
export SERVER_WEBSOCKET_COMPRESSION=0
export BIND_IP=127.0.0.1
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=$INSTANCE_ID

View File

@ -9,7 +9,7 @@ echo "Starting mongoDB"
MONGO_OK=0
while [ "$MONGO_OK" = "0" ]; do
MONGO_OK=$(netstat -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0)
MONGO_OK=$(ss -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0)
sleep 1;
done;
@ -49,7 +49,7 @@ fi
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
export MONGO_URL=mongodb://127.0.1.1/meteor
export NODE_ENV=production
export NODE_VERSION=node-v14.18.1-linux-x64
export NODE_VERSION=node-v14.18.3-linux-x64
export SERVER_WEBSOCKET_COMPRESSION=0
export BIND_IP=127.0.0.1
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js

View File

@ -25,30 +25,6 @@ case "$1" in
chmod +r $TARGET
if ! gem -v | grep -q ^3.; then
gem update --system --no-document
if grep -q focal /etc/lsb-release; then
gem install bundler -v 2.1.4
else
gem install bundler --no-document
fi
fi
if hash gem 2>&-; then
cd /usr/local/bigbluebutton/core
GEMS="builder bundler"
for gem in $GEMS; do
if ! gem list $gem | grep -q $gem; then
gem install $gem
fi
done
/usr/local/bin/bundle
else
echo "## Could not find gem ##"
exit 1
fi
# Run recording link fixup/upgrade script
# Don't abort on failure; users can manually run it later, too
if id $BBB_USER > /dev/null 2>&1 ; then

View File

@ -27,15 +27,21 @@ done
mkdir -p staging/var/log/bigbluebutton
cp -r scripts lib Gemfile Gemfile.lock staging/usr/local/bigbluebutton/core
if [ "$DISTRO" == "focal" ]; then
cp Rakefile staging/usr/local/bigbluebutton/core
fi
pushd staging/usr/local/bigbluebutton/core
bundle config set --local deployment true
bundle install
# Remove unneeded files to reduce package size
bundle clean
rm -r vendor/bundle/ruby/*/cache
find vendor/bundle -name '*.o' -delete
popd
cp Rakefile staging/usr/local/bigbluebutton/core
cp bbb-record-core.logrotate staging/etc/logrotate.d
mkdir -p staging/usr/lib/systemd/system
cp systemd/* staging/usr/lib/systemd/system
SYSTEMDSYSTEMUNITDIR=$(pkg-config --variable systemdsystemunitdir systemd)
mkdir -p "staging${SYSTEMDSYSTEMUNITDIR}"
cp systemd/* "staging${SYSTEMDSYSTEMUNITDIR}"
if [ -f "staging/usr/local/bigbluebutton/core/scripts/basic_stats.nginx" ]; then \
mkdir -p staging/usr/share/bigbluebutton/nginx; \

View File

@ -1,4 +1,4 @@
. ./opts-global.sh
# TODO - add yq
OPTS="$OPTS -t deb -d ffmpeg,ruby-dev,libcurl4-openssl-dev,libxslt1-dev,libxml2-dev,build-essential,bbb-mkclean,libncurses5-dev,zlib1g-dev,python3,python3-lxml,python3-icu,redis-server,poppler-utils,ruby,rsync,libsystemd-dev,python3-attr,python3-cairo,python3-gi,gir1.2-pango-1.0,python3-gi-cairo"
OPTS="$OPTS -t deb -d bbb-mkclean,ffmpeg,gir1.2-pango-1.0,libcurl4,libncurses5,libsystemd0,poppler-utils,python3,python3-attr,python3-cairo,python3-gi,python3-gi-cairo,python3-icu,python3-lxml,redis-server,rsync,ruby,ruby-bundler,zlib1g"

View File

@ -18,10 +18,12 @@
# You should have received a copy of the GNU Lesser General Public License
# along with BigBlueButton. If not, see <http://www.gnu.org/licenses/>.
require 'rubygems'
require 'bundler/setup'
require_relative '../lib/recordandplayback'
require 'recordandplayback/workers'
require 'rubygems'
require 'yaml'
require 'fileutils'
require 'resque'

View File

@ -4,6 +4,7 @@ Description=BigBlueButton recording caption upload handler
[Service]
Type=simple
ExecStart=/usr/local/bigbluebutton/core/scripts/rap-caption-inbox.rb
WorkingDirectory=/usr/local/bigbluebutton/core
User=bigbluebutton
Slice=bbb_record_core.slice
Restart=on-failure

View File

@ -5,7 +5,7 @@ After=redis.service
[Service]
Type=simple
ExecStart=/bin/sh -c '/usr/bin/rake -f ../Rakefile resque:workers >> /var/log/bigbluebutton/bbb-rap-worker.log'
ExecStart=/usr/bin/bundle exec rake -f Rakefile resque:workers
WorkingDirectory=/usr/local/bigbluebutton/core/scripts
Environment=QUEUE=rap:archive,rap:publish,rap:process,rap:sanity,rap:captions,rap:events
Environment=COUNT=1