Merge pull request #7101 from jfsiebel/network-indicator
Implement audio quality bars
This commit is contained in:
commit
236c62700e
@ -13,6 +13,7 @@ import clearPresentationPods from '/imports/api/presentation-pods/server/modifie
|
|||||||
import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
|
import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
|
||||||
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
|
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
|
||||||
import clearNote from '/imports/api/note/server/modifiers/clearNote';
|
import clearNote from '/imports/api/note/server/modifiers/clearNote';
|
||||||
|
import clearNetworkInformation from '/imports/api/network-information/server/modifiers/clearNetworkInformation';
|
||||||
|
|
||||||
export default function meetingHasEnded(meetingId) {
|
export default function meetingHasEnded(meetingId) {
|
||||||
return Meetings.remove({ meetingId }, () => {
|
return Meetings.remove({ meetingId }, () => {
|
||||||
@ -28,6 +29,7 @@ export default function meetingHasEnded(meetingId) {
|
|||||||
clearVoiceUsers(meetingId);
|
clearVoiceUsers(meetingId);
|
||||||
clearUserInfo(meetingId);
|
clearUserInfo(meetingId);
|
||||||
clearNote(meetingId);
|
clearNote(meetingId);
|
||||||
|
clearNetworkInformation(meetingId);
|
||||||
|
|
||||||
return Logger.info(`Cleared Meetings with id ${meetingId}`);
|
return Logger.info(`Cleared Meetings with id ${meetingId}`);
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
|
||||||
|
const NetworkInformation = new Mongo.Collection('network-information');
|
||||||
|
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
NetworkInformation._ensureIndex({ meetingId: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NetworkInformation;
|
@ -0,0 +1,2 @@
|
|||||||
|
import './methods';
|
||||||
|
import './publisher';
|
@ -0,0 +1,6 @@
|
|||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import userInstabilityDetected from './methods/userInstabilityDetected';
|
||||||
|
|
||||||
|
Meteor.methods({
|
||||||
|
userInstabilityDetected,
|
||||||
|
});
|
@ -0,0 +1,22 @@
|
|||||||
|
import { check } from 'meteor/check';
|
||||||
|
import NetworkInformation from '/imports/api/network-information';
|
||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
|
||||||
|
export default function userInstabilityDetected(credentials, sender) {
|
||||||
|
const { meetingId, requesterUserId: receiver } = credentials;
|
||||||
|
|
||||||
|
check(meetingId, String);
|
||||||
|
check(receiver, String);
|
||||||
|
check(sender, String);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
time: new Date().getTime(),
|
||||||
|
meetingId,
|
||||||
|
receiver,
|
||||||
|
sender,
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.debug(`Receiver ${receiver} reported a network instability in meeting ${meetingId}`);
|
||||||
|
|
||||||
|
return NetworkInformation.insert(payload);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import NetworkInformation from '/imports/api/network-information';
|
||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
|
||||||
|
export default function clearNetworkInformation(meetingId) {
|
||||||
|
if (meetingId) {
|
||||||
|
return NetworkInformation.remove({ meetingId }, () => {
|
||||||
|
Logger.info(`Cleared Network Information (${meetingId})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NetworkInformation.remove({}, () => {
|
||||||
|
Logger.info('Cleared Network Information (all)');
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import NetworkInformation from '/imports/api/network-information';
|
||||||
|
|
||||||
|
function networkInformation(credentials) {
|
||||||
|
const { meetingId } = credentials;
|
||||||
|
|
||||||
|
check(meetingId, String);
|
||||||
|
|
||||||
|
return NetworkInformation.find({
|
||||||
|
meetingId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function publish(...args) {
|
||||||
|
const boundNetworkInformation = networkInformation.bind(this);
|
||||||
|
|
||||||
|
return boundNetworkInformation(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Meteor.publish('network-information', publish);
|
@ -5,7 +5,9 @@ import assignPresenter from './methods/assignPresenter';
|
|||||||
import changeRole from './methods/changeRole';
|
import changeRole from './methods/changeRole';
|
||||||
import removeUser from './methods/removeUser';
|
import removeUser from './methods/removeUser';
|
||||||
import toggleUserLock from './methods/toggleUserLock';
|
import toggleUserLock from './methods/toggleUserLock';
|
||||||
|
import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
|
||||||
import userActivitySign from './methods/userActivitySign';
|
import userActivitySign from './methods/userActivitySign';
|
||||||
|
import userChangedSettings from './methods/userChangedSettings';
|
||||||
import userLeftMeeting from './methods/userLeftMeeting';
|
import userLeftMeeting from './methods/userLeftMeeting';
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
@ -15,6 +17,8 @@ Meteor.methods({
|
|||||||
removeUser,
|
removeUser,
|
||||||
validateAuthToken,
|
validateAuthToken,
|
||||||
toggleUserLock,
|
toggleUserLock,
|
||||||
|
setUserEffectiveConnectionType,
|
||||||
userActivitySign,
|
userActivitySign,
|
||||||
|
userChangedSettings,
|
||||||
userLeftMeeting,
|
userLeftMeeting,
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import RedisPubSub from '/imports/startup/server/redis';
|
||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
import setEffectiveConnectionType from '../modifiers/setUserEffectiveConnectionType';
|
||||||
|
|
||||||
|
export default function setUserEffectiveConnectionType(credentials, effectiveConnectionType) {
|
||||||
|
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||||
|
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||||
|
const EVENT_NAME = 'ChangeUserEffectiveConnectionMsg';
|
||||||
|
|
||||||
|
const { meetingId, requesterUserId } = credentials;
|
||||||
|
|
||||||
|
check(meetingId, String);
|
||||||
|
check(requesterUserId, String);
|
||||||
|
check(effectiveConnectionType, String);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
meetingId,
|
||||||
|
userId: requesterUserId,
|
||||||
|
effectiveConnectionType,
|
||||||
|
};
|
||||||
|
|
||||||
|
setEffectiveConnectionType(meetingId, requesterUserId, effectiveConnectionType);
|
||||||
|
|
||||||
|
Logger.verbose(`User ${requesterUserId} effective connection updated to ${effectiveConnectionType}`);
|
||||||
|
|
||||||
|
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import RedisPubSub from '/imports/startup/server/redis';
|
||||||
|
import setChangedSettings from '../modifiers/setChangedSettings';
|
||||||
|
|
||||||
|
export default function userChangedSettings(credentials, setting, value) {
|
||||||
|
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||||
|
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||||
|
const EVENT_NAME = 'UserChangedSettingsEvtMsg';
|
||||||
|
|
||||||
|
const { meetingId, requesterUserId } = credentials;
|
||||||
|
|
||||||
|
if (!meetingId || !requesterUserId) return;
|
||||||
|
|
||||||
|
check(meetingId, String);
|
||||||
|
check(requesterUserId, String);
|
||||||
|
check(setting, String);
|
||||||
|
check(value, Match.Any);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
meetingId,
|
||||||
|
requesterUserId,
|
||||||
|
setting,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
|
||||||
|
setChangedSettings(requesterUserId, setting, value);
|
||||||
|
|
||||||
|
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||||
|
}
|
@ -80,6 +80,7 @@ export default function addUser(meetingId, user) {
|
|||||||
isBreakoutUser: Meeting.meetingProp.isBreakout,
|
isBreakoutUser: Meeting.meetingProp.isBreakout,
|
||||||
parentId: Meeting.breakoutProps.parentId,
|
parentId: Meeting.breakoutProps.parentId,
|
||||||
},
|
},
|
||||||
|
effectiveConnectionType: null,
|
||||||
inactivityCheck: false,
|
inactivityCheck: false,
|
||||||
responseDelay: 0,
|
responseDelay: 0,
|
||||||
loggedOut: false,
|
loggedOut: false,
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { check } from 'meteor/check';
|
||||||
|
import Users from '/imports/api/users';
|
||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
|
||||||
|
export default function setChangedSettings(userId, setting, value) {
|
||||||
|
check(userId, String);
|
||||||
|
check(setting, String);
|
||||||
|
check(value, Match.Any);
|
||||||
|
|
||||||
|
const selector = {
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modifier = {
|
||||||
|
$set: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
modifier.$set[setting] = value;
|
||||||
|
|
||||||
|
const cb = (err, numChanged) => {
|
||||||
|
if (err) {
|
||||||
|
Logger.error(`${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numChanged) {
|
||||||
|
Logger.info(`Updated setting ${setting} to ${value} for user ${userId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Users.update(selector, modifier, cb);
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import { check } from 'meteor/check';
|
||||||
|
import Users from '/imports/api/users';
|
||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
|
||||||
|
export default function setUserEffectiveConnectionType(meetingId, userId, effectiveConnectionType) {
|
||||||
|
check(meetingId, String);
|
||||||
|
check(userId, String);
|
||||||
|
check(effectiveConnectionType, String);
|
||||||
|
|
||||||
|
const selector = {
|
||||||
|
meetingId,
|
||||||
|
userId,
|
||||||
|
effectiveConnectionType: { $ne: effectiveConnectionType },
|
||||||
|
};
|
||||||
|
|
||||||
|
const modifier = {
|
||||||
|
$set: {
|
||||||
|
effectiveConnectionType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const cb = (err, numChanged) => {
|
||||||
|
if (err) {
|
||||||
|
Logger.error(`Updating user ${userId}: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numChanged) {
|
||||||
|
Logger.info(`Updated user ${userId} effective connection to ${effectiveConnectionType} in meeting ${meetingId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Users.update(selector, modifier, cb);
|
||||||
|
}
|
@ -16,6 +16,7 @@ import AudioContainer from '../audio/container';
|
|||||||
import ChatAlertContainer from '../chat/alert/container';
|
import ChatAlertContainer from '../chat/alert/container';
|
||||||
import BannerBarContainer from '/imports/ui/components/banner-bar/container';
|
import BannerBarContainer from '/imports/ui/components/banner-bar/container';
|
||||||
import WaitingNotifierContainer from '/imports/ui/components/waiting-users/alert/container';
|
import WaitingNotifierContainer from '/imports/ui/components/waiting-users/alert/container';
|
||||||
|
import { startBandwidthMonitoring, updateNavigatorConnection } from '/imports/ui/services/network-information/index';
|
||||||
import LockNotifier from '/imports/ui/components/lock-viewers/notify/container';
|
import LockNotifier from '/imports/ui/components/lock-viewers/notify/container';
|
||||||
|
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
@ -24,6 +25,7 @@ const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
|||||||
const APP_CONFIG = Meteor.settings.public.app;
|
const APP_CONFIG = Meteor.settings.public.app;
|
||||||
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
|
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
|
||||||
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
|
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
|
||||||
|
const ENABLE_NETWORK_INFORMATION = APP_CONFIG.enableNetworkInformation;
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
userListLabel: {
|
userListLabel: {
|
||||||
@ -110,11 +112,22 @@ class App extends Component {
|
|||||||
this.handleWindowResize();
|
this.handleWindowResize();
|
||||||
window.addEventListener('resize', this.handleWindowResize, false);
|
window.addEventListener('resize', this.handleWindowResize, false);
|
||||||
|
|
||||||
|
if (ENABLE_NETWORK_INFORMATION) {
|
||||||
|
if (navigator.connection) {
|
||||||
|
this.handleNetworkConnection();
|
||||||
|
navigator.connection.addEventListener('change', this.handleNetworkConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
startBandwidthMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
logger.info({ logCode: 'app_component_componentdidmount' }, 'Client loaded successfully');
|
logger.info({ logCode: 'app_component_componentdidmount' }, 'Client loaded successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('resize', this.handleWindowResize, false);
|
window.removeEventListener('resize', this.handleWindowResize, false);
|
||||||
|
navigator.connection.addEventListener('change', this.handleNetworkConnection, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowResize() {
|
handleWindowResize() {
|
||||||
@ -125,6 +138,10 @@ class App extends Component {
|
|||||||
this.setState({ enableResize: shouldEnableResize });
|
this.setState({ enableResize: shouldEnableResize });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNetworkConnection() {
|
||||||
|
updateNavigatorConnection(navigator.connection);
|
||||||
|
}
|
||||||
|
|
||||||
renderPanel() {
|
renderPanel() {
|
||||||
const { enableResize } = this.state;
|
const { enableResize } = this.state;
|
||||||
const { openPanel } = this.props;
|
const { openPanel } = this.props;
|
||||||
|
@ -5,8 +5,10 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Auth from '/imports/ui/services/auth';
|
import Auth from '/imports/ui/services/auth';
|
||||||
import Meetings from '/imports/api/meetings';
|
import Meetings from '/imports/api/meetings';
|
||||||
import NavBarService from '../nav-bar/service';
|
import Users from '/imports/api/users';
|
||||||
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container';
|
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container';
|
||||||
|
import SlowConnection from '/imports/ui/components/slow-connection/component';
|
||||||
|
import NavBarService from '../nav-bar/service';
|
||||||
|
|
||||||
import NotificationsBar from './component';
|
import NotificationsBar from './component';
|
||||||
|
|
||||||
@ -19,6 +21,14 @@ const STATUS_FAILED = 'failed';
|
|||||||
// failed to connect and waiting to try to reconnect
|
// failed to connect and waiting to try to reconnect
|
||||||
const STATUS_WAITING = 'waiting';
|
const STATUS_WAITING = 'waiting';
|
||||||
|
|
||||||
|
const METEOR_SETTINGS_APP = Meteor.settings.public.app;
|
||||||
|
|
||||||
|
// https://github.com/bigbluebutton/bigbluebutton/issues/5286#issuecomment-465342716
|
||||||
|
const SLOW_CONNECTIONS_TYPES = METEOR_SETTINGS_APP.effectiveConnection;
|
||||||
|
const ENABLE_NETWORK_INFORMATION = METEOR_SETTINGS_APP.enableNetworkInformation;
|
||||||
|
|
||||||
|
const HELP_LINK = METEOR_SETTINGS_APP.helpLink;
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
failedMessage: {
|
failedMessage: {
|
||||||
id: 'app.failedMessage',
|
id: 'app.failedMessage',
|
||||||
@ -60,6 +70,14 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.meeting.alertBreakoutEndsUnderOneMinute',
|
id: 'app.meeting.alertBreakoutEndsUnderOneMinute',
|
||||||
description: 'Alert that tells that the breakout end under a minute',
|
description: 'Alert that tells that the breakout end under a minute',
|
||||||
},
|
},
|
||||||
|
slowEffectiveConnectionDetected: {
|
||||||
|
id: 'app.network.connection.effective.slow',
|
||||||
|
description: 'Alert for detected slow connections',
|
||||||
|
},
|
||||||
|
slowEffectiveConnectionHelpLink: {
|
||||||
|
id: 'app.network.connection.effective.slow.help',
|
||||||
|
description: 'Help link for slow connections',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsBarContainer = (props) => {
|
const NotificationsBarContainer = (props) => {
|
||||||
@ -104,6 +122,22 @@ export default injectIntl(withTracker(({ intl }) => {
|
|||||||
const { status, connected, retryTime } = Meteor.status();
|
const { status, connected, retryTime } = Meteor.status();
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
|
const user = Users.findOne({ userId: Auth.userID });
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
const { effectiveConnectionType } = user;
|
||||||
|
if (ENABLE_NETWORK_INFORMATION && SLOW_CONNECTIONS_TYPES.includes(effectiveConnectionType)) {
|
||||||
|
data.message = (
|
||||||
|
<SlowConnection effectiveConnectionType={effectiveConnectionType}>
|
||||||
|
{intl.formatMessage(intlMessages.slowEffectiveConnectionDetected)}
|
||||||
|
<a href={HELP_LINK} target="_blank" rel="noopener noreferrer">
|
||||||
|
{intl.formatMessage(intlMessages.slowEffectiveConnectionHelpLink)}
|
||||||
|
</a>
|
||||||
|
</SlowConnection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
data.color = 'primary';
|
data.color = 'primary';
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { styles } from './styles';
|
||||||
|
|
||||||
|
const SLOW_CONNECTIONS_TYPES = {
|
||||||
|
critical: {
|
||||||
|
level: styles.bad,
|
||||||
|
bars: styles.oneBar,
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
level: styles.bad,
|
||||||
|
bars: styles.twoBars,
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
level: styles.warning,
|
||||||
|
bars: styles.threeBars,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SlowConnection = ({ children, effectiveConnectionType, iconOnly }) => (
|
||||||
|
<Fragment>
|
||||||
|
<div className={cx(styles.signalBars, styles.sizingBox, SLOW_CONNECTIONS_TYPES[effectiveConnectionType].level, SLOW_CONNECTIONS_TYPES[effectiveConnectionType].bars)}>
|
||||||
|
<div className={cx(styles.firstBar, styles.bar)} />
|
||||||
|
<div className={cx(styles.secondBar, styles.bar)} />
|
||||||
|
<div className={cx(styles.thirdBar, styles.bar)} />
|
||||||
|
<div className={cx(styles.fourthBar, styles.bar)} />
|
||||||
|
</div>
|
||||||
|
{!iconOnly ? (<span>{children}</span>) : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SlowConnection;
|
@ -0,0 +1,34 @@
|
|||||||
|
.sizingBox {
|
||||||
|
width: 27px;
|
||||||
|
height: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signalBars {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signalBars .bar {
|
||||||
|
width: 4px;
|
||||||
|
margin-left: 1px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.signalBars .bar.firstBar { height: 25%; }
|
||||||
|
.signalBars .bar.secondBar { height: 50%; }
|
||||||
|
.signalBars .bar.thirdBar { height: 75%; }
|
||||||
|
.signalBars .bar.fourthBar { height: 100%; }
|
||||||
|
|
||||||
|
.bad .bar {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
}
|
||||||
|
.warning .bar {
|
||||||
|
background-color: #f1c40f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fourBars .bar.fifthBar,
|
||||||
|
.threeBars .bar.fifthBar,
|
||||||
|
.threeBars .bar.fourthBar,
|
||||||
|
.oneBar .bar:not(.firstBar),
|
||||||
|
.twoBars .bar:not(.firstBar):not(.secondBar) {
|
||||||
|
background-color: #c3c3c3;
|
||||||
|
}
|
@ -16,6 +16,7 @@ const SUBSCRIPTIONS = [
|
|||||||
'users', 'meetings', 'polls', 'presentations', 'slides', 'captions',
|
'users', 'meetings', 'polls', 'presentations', 'slides', 'captions',
|
||||||
'voiceUsers', 'whiteboard-multi-user', 'screenshare', 'group-chat',
|
'voiceUsers', 'whiteboard-multi-user', 'screenshare', 'group-chat',
|
||||||
'presentation-pods', 'users-settings', 'guestUser', 'users-infos', 'note',
|
'presentation-pods', 'users-settings', 'guestUser', 'users-infos', 'note',
|
||||||
|
'network-information',
|
||||||
];
|
];
|
||||||
|
|
||||||
class Subscriptions extends Component {
|
class Subscriptions extends Component {
|
||||||
|
@ -490,6 +490,7 @@ class UserDropdown extends PureComponent {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
compact,
|
compact,
|
||||||
|
currentUser,
|
||||||
user,
|
user,
|
||||||
intl,
|
intl,
|
||||||
isMeetingLocked,
|
isMeetingLocked,
|
||||||
@ -508,6 +509,8 @@ class UserDropdown extends PureComponent {
|
|||||||
|
|
||||||
const userItemContentsStyle = {};
|
const userItemContentsStyle = {};
|
||||||
|
|
||||||
|
const { isModerator } = currentUser;
|
||||||
|
|
||||||
userItemContentsStyle[styles.dropdown] = true;
|
userItemContentsStyle[styles.dropdown] = true;
|
||||||
userItemContentsStyle[styles.userListItem] = !isActionsOpen;
|
userItemContentsStyle[styles.userListItem] = !isActionsOpen;
|
||||||
userItemContentsStyle[styles.usertListItemWithMenu] = isActionsOpen;
|
userItemContentsStyle[styles.usertListItemWithMenu] = isActionsOpen;
|
||||||
@ -551,7 +554,7 @@ class UserDropdown extends PureComponent {
|
|||||||
{<UserIcons
|
{<UserIcons
|
||||||
{...{
|
{...{
|
||||||
user,
|
user,
|
||||||
compact,
|
isModerator,
|
||||||
}}
|
}}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Icon from '/imports/ui/components/icon/component';
|
import SlowConnection from '/imports/ui/components/slow-connection/component';
|
||||||
|
import Auth from '/imports/ui/services/auth';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
|
|
||||||
|
const METEOR_SETTINGS_APP = Meteor.settings.public.app;
|
||||||
|
|
||||||
|
const SLOW_CONNECTIONS_TYPES = METEOR_SETTINGS_APP.effectiveConnection;
|
||||||
|
const ENABLE_NETWORK_INFORMATION = METEOR_SETTINGS_APP.enableNetworkInformation;
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
isModerator: PropTypes.bool.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
isPresenter: PropTypes.bool.isRequired,
|
isPresenter: PropTypes.bool.isRequired,
|
||||||
@ -11,31 +18,33 @@ const propTypes = {
|
|||||||
isModerator: PropTypes.bool.isRequired,
|
isModerator: PropTypes.bool.isRequired,
|
||||||
image: PropTypes.string,
|
image: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
compact: PropTypes.bool.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserIcons = (props) => {
|
const UserIcons = (props) => {
|
||||||
const {
|
const {
|
||||||
user,
|
isModerator,
|
||||||
compact,
|
user: {
|
||||||
|
effectiveConnectionType,
|
||||||
|
id,
|
||||||
|
},
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (compact || user.isSharingWebcam) {
|
const showNetworkInformation = ENABLE_NETWORK_INFORMATION
|
||||||
return null;
|
&& SLOW_CONNECTIONS_TYPES.includes(effectiveConnectionType)
|
||||||
}
|
&& (id === Auth.userID || isModerator);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.userIcons}>
|
<div className={styles.userIcons}>
|
||||||
{
|
{
|
||||||
user.isSharingWebcam ?
|
showNetworkInformation ? (
|
||||||
<span className={styles.userIconsContainer}>
|
<span className={styles.userIconsContainer}>
|
||||||
<Icon iconName="video" />
|
<SlowConnection effectiveConnectionType={effectiveConnectionType} iconOnly />
|
||||||
</span>
|
</span>
|
||||||
: null
|
) : null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
UserIcons.propTypes = propTypes;
|
UserIcons.propTypes = propTypes;
|
||||||
export default UserIcons;
|
export default memo(UserIcons);
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 0 0 0 var(--sm-padding-x)/2;
|
margin: 0 0 0 var(--sm-padding-x)/2;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 1rem;
|
font-size: 1.25rem;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
color: var(--user-icons-color);
|
color: var(--user-icons-color);
|
||||||
|
|
||||||
|
@ -7,13 +7,21 @@ import { fetchWebRTCMappedStunTurnServers } from '/imports/utils/fetchStunTurnSe
|
|||||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
import ReconnectingWebSocket from 'reconnecting-websocket';
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
import browser from 'browser-detect';
|
import browser from 'browser-detect';
|
||||||
|
import {
|
||||||
|
updateCurrentWebcamsConnection,
|
||||||
|
getCurrentWebcams,
|
||||||
|
deleteWebcamConnection,
|
||||||
|
newWebcamConnection,
|
||||||
|
updateWebcamStats,
|
||||||
|
} from '/imports/ui/services/network-information/index';
|
||||||
import { tryGenerateIceCandidates } from '../../../utils/safari-webrtc';
|
import { tryGenerateIceCandidates } from '../../../utils/safari-webrtc';
|
||||||
import Auth from '/imports/ui/services/auth';
|
import Auth from '/imports/ui/services/auth';
|
||||||
|
|
||||||
import VideoService from './service';
|
import VideoService from './service';
|
||||||
import VideoList from './video-list/component';
|
import VideoList from './video-list/component';
|
||||||
|
|
||||||
// const VIDEO_CONSTRAINTS = Meteor.settings.public.kurento.cameraConstraints;
|
const APP_CONFIG = Meteor.settings.public.app;
|
||||||
|
const ENABLE_NETWORK_INFORMATION = APP_CONFIG.enableNetworkInformation;
|
||||||
const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles;
|
const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles;
|
||||||
|
|
||||||
const intlClientErrors = defineMessages({
|
const intlClientErrors = defineMessages({
|
||||||
@ -158,6 +166,30 @@ class VideoProvider extends Component {
|
|||||||
|
|
||||||
this.visibility.onVisible(this.unpauseViewers);
|
this.visibility.onVisible(this.unpauseViewers);
|
||||||
this.visibility.onHidden(this.pauseViewers);
|
this.visibility.onHidden(this.pauseViewers);
|
||||||
|
|
||||||
|
if (ENABLE_NETWORK_INFORMATION) {
|
||||||
|
this.currentWebcamsStatsInterval = setInterval(() => {
|
||||||
|
const currentWebcams = getCurrentWebcams();
|
||||||
|
if (!currentWebcams) return;
|
||||||
|
|
||||||
|
const { payload } = currentWebcams;
|
||||||
|
|
||||||
|
payload.forEach((id) => {
|
||||||
|
const peer = this.webRtcPeers[id];
|
||||||
|
|
||||||
|
const hasLocalStream = peer && peer.started === true
|
||||||
|
&& peer.peerConnection.getLocalStreams().length > 0;
|
||||||
|
const hasRemoteStream = peer && peer.started === true
|
||||||
|
&& peer.peerConnection.getRemoteStreams().length > 0;
|
||||||
|
|
||||||
|
if (hasLocalStream) {
|
||||||
|
this.customGetStats(peer.peerConnection, peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0], (stats => updateWebcamStats(id, stats)), true);
|
||||||
|
} else if (hasRemoteStream) {
|
||||||
|
this.customGetStats(peer.peerConnection, peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], (stats => updateWebcamStats(id, stats)), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUpdate({ users, userId }) {
|
componentWillUpdate({ users, userId }) {
|
||||||
@ -196,6 +228,8 @@ class VideoProvider extends Component {
|
|||||||
this.stopWebRTCPeer(id);
|
this.stopWebRTCPeer(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearInterval(this.currentWebcamsStatsInterval);
|
||||||
|
|
||||||
// Close websocket connection to prevent multiple reconnects from happening
|
// Close websocket connection to prevent multiple reconnects from happening
|
||||||
this.ws.close();
|
this.ws.close();
|
||||||
}
|
}
|
||||||
@ -441,6 +475,10 @@ class VideoProvider extends Component {
|
|||||||
webRtcPeer.dispose();
|
webRtcPeer.dispose();
|
||||||
}
|
}
|
||||||
delete this.webRtcPeers[id];
|
delete this.webRtcPeers[id];
|
||||||
|
if (ENABLE_NETWORK_INFORMATION) {
|
||||||
|
deleteWebcamConnection(id);
|
||||||
|
updateCurrentWebcamsConnection(this.webRtcPeers);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger('warn', 'No WebRTC peer to stop (not an error)', 'video_provider_no_peer_to_destroy', { cameraId: id });
|
this.logger('warn', 'No WebRTC peer to stop (not an error)', 'video_provider_no_peer_to_destroy', { cameraId: id });
|
||||||
}
|
}
|
||||||
@ -527,6 +565,10 @@ class VideoProvider extends Component {
|
|||||||
if (this.webRtcPeers[id].peerConnection) {
|
if (this.webRtcPeers[id].peerConnection) {
|
||||||
this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = this._getOnIceConnectionStateChangeCallback(id);
|
this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = this._getOnIceConnectionStateChangeCallback(id);
|
||||||
}
|
}
|
||||||
|
if (ENABLE_NETWORK_INFORMATION) {
|
||||||
|
newWebcamConnection(id);
|
||||||
|
updateCurrentWebcamsConnection(this.webRtcPeers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -680,7 +722,7 @@ class VideoProvider extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customGetStats(peer, mediaStreamTrack, callback) {
|
customGetStats(peer, mediaStreamTrack, callback, monitoring = false) {
|
||||||
const { stats } = this.state;
|
const { stats } = this.state;
|
||||||
const statsState = stats;
|
const statsState = stats;
|
||||||
let promise;
|
let promise;
|
||||||
@ -735,6 +777,7 @@ class VideoProvider extends Component {
|
|||||||
encodeUsagePercent: videoInOrOutbound.encodeUsagePercent,
|
encodeUsagePercent: videoInOrOutbound.encodeUsagePercent,
|
||||||
rtt: videoInOrOutbound.rtt,
|
rtt: videoInOrOutbound.rtt,
|
||||||
currentDelay: videoInOrOutbound.currentDelay,
|
currentDelay: videoInOrOutbound.currentDelay,
|
||||||
|
pliCount: videoInOrOutbound.pliCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
const videoStatsArray = statsState;
|
const videoStatsArray = statsState;
|
||||||
@ -742,7 +785,10 @@ class VideoProvider extends Component {
|
|||||||
while (videoStatsArray.length > 5) { // maximum interval to consider
|
while (videoStatsArray.length > 5) { // maximum interval to consider
|
||||||
videoStatsArray.shift();
|
videoStatsArray.shift();
|
||||||
}
|
}
|
||||||
this.setState({ stats: videoStatsArray });
|
|
||||||
|
if (!monitoring) {
|
||||||
|
this.setState({ stats: videoStatsArray });
|
||||||
|
}
|
||||||
|
|
||||||
const firstVideoStats = videoStatsArray[0];
|
const firstVideoStats = videoStatsArray[0];
|
||||||
const lastVideoStats = videoStatsArray[videoStatsArray.length - 1];
|
const lastVideoStats = videoStatsArray[videoStatsArray.length - 1];
|
||||||
@ -767,9 +813,9 @@ class VideoProvider extends Component {
|
|||||||
|
|
||||||
let videoBitrate;
|
let videoBitrate;
|
||||||
if (videoStats.packetsReceived > 0) { // Remote video
|
if (videoStats.packetsReceived > 0) { // Remote video
|
||||||
videoLostPercentage = ((videoStats.packetsLost / ((videoStats.packetsLost + videoStats.packetsReceived) * 100)) || 0).toFixed(1);
|
videoLostPercentage = ((videoStats.packetsLost / ((videoStats.packetsLost + videoStats.packetsReceived)) * 100) || 0).toFixed(1);
|
||||||
videoBitrate = Math.floor(videoKbitsReceivedPerSecond || 0);
|
videoBitrate = Math.floor(videoKbitsReceivedPerSecond || 0);
|
||||||
videoLostRecentPercentage = ((videoIntervalPacketsLost / ((videoIntervalPacketsLost + videoIntervalPacketsReceived) * 100)) || 0).toFixed(1);
|
videoLostRecentPercentage = ((videoIntervalPacketsLost / ((videoIntervalPacketsLost + videoIntervalPacketsReceived)) * 100) || 0).toFixed(1);
|
||||||
} else {
|
} else {
|
||||||
videoLostPercentage = (((videoStats.packetsLost / (videoStats.packetsLost + videoStats.packetsSent)) * 100) || 0).toFixed(1);
|
videoLostPercentage = (((videoStats.packetsLost / (videoStats.packetsLost + videoStats.packetsSent)) * 100) || 0).toFixed(1);
|
||||||
videoBitrate = Math.floor(videoKbitsSentPerSecond || 0);
|
videoBitrate = Math.floor(videoKbitsSentPerSecond || 0);
|
||||||
@ -793,6 +839,7 @@ class VideoProvider extends Component {
|
|||||||
encodeUsagePercent: videoStats.encodeUsagePercent,
|
encodeUsagePercent: videoStats.encodeUsagePercent,
|
||||||
rtt: videoStats.rtt,
|
rtt: videoStats.rtt,
|
||||||
currentDelay: videoStats.currentDelay,
|
currentDelay: videoStats.currentDelay,
|
||||||
|
pliCount: videoStats.pliCount,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,231 @@
|
|||||||
|
import NetworkInformation from '/imports/api/network-information';
|
||||||
|
import { makeCall } from '/imports/ui/services/api';
|
||||||
|
import Auth from '/imports/ui/services/auth';
|
||||||
|
import Users from '/imports/api/users';
|
||||||
|
import logger from '/imports/startup/client/logger';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const NetworkInformationLocal = new Mongo.Collection(null);
|
||||||
|
|
||||||
|
const NAVIGATOR_CONNECTION = 'NAVIGATOR_CONNECTION';
|
||||||
|
const NUMBER_OF_WEBCAMS_CHANGED = 'NUMBER_OF_WEBCAMS_CHANGED';
|
||||||
|
const STARTED_WEBCAM_SHARING = 'STARTED_WEBCAM_SHARING';
|
||||||
|
const STOPPED_WEBCAM_SHARING = 'STOPPED_WEBCAM_SHARING';
|
||||||
|
const WEBCAMS_GET_STATUS = 'WEBCAMS_GET_STATUS';
|
||||||
|
|
||||||
|
const DANGER_BEGIN_TIME = 5000;
|
||||||
|
const DANGER_END_TIME = 30000;
|
||||||
|
|
||||||
|
const WARNING_END_TIME = 60000;
|
||||||
|
|
||||||
|
let monitoringIntervalRef;
|
||||||
|
|
||||||
|
export const updateCurrentWebcamsConnection = (connections) => {
|
||||||
|
const doc = {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
event: NUMBER_OF_WEBCAMS_CHANGED,
|
||||||
|
payload: Object.keys(connections),
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkInformationLocal.insert(doc);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteWebcamConnection = (id) => {
|
||||||
|
const doc = {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
event: STOPPED_WEBCAM_SHARING,
|
||||||
|
payload: { id },
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkInformationLocal.insert(doc);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurrentWebcams = () => NetworkInformationLocal
|
||||||
|
.findOne({
|
||||||
|
event: NUMBER_OF_WEBCAMS_CHANGED,
|
||||||
|
}, { sort: { timestamp: -1 } });
|
||||||
|
|
||||||
|
export const newWebcamConnection = (id) => {
|
||||||
|
const doc = {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
event: STARTED_WEBCAM_SHARING,
|
||||||
|
payload: { id },
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkInformationLocal.insert(doc);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startBandwidthMonitoring = () => {
|
||||||
|
monitoringIntervalRef = setInterval(() => {
|
||||||
|
const monitoringTime = new Date().getTime();
|
||||||
|
|
||||||
|
const dangerLowerBoundary = monitoringTime - DANGER_BEGIN_TIME;
|
||||||
|
|
||||||
|
const warningLowerBoundary = monitoringTime - DANGER_END_TIME;
|
||||||
|
const warningUpperBoundary = monitoringTime - WARNING_END_TIME;
|
||||||
|
|
||||||
|
// Remove old documents to reduce the size of the local collection.
|
||||||
|
NetworkInformationLocal.remove({
|
||||||
|
event: WEBCAMS_GET_STATUS,
|
||||||
|
timestamp: { $lt: warningUpperBoundary },
|
||||||
|
});
|
||||||
|
|
||||||
|
const usersWatchingWebcams = Users.find({
|
||||||
|
userId: { $ne: Auth.userID },
|
||||||
|
viewParticipantsWebcams: true,
|
||||||
|
connectionStatus: 'online',
|
||||||
|
}).map(user => user.userId);
|
||||||
|
|
||||||
|
const warningZone = NetworkInformationLocal
|
||||||
|
.find({
|
||||||
|
event: WEBCAMS_GET_STATUS,
|
||||||
|
timestamp: { $lte: warningLowerBoundary, $gt: warningUpperBoundary },
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
'payload.id': Auth.userID,
|
||||||
|
'payload.stats.deltaPliCount': { $gt: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'payload.id': { $ne: Auth.userID },
|
||||||
|
'payload.stats.deltaPacketsLost': { $gt: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).count();
|
||||||
|
|
||||||
|
const warningZoneReceivers = NetworkInformation
|
||||||
|
.find({
|
||||||
|
receiver: { $in: usersWatchingWebcams },
|
||||||
|
sender: Auth.userID,
|
||||||
|
time: { $lte: warningLowerBoundary, $gt: warningUpperBoundary },
|
||||||
|
}).count();
|
||||||
|
|
||||||
|
const dangerZone = _.uniqBy(NetworkInformationLocal
|
||||||
|
.find({
|
||||||
|
event: WEBCAMS_GET_STATUS,
|
||||||
|
timestamp: { $lt: dangerLowerBoundary, $gte: warningLowerBoundary },
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
'payload.id': Auth.userID,
|
||||||
|
'payload.stats.deltaPliCount': { $gt: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'payload.id': { $ne: Auth.userID },
|
||||||
|
'payload.stats.deltaPacketsLost': { $gt: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).fetch(), 'payload.id').length;
|
||||||
|
|
||||||
|
const dangerZoneReceivers = _.uniqBy(NetworkInformation
|
||||||
|
.find({
|
||||||
|
receiver: { $in: usersWatchingWebcams },
|
||||||
|
sender: Auth.userID,
|
||||||
|
time: { $lt: dangerLowerBoundary, $gte: warningLowerBoundary },
|
||||||
|
}).fetch(), 'receiver').length;
|
||||||
|
|
||||||
|
let effectiveType = 'good';
|
||||||
|
|
||||||
|
if (dangerZone) {
|
||||||
|
if (!dangerZoneReceivers) {
|
||||||
|
effectiveType = 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dangerZoneReceivers === usersWatchingWebcams.length) {
|
||||||
|
effectiveType = 'danger';
|
||||||
|
}
|
||||||
|
} else if (warningZone) {
|
||||||
|
if (!warningZoneReceivers) {
|
||||||
|
effectiveType = 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warningZoneReceivers === usersWatchingWebcams.length) {
|
||||||
|
effectiveType = 'warning';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastEffectiveConnectionType = Users.findOne({ userId: Auth.userID });
|
||||||
|
|
||||||
|
if (lastEffectiveConnectionType
|
||||||
|
&& lastEffectiveConnectionType.effectiveConnectionType !== effectiveType) {
|
||||||
|
logger.info({ logCode: 'user_connection_instability' }, `User ${Auth.userID} effective connection is now ${effectiveType}`);
|
||||||
|
makeCall('setUserEffectiveConnectionType', effectiveType);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stopBandwidthMonitoring = () => {
|
||||||
|
clearInterval(monitoringIntervalRef);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateNavigatorConnection = ({ effectiveType, downlink, rtt }) => {
|
||||||
|
const doc = {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
event: NAVIGATOR_CONNECTION,
|
||||||
|
payload: {
|
||||||
|
effectiveType,
|
||||||
|
downlink,
|
||||||
|
rtt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkInformationLocal.insert(doc);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateWebcamStats = (id, stats) => {
|
||||||
|
if (!stats) return;
|
||||||
|
|
||||||
|
const lastStatus = NetworkInformationLocal
|
||||||
|
.findOne(
|
||||||
|
{ event: WEBCAMS_GET_STATUS, 'payload.id': id },
|
||||||
|
{ sort: { timestamp: -1 } },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { video } = stats;
|
||||||
|
|
||||||
|
const doc = {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
event: WEBCAMS_GET_STATUS,
|
||||||
|
payload: { id, stats: video },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastStatus) {
|
||||||
|
const {
|
||||||
|
payload: {
|
||||||
|
stats: {
|
||||||
|
packetsLost,
|
||||||
|
packetsReceived,
|
||||||
|
packetsSent,
|
||||||
|
pliCount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = lastStatus;
|
||||||
|
const normalizedVideo = { ...video };
|
||||||
|
|
||||||
|
normalizedVideo.deltaPacketsLost = video.packetsLost - packetsLost;
|
||||||
|
normalizedVideo.deltaPacketsReceived = video.packetsReceived - packetsReceived;
|
||||||
|
normalizedVideo.deltaPacketsSent = video.packetsSent - packetsSent;
|
||||||
|
normalizedVideo.deltaPliCount = video.pliCount - pliCount;
|
||||||
|
|
||||||
|
doc.payload = {
|
||||||
|
id,
|
||||||
|
stats: normalizedVideo,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (normalizedVideo.deltaPacketsLost > 0) {
|
||||||
|
makeCall('userInstabilityDetected', id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkInformationLocal.insert(doc);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
NetworkInformationLocal,
|
||||||
|
updateCurrentWebcamsConnection,
|
||||||
|
deleteWebcamConnection,
|
||||||
|
getCurrentWebcams,
|
||||||
|
newWebcamConnection,
|
||||||
|
startBandwidthMonitoring,
|
||||||
|
stopBandwidthMonitoring,
|
||||||
|
updateNavigatorConnection,
|
||||||
|
updateWebcamStats,
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import Storage from '/imports/ui/services/storage/session';
|
import Storage from '/imports/ui/services/storage/session';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { makeCall } from '/imports/ui/services/api';
|
||||||
|
|
||||||
const SETTINGS = [
|
const SETTINGS = [
|
||||||
'application',
|
'application',
|
||||||
@ -33,9 +34,9 @@ class Settings {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Sets default locale to browser locale
|
// Sets default locale to browser locale
|
||||||
defaultValues.application.locale = navigator.languages ? navigator.languages[0] : false ||
|
defaultValues.application.locale = navigator.languages ? navigator.languages[0] : false
|
||||||
navigator.language ||
|
|| navigator.language
|
||||||
defaultValues.application.locale;
|
|| defaultValues.application.locale;
|
||||||
|
|
||||||
this.setDefault(defaultValues);
|
this.setDefault(defaultValues);
|
||||||
}
|
}
|
||||||
@ -55,7 +56,15 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
Object.keys(this).forEach(k => Storage.setItem(`settings${k}`, this[k].value));
|
Object.keys(this).forEach((k) => {
|
||||||
|
if (k === '_dataSaving') {
|
||||||
|
const { value: { viewParticipantsWebcams } } = this[k];
|
||||||
|
|
||||||
|
makeCall('userChangedSettings', 'viewParticipantsWebcams', viewParticipantsWebcams);
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage.setItem(`settings${k}`, this[k].value);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ const mapUser = (user) => {
|
|||||||
isOnline: user.connectionStatus === 'online',
|
isOnline: user.connectionStatus === 'online',
|
||||||
clientType: user.clientType,
|
clientType: user.clientType,
|
||||||
loginTime: user.loginTime,
|
loginTime: user.loginTime,
|
||||||
|
effectiveConnectionType: user.effectiveConnectionType,
|
||||||
externalUserId: user.extId,
|
externalUserId: user.extId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ public:
|
|||||||
basename: "/html5client"
|
basename: "/html5client"
|
||||||
askForFeedbackOnLogout: false
|
askForFeedbackOnLogout: false
|
||||||
allowUserLookup: false
|
allowUserLookup: false
|
||||||
|
enableNetworkInformation: false
|
||||||
defaultSettings:
|
defaultSettings:
|
||||||
application:
|
application:
|
||||||
animations: true
|
animations: true
|
||||||
@ -83,6 +84,10 @@ public:
|
|||||||
connectionTimeout: 60000
|
connectionTimeout: 60000
|
||||||
showHelpButton: true
|
showHelpButton: true
|
||||||
enableExternalVideo: true
|
enableExternalVideo: true
|
||||||
|
effectiveConnection:
|
||||||
|
- critical
|
||||||
|
- danger
|
||||||
|
- warning
|
||||||
kurento:
|
kurento:
|
||||||
wsUrl: HOST
|
wsUrl: HOST
|
||||||
chromeDefaultExtensionKey: akgoaoikmbmhcopjgakkcepdgdgkjfbc
|
chromeDefaultExtensionKey: akgoaoikmbmhcopjgakkcepdgdgkjfbc
|
||||||
|
@ -610,6 +610,8 @@
|
|||||||
"app.externalVideo.urlInput": "Add YouTube URL",
|
"app.externalVideo.urlInput": "Add YouTube URL",
|
||||||
"app.externalVideo.urlError": "This URL isn't a valid YouTube video",
|
"app.externalVideo.urlError": "This URL isn't a valid YouTube video",
|
||||||
"app.externalVideo.close": "Close",
|
"app.externalVideo.close": "Close",
|
||||||
|
"app.network.connection.effective.slow": "We're noticing connectivity issues.",
|
||||||
|
"app.network.connection.effective.slow.help": "How to fix it?",
|
||||||
"app.externalVideo.noteLabel": "Note: Shared YouTube videos will not appear in the recording",
|
"app.externalVideo.noteLabel": "Note: Shared YouTube videos will not appear in the recording",
|
||||||
"app.actionsBar.actionsDropdown.shareExternalVideo": "Share YouTube video",
|
"app.actionsBar.actionsDropdown.shareExternalVideo": "Share YouTube video",
|
||||||
"app.actionsBar.actionsDropdown.stopShareExternalVideo": "Stop sharing YouTube video",
|
"app.actionsBar.actionsDropdown.stopShareExternalVideo": "Stop sharing YouTube video",
|
||||||
|
@ -19,6 +19,7 @@ import '/imports/api/users-settings/server';
|
|||||||
import '/imports/api/voice-users/server';
|
import '/imports/api/voice-users/server';
|
||||||
import '/imports/api/whiteboard-multi-user/server';
|
import '/imports/api/whiteboard-multi-user/server';
|
||||||
import '/imports/api/video/server';
|
import '/imports/api/video/server';
|
||||||
|
import '/imports/api/network-information/server';
|
||||||
import '/imports/api/users-infos/server';
|
import '/imports/api/users-infos/server';
|
||||||
import '/imports/api/note/server';
|
import '/imports/api/note/server';
|
||||||
import '/imports/api/external-videos/server';
|
import '/imports/api/external-videos/server';
|
||||||
|
Loading…
Reference in New Issue
Block a user