Merge pull request #14704 from antobinary/merge-25-develop
chore: Merge 2.5 into develop
This commit is contained in:
commit
d66c162669
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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(),
|
||||
|
@ -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() {
|
||||
|
@ -668,7 +668,7 @@ public class Meeting {
|
||||
meetingExpireWhenLastUserLeftInMinutes = value;
|
||||
}
|
||||
|
||||
public Integer getmeetingExpireWhenLastUserLeftInMinutes() {
|
||||
public Integer getMeetingExpireWhenLastUserLeftInMinutes() {
|
||||
return meetingExpireWhenLastUserLeftInMinutes;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.5.0-alpha.5
|
||||
BIGBLUEBUTTON_RELEASE=2.5.0-alpha.6
|
||||
|
@ -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 )
|
||||
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
METEOR@2.5
|
||||
METEOR@2.6.1
|
||||
|
@ -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
|
||||
|
@ -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 '_____________'
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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 {
|
||||
// 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,
|
||||
};
|
||||
|
||||
// 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')) {
|
||||
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}`);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default withTracker((params) => {
|
||||
const { cursorId } = params;
|
||||
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 } = cursor;
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
const {
|
||||
xPercent: cursorX,
|
||||
yPercent: cursorY,
|
||||
userName,
|
||||
userId,
|
||||
presenter,
|
||||
} = cursor;
|
||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||
|
||||
return {
|
||||
cursorX,
|
||||
cursorY,
|
||||
userName,
|
||||
presenter: presenter,
|
||||
uid: userId,
|
||||
isRTL,
|
||||
isViewersCursorLocked,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
cursorX: -1,
|
||||
cursorY: -1,
|
||||
userName: '',
|
||||
userName: "",
|
||||
};
|
||||
})(CursorContainer);
|
||||
|
||||
})(CursorContainer)
|
||||
);
|
||||
|
||||
CursorContainer.propTypes = {
|
||||
// Defines the 'x' coordinate for the cursor, in percentages of the slide's width
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -337,6 +337,7 @@ const isMeetingLocked = (id) => {
|
||||
|| lockSettings.disablePublicChat
|
||||
|| lockSettings.disableNotes
|
||||
|| lockSettings.hideUserList
|
||||
|| lockSettings.hideViewersCursor
|
||||
|| usersProp.webcamsOnlyForModerator) {
|
||||
isLocked = true;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
this.mirrorOwnWebcam = VideoService.mirrorOwnWebcam(props.userId);
|
||||
const handleSetVideoIsReady = () => {
|
||||
setVideoIsReady(true);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
this.setVideoIsReady = this.setVideoIsReady.bind(this);
|
||||
this.onFullscreenChange = this.onFullscreenChange.bind(this);
|
||||
this.onStreamStateChange = this.onStreamStateChange.bind(this);
|
||||
}
|
||||
/* 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);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { onVideoItemMount, cameraId } = this.props;
|
||||
// component did mount
|
||||
useEffect(() => {
|
||||
onVideoItemMount(videoTag.current);
|
||||
subscribeToStreamStateChange(cameraId, onStreamStateChange);
|
||||
videoTag.current.addEventListener('loadeddata', handleSetVideoIsReady);
|
||||
|
||||
onVideoItemMount(this.videoTag);
|
||||
this.videoTag.addEventListener('loadeddata', this.setVideoIsReady);
|
||||
this.videoContainer.addEventListener(FULLSCREEN_CHANGE_EVENT, this.onFullscreenChange);
|
||||
subscribeToStreamStateChange(cameraId, this.onStreamStateChange);
|
||||
}
|
||||
return () => {
|
||||
videoTag.current.removeEventListener('loadeddata', handleSetVideoIsReady);
|
||||
};
|
||||
}, []);
|
||||
|
||||
componentDidUpdate() {
|
||||
// component will mount
|
||||
useEffect(() => {
|
||||
const playElement = (elem) => {
|
||||
if (elem.paused) {
|
||||
elem.play().catch((error) => {
|
||||
@ -100,197 +79,58 @@ 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
|
||||
ref={videoContainer}
|
||||
talking={talking}
|
||||
fullscreen={isFullscreenContext}
|
||||
data-test={talking ? 'webcamItemTalkingUser' : 'webcamItem'}
|
||||
animations={animations}
|
||||
>
|
||||
{
|
||||
!videoIsReady
|
||||
&& (
|
||||
videoIsReady
|
||||
? (
|
||||
<>
|
||||
<Styled.TopBar>
|
||||
<PinArea
|
||||
user={user}
|
||||
/>
|
||||
<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}
|
||||
@ -299,69 +139,28 @@ class VideoListItem extends Component {
|
||||
<Styled.LoadingText>{name}</Styled.LoadingText>
|
||||
</Styled.WebcamConnecting>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
shouldRenderReconnect
|
||||
&& <Styled.Reconnecting />
|
||||
}
|
||||
|
||||
<Styled.VideoContainer ref={(ref) => { this.videoContainer = ref; }}>
|
||||
<Styled.VideoContainer>
|
||||
<Styled.Video
|
||||
muted
|
||||
data-test={this.mirrorOwnWebcam ? 'mirroredVideoContainer' : 'videoContainer'}
|
||||
mirrored={isMirrored}
|
||||
unhealthyStream={shouldRenderReconnect}
|
||||
ref={(ref) => { this.videoTag = ref; }}
|
||||
ref={videoTag}
|
||||
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' },
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{shouldRenderReconnect && <Styled.Reconnecting />}
|
||||
</Styled.Content>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(VideoListItem);
|
||||
|
||||
VideoListItem.defaultProps = {
|
||||
numOfStreams: 0,
|
||||
user: null,
|
||||
};
|
||||
|
||||
VideoListItem.propTypes = {
|
||||
@ -372,6 +171,10 @@ 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,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FullscreenWrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default {
|
||||
FullscreenWrapper,
|
||||
};
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
31
bigbluebutton-tests/playwright/package-lock.json
generated
31
bigbluebutton-tests/playwright/package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
10597
bigbluebutton-tests/puppeteer/package-lock.json
generated
10597
bigbluebutton-tests/puppeteer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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; \
|
||||
|
@ -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"
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user