Merge branch 'v2.6.x-release' into presentation-test-impr

This commit is contained in:
Anton B 2023-04-10 15:50:49 -03:00
commit 659c4030dd
133 changed files with 2025 additions and 574 deletions

View File

@ -1 +1 @@
git clone --branch v2.9.8 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
git clone --branch v2.9.10 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -21,6 +21,7 @@ const SFU_URL = Meteor.settings.public.kurento.wsUrl;
const DEFAULT_LISTENONLY_MEDIA_SERVER = Meteor.settings.public.kurento.listenOnlyMediaServer;
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
const TRACE_LOGS = Meteor.settings.public.kurento.traceLogs;
const GATHERING_TIMEOUT = Meteor.settings.public.kurento.gatheringTimeout;
const MEDIA = Meteor.settings.public.media;
const DEFAULT_FULLAUDIO_MEDIA_SERVER = MEDIA.audio.fullAudioMediaServer;
const LISTEN_ONLY_OFFERING = MEDIA.listenOnlyOffering;
@ -265,6 +266,7 @@ export default class SFUAudioBridge extends BaseAudioBridge {
traceLogs: TRACE_LOGS,
networkPriority: NETWORK_PRIORITY,
mediaStreamFactory: this.mediaStreamFactory,
gatheringTimeout: GATHERING_TIMEOUT,
};
this.broker = new AudioBroker(
@ -352,6 +354,7 @@ export default class SFUAudioBridge extends BaseAudioBridge {
iceServers,
offering: LISTEN_ONLY_OFFERING,
traceLogs: TRACE_LOGS,
gatheringTimeout: GATHERING_TIMEOUT,
};
this.broker = new AudioBroker(

View File

@ -6,6 +6,7 @@ import { setSharingScreen, screenShareEndAlert } from '/imports/ui/components/sc
import { SCREENSHARING_ERRORS } from './errors';
import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils';
import MediaStreamUtils from '/imports/utils/media-stream-utils';
import { notifyStreamStateChange } from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
const SFU_CONFIG = Meteor.settings.public.kurento;
const SFU_URL = SFU_CONFIG.wsUrl;
@ -13,6 +14,7 @@ const OFFERING = SFU_CONFIG.screenshare.subscriberOffering;
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
const TRACE_LOGS = Meteor.settings.public.kurento.traceLogs;
const { screenshare: NETWORK_PRIORITY } = Meteor.settings.public.media.networkPriorities || {};
const GATHERING_TIMEOUT = Meteor.settings.public.kurento.gatheringTimeout;
const BRIDGE_NAME = 'kurento'
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
@ -51,6 +53,7 @@ export default class KurentoScreenshareBridge {
this.reconnecting = false;
this.reconnectionTimeout;
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
this.startedOnce = false;
}
get gdmStream() {
@ -63,7 +66,7 @@ export default class KurentoScreenshareBridge {
_shouldReconnect() {
// Sender/presenter reconnect is *not* implemented yet
return this.broker.started && this.role === RECV_ROLE;
return this.reconnectionTimeout == null && this.role === RECV_ROLE;
}
/**
@ -146,9 +149,12 @@ export default class KurentoScreenshareBridge {
return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS;
}
scheduleReconnect(immediate = false) {
scheduleReconnect({
overrideTimeout,
} = { }) {
if (this.reconnectionTimeout == null) {
const nextRestartInterval = immediate ? 0 : this.restartIntervalMs;
let nextRestartInterval = this.restartIntervalMs;
if (typeof overrideTimeout === 'number') nextRestartInterval = overrideTimeout;
this.reconnectionTimeout = setTimeout(
this.handleConnectionTimeoutExpiry.bind(this),
@ -197,6 +203,7 @@ export default class KurentoScreenshareBridge {
BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio);
}
this.startedOnce = true;
this.clearReconnectionTimeout();
this.connectionAttempts = 0;
}
@ -208,21 +215,31 @@ export default class KurentoScreenshareBridge {
logger.error({
logCode: 'screenshare_broker_failure',
extraInfo: {
errorCode, errorMessage,
errorCode,
errorMessage,
role: this.broker.role,
started: this.broker.started,
reconnecting: this.reconnecting,
bridge: BRIDGE_NAME
bridge: BRIDGE_NAME,
},
}, `Screenshare broker failure: ${errorMessage}`);
notifyStreamStateChange('screenshare', 'failed');
// Screensharing was already successfully negotiated and error occurred during
// during call; schedule a reconnect
// If the session has not yet started, a reconnect should already be scheduled
if (this._shouldReconnect()) {
// this.broker.started => whether the reconnect should happen immediately.
// If this session had alredy been established, it should.
this.scheduleReconnect(this.broker.started);
// If this session previously established connection (N-sessions back)
// and it failed abruptly, then the timeout is overridden to a intermediate value
// (BASE_RECONNECTION_TIMEOUT)
let overrideTimeout;
if (this.broker?.started) {
overrideTimeout = 0;
} else if (this.startedOnce) {
overrideTimeout = BridgeService.BASE_RECONNECTION_TIMEOUT;
}
this.scheduleReconnect({ overrideTimeout });
}
return error;
@ -241,6 +258,7 @@ export default class KurentoScreenshareBridge {
signalCandidates: SIGNAL_CANDIDATES,
forceRelay: shouldForceRelay(),
traceLogs: TRACE_LOGS,
gatheringTimeout: GATHERING_TIMEOUT,
};
this.broker = new ScreenshareBroker(
@ -264,6 +282,7 @@ export default class KurentoScreenshareBridge {
logCode: 'screenshare_presenter_start_success',
}, 'Screenshare presenter started succesfully');
this.clearReconnectionTimeout();
this.startedOnce = true;
this.reconnecting = false;
this.connectionAttempts = 0;
}
@ -309,6 +328,7 @@ export default class KurentoScreenshareBridge {
forceRelay: shouldForceRelay(),
traceLogs: TRACE_LOGS,
networkPriority: NETWORK_PRIORITY,
gatheringTimeout: GATHERING_TIMEOUT,
};
this.broker = new ScreenshareBroker(

View File

@ -16,6 +16,7 @@ const {
maxTimeout: MAX_MEDIA_TIMEOUT,
maxConnectionAttempts: MAX_CONN_ATTEMPTS,
timeoutIncreaseFactor: TIMEOUT_INCREASE_FACTOR,
baseReconnectionTimeout: BASE_RECONNECTION_TIMEOUT,
} = MEDIA_TIMEOUTS;
const HAS_DISPLAY_MEDIA = (typeof navigator.getDisplayMedia === 'function'
@ -111,7 +112,7 @@ const getMediaServerAdapter = () => {
const getNextReconnectionInterval = (oldInterval) => {
return Math.min(
TIMEOUT_INCREASE_FACTOR * oldInterval,
(TIMEOUT_INCREASE_FACTOR * Math.max(oldInterval, BASE_RECONNECTION_TIMEOUT)),
MAX_MEDIA_TIMEOUT,
);
}
@ -157,6 +158,7 @@ export default {
screenshareLoadAndPlayMediaStream,
getMediaServerAdapter,
BASE_MEDIA_TIMEOUT,
BASE_RECONNECTION_TIMEOUT,
MAX_CONN_ATTEMPTS,
BASE_BITRATE,
};

View File

@ -34,15 +34,18 @@ export default function zoomSlide(slideNumber, podId, widthRatio, heightRatio, x
throw new Meteor.Error('presentation-not-found', 'You need a presentation to be able to switch slides');
}
let validSlideNum = slideNumber;
if (validSlideNum > Presentation?.pages?.length) validSlideNum = 1;
const Slide = Slides.findOne({
meetingId,
podId,
presentationId: Presentation.id,
num: slideNumber,
num: validSlideNum,
});
if (!Slide) {
throw new Meteor.Error('slide-not-found', `Slide number ${slideNumber} not found in the current presentation`);
throw new Meteor.Error('slide-not-found', `Slide number ${validSlideNum} not found in the current presentation`);
}
const payload = {

View File

@ -246,8 +246,6 @@ class Base extends Component {
}
renderByState() {
const { updateLoadingState } = this;
const stateControls = { updateLoadingState };
const { loading, userRemoved } = this.state;
const {
codeError,
@ -301,7 +299,7 @@ class Base extends Component {
return (<MeetingEnded code={codeError} callback={() => Base.setExitReason('logout')} />);
}
return (<AppContainer {...this.props} baseControls={stateControls} />);
return (<AppContainer {...this.props} />);
}
render() {

View File

@ -125,7 +125,7 @@ const ScreenshareButton = ({
const handleFailure = (error) => {
const {
errorCode = SCREENSHARING_ERRORS.UNKNOWN_ERROR.errorCode,
errorMessage,
errorMessage = error.message,
} = error;
const localizedError = getErrorLocale(errorCode);

View File

@ -1,6 +1,5 @@
import React, { useEffect, useRef } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { defineMessages, injectIntl } from 'react-intl';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import Meetings, { LayoutMeetings } from '/imports/api/meetings';
@ -38,13 +37,6 @@ import App from './component';
const CUSTOM_STYLE_URL = Meteor.settings.public.app.customStyleUrl;
const intlMessages = defineMessages({
waitingApprovalMessage: {
id: 'app.guest.waiting',
description: 'Message while a guest is waiting to be approved',
},
});
const endMeeting = (code, ejectedReason) => {
Session.set('codeError', code);
Session.set('errorMessageDescription', ejectedReason);
@ -204,7 +196,7 @@ const currentUserEmoji = (currentUser) => (currentUser
}
);
export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => {
export default withModalMounter(withTracker(() => {
Users.find({ userId: Auth.userID, meetingId: Auth.meetingID }).observe({
removed(userData) {
// wait 3secs (before endMeeting), client will try to authenticate again
@ -260,10 +252,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
presentationVideoRate: meetingLayoutVideoRate,
} = meetingLayoutObj;
if (currentUser && !currentUser.approved) {
baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage));
}
const UserInfo = UserInfos.find({
meetingId: Auth.meetingID,
requesterUserId: Auth.userID,
@ -330,4 +318,4 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
isModalOpen: !!getModal(),
ignorePollNotifications: Session.get('ignorePollNotifications'),
};
})(AppContainer)));
})(AppContainer));

View File

@ -11,6 +11,8 @@ const {
let audioContext = null;
let sourceContext = null;
let contextDestination = null;
let stubAudioElement = null;
let delayNode = null;
const useRTCLoopback = () => (browserInfo.isChrome || browserInfo.isEdge) && USE_RTC_LOOPBACK_CHR;
@ -31,20 +33,45 @@ const cleanupDelayNode = () => {
audioContext.close();
audioContext = null;
}
if (contextDestination) {
contextDestination.disconnect();
contextDestination = null;
}
if (stubAudioElement) {
stubAudioElement.pause();
stubAudioElement.srcObject = null;
stubAudioElement = null;
}
};
const addDelayNode = (stream) => {
if (stream) {
if (delayNode || audioContext || sourceContext) cleanupDelayNode();
const audioElement = document.querySelector(MEDIA_TAG);
// Workaround: attach the stream to a muted stub audio element to be able to play it in
// Chromium-based browsers. See https://bugs.chromium.org/p/chromium/issues/detail?id=933677
stubAudioElement = new Audio();
stubAudioElement.muted = true;
stubAudioElement.srcObject = stream;
// Create a new AudioContext to be able to add a delay to the stream
audioContext = new AudioContext();
sourceContext = audioContext.createMediaStreamSource(stream);
contextDestination = audioContext.createMediaStreamDestination();
// Create a DelayNode to add a delay to the stream
delayNode = new DelayNode(audioContext, { delayTime, maxDelayTime });
// Connect the stream to the DelayNode and then to the MediaStreamDestinationNode
// to be able to play the stream.
sourceContext.connect(delayNode);
delayNode.connect(audioContext.destination);
delayNode.connect(contextDestination);
delayNode.delayTime.setValueAtTime(delayTime, audioContext.currentTime);
// Play the stream with the delay in the default audio element (remote-media)
audioElement.srcObject = contextDestination.stream;
}
};
const deattachEchoStream = () => {
const audioElement = document.querySelector(MEDIA_TAG);
@ -59,11 +86,12 @@ const deattachEchoStream = () => {
const playEchoStream = async (stream, loopbackAgent = null) => {
if (stream) {
const audioElement = document.querySelector(MEDIA_TAG);
deattachEchoStream();
let streamToPlay = stream;
if (loopbackAgent) {
// Chromium based browsers need audio to go through PCs for echo cancellation
// to work. See https://bugs.chromium.org/p/chromium/issues/detail?id=687574
try {
await loopbackAgent.start(stream);
streamToPlay = loopbackAgent.loopbackStream;
@ -73,13 +101,16 @@ const playEchoStream = async (stream, loopbackAgent = null) => {
}
if (DELAY_ENABLED) {
// Start muted to avoid weird artifacts and prevent playing the stream twice (Chromium)
audioElement.muted = true;
addDelayNode(streamToPlay);
}
} else {
// No delay: play the stream in the default audio element (remote-media),
// no strings attached.
const audioElement = document.querySelector(MEDIA_TAG);
audioElement.srcObject = streamToPlay;
audioElement.muted = false;
audioElement.play();
}
}
};
export default {

View File

@ -10,10 +10,10 @@ import { btnSpacing } from '/imports/ui/stylesheets/styled-components/general';
const EmojiButtonIcon = styled(Icon)`
position: absolute;
top: 0;
height: 60%;
height: 40%;
left: 0;
width: 75%;
margin-left: 15%;
margin-left: 14%;
font-size: 50%;
margin-top: 30%;
color: ${btnDefaultColor};

View File

@ -98,7 +98,7 @@ const FullscreenButtonComponent = ({
label={formattedLabel(isFullscreen)}
hideLabel
isStyled={fullScreenStyle}
data-test="presentationFullscreenButton"
data-test="webcamFullscreenButton"
/>
</Styled.FullscreenButtonWrapper>
);

View File

@ -6,6 +6,7 @@ import Menu from "@material-ui/core/Menu";
import { Divider } from "@material-ui/core";
import Icon from "/imports/ui/components/common/icon/component";
import { SMALL_VIEWPORT_BREAKPOINT } from '/imports/ui/components/layout/enums';
import KEY_CODES from '/imports/utils/keyCodes';
import { ENTER } from "/imports/utils/keyCodes";
@ -32,19 +33,52 @@ class BBBMenu extends React.Component {
this.optsToMerge = {};
this.autoFocus = false;
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleClose = this.handleClose.bind(this);
}
componentDidUpdate() {
const { anchorEl } = this.state;
if (this.props.open === false && anchorEl) {
const { open } = this.props;
if (open === false && anchorEl) {
this.setState({ anchorEl: null });
} else if (this.props.open === true && !anchorEl) {
} else if (open === true && !anchorEl) {
this.setState({ anchorEl: this.anchorElRef });
}
}
handleKeyDown(event) {
const { anchorEl } = this.state;
const isMenuOpen = Boolean(anchorEl);
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.which)) {
this.handleClose();
return;
}
if (isMenuOpen && [KEY_CODES.ARROW_UP, KEY_CODES.ARROW_DOWN].includes(event.which)) {
event.preventDefault();
event.stopPropagation();
const menuItems = Array.from(document.querySelectorAll('[data-key^="menuItem-"]'));
if (menuItems.length === 0) return;
const focusedIndex = menuItems.findIndex(item => item === document.activeElement);
const nextIndex = event.which === KEY_CODES.ARROW_UP ? focusedIndex - 1 : focusedIndex + 1;
let indexToFocus = 0;
if (nextIndex < 0) {
indexToFocus = menuItems.length - 1;
} else if (nextIndex >= menuItems.length) {
indexToFocus = 0;
} else {
indexToFocus = nextIndex;
}
menuItems[indexToFocus].focus();
}
};
handleClick(event) {
this.setState({ anchorEl: event.currentTarget });
};
@ -90,6 +124,7 @@ class BBBMenu extends React.Component {
emoji={emojiSelected ? 'yes' : 'no'}
key={label}
data-test={dataTest}
data-key={`menuItem-${dataTest}`}
disableRipple={true}
disableGutters={true}
disabled={disabled}
@ -115,7 +150,7 @@ class BBBMenu extends React.Component {
render() {
const { anchorEl } = this.state;
const { trigger, intl, customStyles, dataTest, opts } = this.props;
const { trigger, intl, customStyles, dataTest, opts, accessKey } = this.props;
const actionsItems = this.makeMenuItems();
let menuStyles = { zIndex: 9999 };
@ -140,7 +175,7 @@ class BBBMenu extends React.Component {
if (e.which !== ENTER) return null;
this.handleClick(e);
}}
accessKey={this.props?.accessKey}
accessKey={accessKey}
ref={(ref) => this.anchorElRef = ref}
>
{trigger}
@ -154,6 +189,7 @@ class BBBMenu extends React.Component {
onClose={this.handleClose}
style={menuStyles}
data-test={dataTest}
onKeyDownCapture={this.handleKeyDown}
>
{actionsItems}
{anchorEl && window.innerWidth < SMALL_VIEWPORT_BREAKPOINT &&
@ -204,6 +240,7 @@ BBBMenu.propTypes = {
divider: PropTypes.bool,
dividerTop: PropTypes.bool,
accessKey: PropTypes.string,
dataTest: PropTypes.string,
})).isRequired,
onCloseCallback: PropTypes.func,

View File

@ -9,3 +9,9 @@
fill: #333333;
background-color: transparent;
}
.tippy-tooltip.bbbtip-theme>.tippy-content{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -452,7 +452,7 @@ class ConnectionStatusComponent extends PureComponent {
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${videoUploadLabel}`}</div>
<div>{`${videoCurrentUploadRate}k ↑`}</div>
<div data-test="videoUploadRateData">{`${videoCurrentUploadRate}k ↑`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${intl.formatMessage(intlMessages.jitter)}`}</div>

View File

@ -79,7 +79,7 @@ export const ACTIONS = {
SET_PRESENTATION_IS_OPEN: 'setPresentationIsOpen',
SET_PRESENTATION_CURRENT_SLIDE_SIZE: 'setPresentationCurrentSlideSize',
SET_PRESENTATION_NUM_CURRENT_SLIDE: 'setPresentationNumCurrentSlide',
SET_PRESENTATION_SLIDES_LENGTH: 'setPresentationSlideslength',
SET_PRESENTATION_SLIDES_LENGTH: 'setPresentationSlidesLength',
SET_PRESENTATION_SIZE: 'setPresentationSize',
SET_PRESENTATION_OUTPUT: 'setPresentationOutput',
SET_PRESENTATION_IS_RESIZABLE: 'setPresentationIsResizable',

View File

@ -5,6 +5,7 @@ import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
import { ACTIONS, CAMERADOCK_POSITION, PANELS } from '../enums';
import Storage from '/imports/ui/services/storage/session';
import { isPresentationEnabled } from '/imports/ui/services/features';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
@ -30,6 +31,10 @@ const CustomLayout = (props) => {
const currentPanelType = layoutSelect((i) => i.currentPanelType);
const presentationInput = layoutSelectInput((i) => i.presentation);
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
const screenShareInput = layoutSelectInput((i) => i.screenShare);
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
@ -207,14 +212,21 @@ const CustomLayout = (props) => {
};
const calculatesSidebarContentHeight = (cameraDockHeight) => {
const { isOpen } = presentationInput;
const { isOpen, slidesLength } = presentationInput;
const { hasExternalVideo } = externalVideoInput;
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
let sidebarContentHeight = 0;
if (sidebarContentInput.isOpen) {
if (isMobile) {
sidebarContentHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
} else if (cameraDockInput.numCameras > 0
&& cameraDockInput.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM
&& isOpen) {
&& isOpen && !isGeneralMediaOff) {
sidebarContentHeight = windowHeight() - cameraDockHeight;
} else {
sidebarContentHeight = windowHeight();
@ -361,7 +373,7 @@ const CustomLayout = (props) => {
);
}
cameraDockBounds.top = windowHeight() - cameraDockHeight;
cameraDockBounds.top = windowHeight() - cameraDockHeight - bannerAreaHeight();
cameraDockBounds.left = !isRTL ? sidebarNavWidth : 0;
cameraDockBounds.right = isRTL ? sidebarNavWidth : 0;
cameraDockBounds.minWidth = sidebarContentWidth;
@ -375,7 +387,11 @@ const CustomLayout = (props) => {
};
const calculatesMediaBounds = (sidebarNavWidth, sidebarContentWidth, cameraDockBounds) => {
const { isOpen } = presentationInput;
const { isOpen, slidesLength } = presentationInput;
const { hasExternalVideo } = externalVideoInput;
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const { height: actionBarHeight } = calculatesActionbarHeight();
const mediaAreaHeight = windowHeight()
- (DEFAULT_VALUES.navBarHeight + actionBarHeight + bannerAreaHeight());
@ -384,7 +400,10 @@ const CustomLayout = (props) => {
const { element: fullscreenElement } = fullscreen;
const { navBarHeight, camerasMargin } = DEFAULT_VALUES;
if (!isOpen) {
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
if (!isOpen || isGeneralMediaOff) {
mediaBounds.width = 0;
mediaBounds.height = 0;
mediaBounds.top = 0;

View File

@ -8,6 +8,7 @@ import CustomLayout from '/imports/ui/components/layout/layout-manager/customLay
import SmartLayout from '/imports/ui/components/layout/layout-manager/smartLayout';
import PresentationFocusLayout from '/imports/ui/components/layout/layout-manager/presentationFocusLayout';
import VideoFocusLayout from '/imports/ui/components/layout/layout-manager/videoFocusLayout';
import { isPresentationEnabled } from '/imports/ui/services/features';
const propTypes = {
layoutType: PropTypes.string.isRequired,
@ -23,6 +24,7 @@ const LayoutEngine = ({ layoutType }) => {
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
const screenShareInput = layoutSelectInput((i) => i.screenShare);
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
const fullscreen = layoutSelect((i) => i.fullscreen);
const isRTL = layoutSelect((i) => i.isRTL);
@ -46,10 +48,10 @@ const LayoutEngine = ({ layoutType }) => {
};
const baseCameraDockBounds = (mediaAreaBounds, sidebarSize) => {
const { isOpen, currentSlide } = presentationInput;
const { num: currentSlideNumber } = currentSlide;
const { isOpen, slidesLength } = presentationInput;
const { hasExternalVideo } = externalVideoInput;
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const cameraDockBounds = {};
@ -60,9 +62,10 @@ const LayoutEngine = ({ layoutType }) => {
return cameraDockBounds;
}
const isSmartLayout = (layoutType === LAYOUT_TYPE.SMART_LAYOUT);
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
if (!isOpen || (isSmartLayout && currentSlideNumber === 0 && !hasExternalVideo && !hasScreenShare)) {
if (!isOpen || isGeneralMediaOff) {
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height - bannerAreaHeight();

View File

@ -8,6 +8,7 @@ import {
PANELS,
CAMERADOCK_POSITION,
} from '/imports/ui/components/layout/enums';
import { isPresentationEnabled } from '/imports/ui/services/features';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
@ -33,6 +34,10 @@ const PresentationFocusLayout = (props) => {
const currentPanelType = layoutSelect((i) => i.currentPanelType);
const presentationInput = layoutSelectInput((i) => i.presentation);
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
const screenShareInput = layoutSelectInput((i) => i.screenShare);
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
@ -147,7 +152,14 @@ const PresentationFocusLayout = (props) => {
};
const calculatesSidebarContentHeight = () => {
const { isOpen } = presentationInput;
const { isOpen, slidesLength } = presentationInput;
const { hasExternalVideo } = externalVideoInput;
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const {
navBarHeight,
sidebarContentMinHeight,
@ -160,7 +172,7 @@ const PresentationFocusLayout = (props) => {
height = windowHeight() - navBarHeight - bannerAreaHeight();
minHeight = height;
maxHeight = height;
} else if (cameraDockInput.numCameras > 0 && isOpen) {
} else if (cameraDockInput.numCameras > 0 && isOpen && !isGeneralMediaOff) {
if (sidebarContentInput.height === 0) {
height = (windowHeight() * 0.75) - bannerAreaHeight();
} else {
@ -221,13 +233,15 @@ const PresentationFocusLayout = (props) => {
max((windowHeight() - sidebarContentHeight), cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
);
const bannerAreaDiff = windowHeight() - sidebarContentHeight - cameraDockHeight - bannerAreaHeight();
cameraDockHeight += bannerAreaDiff;
} else {
cameraDockHeight = min(
max(cameraDockInput.height, cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
);
}
cameraDockBounds.top = windowHeight() - cameraDockHeight;
cameraDockBounds.top = windowHeight() - cameraDockHeight - bannerAreaHeight();
cameraDockBounds.left = !isRTL ? sidebarNavWidth : 0;
cameraDockBounds.right = isRTL ? sidebarNavWidth : 0;
cameraDockBounds.minWidth = sidebarContentWidth;

View File

@ -261,15 +261,18 @@ const SmartLayout = (props) => {
}
const calculatesMediaBounds = (mediaAreaBounds, slideSize, sidebarSize, screenShareSize) => {
const { isOpen, currentSlide } = presentationInput;
const { isOpen, slidesLength } = presentationInput;
const { hasExternalVideo } = externalVideoInput;
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
const { num: currentSlideNumber } = currentSlide;
if (!isOpen || ((isPresentationEnabled() && currentSlideNumber === 0) && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned)) {
if (!isOpen || isGeneralMediaOff) {
mediaBounds.width = 0;
mediaBounds.height = 0;
mediaBounds.top = 0;

View File

@ -9,6 +9,7 @@ import {
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
import { isPresentationEnabled } from '/imports/ui/services/features';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
@ -32,6 +33,10 @@ const VideoFocusLayout = (props) => {
const currentPanelType = layoutSelect((i) => i.currentPanelType);
const presentationInput = layoutSelectInput((i) => i.presentation);
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
const screenShareInput = layoutSelectInput((i) => i.screenShare);
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
@ -154,6 +159,14 @@ const VideoFocusLayout = (props) => {
};
const calculatesSidebarContentHeight = () => {
const { isOpen, slidesLength } = presentationInput;
const { hasExternalVideo } = externalVideoInput;
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
let minHeight = 0;
let height = 0;
let maxHeight = 0;
@ -162,7 +175,7 @@ const VideoFocusLayout = (props) => {
height = windowHeight() - DEFAULT_VALUES.navBarHeight - bannerAreaHeight();
minHeight = height;
maxHeight = height;
} else if (cameraDockInput.numCameras > 0 && presentationInput.isOpen) {
} else if (cameraDockInput.numCameras > 0 && isOpen && !isGeneralMediaOff) {
if (sidebarContentInput.height > 0 && sidebarContentInput.height < windowHeight()) {
height = sidebarContentInput.height - bannerAreaHeight();
} else {

View File

@ -4,7 +4,6 @@ import { defineMessages, injectIntl } from 'react-intl';
import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import SettingsService from '/imports/ui/components/settings/service';
import getFromUserSettings from '/imports/ui/services/users-settings';
import deviceInfo from '/imports/utils/deviceInfo';
import Toggle from '/imports/ui/components/common/switch/component';
import Button from '/imports/ui/components/common/button/component';
@ -26,10 +25,8 @@ const LayoutModalComponent = (props) => {
const [isKeepPushingLayout, setIsKeepPushingLayout] = useState(application.pushLayout);
const BASE_NAME = Meteor.settings.public.app.basename;
const CUSTOM_STYLE_URL = Boolean(Meteor.settings.public.app.customStyleUrl);
const customStyleUrl = Boolean(getFromUserSettings('bbb_custom_style_url', CUSTOM_STYLE_URL));
const LAYOUTS_PATH = `${BASE_NAME}/resources/images/layouts/${customStyleUrl ? 'customStyle/' : ''}`;
const LAYOUTS_PATH = `${BASE_NAME}/resources/images/layouts/`;
const isKeepPushingLayoutEnabled = SettingsService.isKeepPushingLayoutEnabled();
const intlMessages = defineMessages({

View File

@ -134,7 +134,7 @@ class Presentation extends PureComponent {
window.addEventListener('resize', this.onResize, false);
const {
currentSlide, slidePosition, layoutContextDispatch,
currentSlide, slidePosition, numPages, layoutContextDispatch,
} = this.props;
if (currentSlide) {
@ -149,6 +149,10 @@ class Presentation extends PureComponent {
height: slidePosition.height,
},
});
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_SLIDES_LENGTH,
value: numPages,
})
}
}
@ -167,6 +171,7 @@ class Presentation extends PureComponent {
numCameras,
intl,
multiUser,
numPages,
} = this.props;
const {
@ -186,6 +191,13 @@ class Presentation extends PureComponent {
this.onResize();
}
if (numPages !== prevProps.numPages) {
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_SLIDES_LENGTH,
value: numPages,
})
}
if (
currentSlide?.num != null
&& prevProps?.currentSlide?.num != null
@ -802,6 +814,7 @@ Presentation.propTypes = {
name: PropTypes.string.isRequired,
}),
presentationIsOpen: PropTypes.bool.isRequired,
numPages: PropTypes.number.isRequired,
publishedPoll: PropTypes.bool.isRequired,
presentationBounds: PropTypes.shape({
top: PropTypes.number,

View File

@ -71,6 +71,7 @@ const fetchedpresentation = {};
export default lockContextContainer(
withTracker(({ podId, presentationIsOpen, userLocks }) => {
const currentSlide = PresentationService.getCurrentSlide(podId);
const numPages = PresentationService.getSlidesLength(podId);
const presentationIsDownloadable = PresentationService.isPresentationDownloadable(podId);
const isViewersCursorLocked = userLocks?.hideViewersCursor;
@ -127,6 +128,7 @@ export default lockContextContainer(
presentationIsDownloadable,
mountPresentation: !!currentSlide,
currentPresentation: PresentationService.getCurrentPresentation(podId),
numPages,
notify,
zoomSlide: PresentationToolbarService.zoomSlide,
podId,

View File

@ -57,6 +57,14 @@ const intlMessages = defineMessages({
description: 'used for aria whiteboard options button label',
defaultMessage: 'Whiteboard',
},
hideToolsDesc: {
id: 'app.presentation.presentationToolbar.hideToolsDesc',
description: 'Hide toolbar label',
},
showToolsDesc: {
id: 'app.presentation.presentationToolbar.showToolsDesc',
description: 'Show toolbar label',
},
});
const propTypes = {
@ -128,6 +136,11 @@ const PresentationMenu = (props) => {
: intl.formatMessage(intlMessages.fullscreenLabel)
);
const formattedVisibilityLabel = (visible) => (visible
? intl.formatMessage(intlMessages.hideToolsDesc)
: intl.formatMessage(intlMessages.showToolsDesc)
);
function renderToastContent() {
const { loading, hasError } = state;
@ -256,6 +269,36 @@ const PresentationMenu = (props) => {
);
}
const tools = document.querySelector('#TD-Tools');
if (tools && (props.hasWBAccess || props.amIPresenter)){
const isVisible = tools.style.visibility == 'hidden' ? false : true;
const styles = document.querySelector('#TD-Styles').parentElement;
const option = document.querySelector('#WhiteboardOptionButton');
if (option) {
//When the RTL-LTR changed, the toolbar appears again,
// while the opacity of this button remains the same.
//So we need to reset the opacity here.
option.style.opacity = isVisible ? 'unset' : '0.2';
}
menuItems.push(
{
key: 'list-item-toolvisibility',
dataTest: 'toolVisibility',
label: formattedVisibilityLabel(isVisible),
icon: isVisible ? 'close' : 'pen_tool',
onClick: () => {
tools.style.visibility = isVisible ? 'hidden' : 'visible';
if (styles) {
styles.style.visibility = isVisible ? 'hidden' : 'visible';
}
if (option) {
option.style.opacity = isVisible ? '0.2' : 'unset';
}
},
},
);
}
return menuItems;
}
@ -294,7 +337,7 @@ const PresentationMenu = (props) => {
}
return (
<Styled.Right>
<Styled.Right id='WhiteboardOptionButton'>
<BBBMenu
trigger={(
<TooltipContainer title={intl.formatMessage(intlMessages.optionsLabel)}>

View File

@ -6,6 +6,8 @@ import FullscreenService from '/imports/ui/components/common/fullscreen-button/s
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings';
import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/context';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
import UserService from '/imports/ui/components/user-list/service';
const PresentationMenuContainer = (props) => {
const fullscreen = layoutSelect((i) => i.fullscreen);
@ -34,6 +36,8 @@ export default withTracker((props) => {
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
const meetingId = Auth.meetingID;
const meetingObject = Meetings.findOne({ meetingId }, { fields: { 'meetingProp.name': 1 } });
const hasWBAccess = WhiteboardService.hasMultiUserAccess(WhiteboardService.getCurrentWhiteboardId(), Auth.userID);
const amIPresenter = UserService.isUserPresenter(Auth.userID);
return {
...props,
@ -41,6 +45,8 @@ export default withTracker((props) => {
isIphone,
isDropdownOpen: Session.get('dropdownOpen'),
meetingName: meetingObject.meetingProp.name,
hasWBAccess,
amIPresenter,
};
})(PresentationMenuContainer);

View File

@ -56,6 +56,10 @@ const getCurrentSlide = (podId) => {
});
};
const getSlidesLength = (podId) => {
return getCurrentPresentation(podId)?.pages?.length || 0;
}
const getSlidePosition = (podId, presentationId, slideId) => SlidePositions.findOne({
podId,
presentationId,
@ -222,4 +226,5 @@ export default {
currentSlidHasContent,
parseCurrentSlideContent,
getCurrentPresentation,
getSlidesLength,
};

View File

@ -242,9 +242,16 @@ class ScreenshareComponent extends React.Component {
try {
mediaFlowing = isMediaFlowing(previousStats, currentStats);
} catch (_error) {
} catch (error) {
// Stats processing failed for whatever reason - maintain previous state
mediaFlowing = prevMediaFlowing;
logger.warn({
logCode: 'screenshare_media_monitor_stats_failed',
extraInfo: {
errorName: error.name,
errorMessage: error.message,
},
}, 'Failed to collect screenshare stats, flow monitor');
}
previousStats = currentStats;
@ -323,9 +330,7 @@ class ScreenshareComponent extends React.Component {
this.clearMediaFlowingMonitor();
// Current state is media not flowing - stream is now healthy so flip it
if (!mediaFlowing) this.setState({ mediaFlowing: isStreamHealthy });
} else {
if (this.mediaFlowMonitor == null) this.monitorMediaFlow();
}
} else if (this.mediaFlowMonitor == null) this.monitorMediaFlow();
}
renderFullscreenButton() {

View File

@ -245,8 +245,8 @@ const getStats = async (statsTypes = DEFAULT_SCREENSHARE_STATS_TYPES) => {
// This method may throw errors
const isMediaFlowing = (previousStats, currentStats) => {
const bpsData = ConnectionStatusService.calculateBitsPerSecond(
currentStats.screenshareStats,
previousStats.screenshareStats,
currentStats?.screenshareStats,
previousStats?.screenshareStats,
);
const bpsDataAggr = Object.values(bpsData)
.reduce((sum, partialBpsData = 0) => sum + parseFloat(partialBpsData), 0);

View File

@ -322,8 +322,30 @@ const getActiveChats = ({ groupChatsMessages, groupChats, users }) => {
});
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
return chatInfo.filter((chat) => !currentClosedChats.includes(chat.chatId)
const removeClosedChats = chatInfo.filter((chat) => !currentClosedChats.includes(chat.chatId)
&& chat.shouldDisplayInChatList);
const sortByChatIdAndUnread = removeClosedChats.sort((a, b) => {
if (a.chatId === PUBLIC_GROUP_CHAT_ID) {
return -1;
}
if (b.chatId === PUBLIC_CHAT_ID) {
return 0;
}
if (a.unreadCounter > b.unreadCounter) {
return -1;
} else if (b.unreadCounter > a.unreadCounter) {
return 1;
} else {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
return 1;
}
return 0;
}
});
return sortByChatIdAndUnread;
};
const isVoiceOnlyUser = (userId) => userId.toString().startsWith('v_');

View File

@ -305,6 +305,7 @@ const VirtualBgSelector = ({
disabled={disabled}
isVisualEffects={isVisualEffects}
background={getVirtualBackgroundThumbnail(imageName)}
data-test="selectDefaultBackground"
/>
<div aria-hidden className="sr-only" id={`vr-cam-btn-${index + 1}`}>
{intl.formatMessage(intlMessages.camBgAriaDesc, { 0: label })}
@ -343,6 +344,7 @@ const VirtualBgSelector = ({
disabled={disabled}
isVisualEffects={isVisualEffects}
background={data}
data-test="selectCustomBackground"
/>
<Styled.ButtonWrapper>
<Styled.ButtonRemove
@ -387,6 +389,7 @@ const VirtualBgSelector = ({
}
}}
isVisualEffects={isVisualEffects}
data-test="inputBackgroundButton"
/>
<input
ref={customBgSelectorRef}
@ -414,6 +417,7 @@ const VirtualBgSelector = ({
disabled={disabled}
onClick={() => _virtualBgSelected(EFFECT_TYPES.NONE_TYPE)}
isVisualEffects={isVisualEffects}
data-test="noneBackgroundButton"
/>
<div aria-hidden className="sr-only" id={`vr-cam-btn-none`}>
{intl.formatMessage(intlMessages.camBgAriaDesc, { 0: EFFECT_TYPES.NONE_TYPE })}

View File

@ -25,7 +25,16 @@ import WebRtcPeer from '/imports/ui/services/webrtc-base/peer';
// Default values and default empty object to be backwards compat with 2.2.
// FIXME Remove hardcoded defaults 2.3.
const WS_CONN_TIMEOUT = Meteor.settings.public.kurento.wsConnectionTimeout || 4000;
const {
connectionTimeout: WS_CONN_TIMEOUT = 4000,
maxRetries: WS_MAX_RETRIES = 5,
debug: WS_DEBUG,
heartbeat: WS_HEARTBEAT_OPTS = {
interval: 15000,
delay: 3000,
reconnectOnFailure: true,
},
} = Meteor.settings.public.kurento.cameraWsOptions;
const { webcam: NETWORK_PRIORITY } = Meteor.settings.public.media.networkPriorities || {};
const {
@ -36,9 +45,9 @@ const {
enabled: CAMERA_QUALITY_THRESHOLDS_ENABLED = true,
privilegedStreams: CAMERA_QUALITY_THR_PRIVILEGED = true,
} = Meteor.settings.public.kurento.cameraQualityThresholds;
const PING_INTERVAL = 15000;
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
const TRACE_LOGS = Meteor.settings.public.kurento.traceLogs;
const GATHERING_TIMEOUT = Meteor.settings.public.kurento.gatheringTimeout;
const intlClientErrors = defineMessages({
permissionError: {
@ -113,6 +122,7 @@ const propTypes = {
swapLayout: PropTypes.bool.isRequired,
currentVideoPageIndex: PropTypes.number.isRequired,
totalNumberOfStreams: PropTypes.number.isRequired,
isMeteorConnected: PropTypes.bool.isRequired,
};
class VideoProvider extends Component {
@ -120,6 +130,23 @@ class VideoProvider extends Component {
VideoService.onBeforeUnload();
}
static isAbleToAttach(peer) {
// Conditions to safely attach a stream to a video element in all browsers:
// 1 - Peer exists
// 2 - It hasn't been attached yet
// 3a - If the stream is a remote one, the safest (*ahem* Safari) moment to
// do so is waiting for the server to confirm that media has flown out of it
// towards te remote end (peer.started)
// 3b - If the stream is a local one (webcam sharer) and is started
// 4 - If the stream is local one, check if there area video tracks there are
// video tracks: attach it
if (peer == null || peer.attached) return false;
if (peer.started) return true;
return peer.isPublisher
&& peer.getLocalStream()
&& peer.getLocalStream().getVideoTracks().length > 0;
}
constructor(props) {
super(props);
@ -128,16 +155,9 @@ class VideoProvider extends Component {
socketOpen: false,
};
this._isMounted = false;
this.info = VideoService.getInfo();
// Set a valid bbb-webrtc-sfu application server socket in the settings
this.ws = new ReconnectingWebSocket(
VideoService.getAuthenticatedURL(),
[],
{ connectionTimeout: WS_CONN_TIMEOUT },
);
this.wsQueue = [];
// Signaling message queue arrays indexed by stream (== cameraId)
this.wsQueues = {};
this.restartTimeout = {};
this.restartTimer = {};
this.webRtcPeers = {};
@ -161,53 +181,120 @@ class VideoProvider extends Component {
componentDidMount() {
this._isMounted = true;
VideoService.updatePeerDictionaryReference(this.webRtcPeers);
this.ws.onopen = this.onWsOpen;
this.ws.onclose = this.onWsClose;
window.addEventListener('online', this.openWs);
window.addEventListener('offline', this.onWsClose);
this.ws.onmessage = this.onWsMessage;
this.ws = this.openWs();
window.addEventListener('beforeunload', VideoProvider.onBeforeUnload);
}
componentDidUpdate(prevProps) {
const { isUserLocked, streams, currentVideoPageIndex } = this.props;
const {
isUserLocked,
streams,
currentVideoPageIndex,
isMeteorConnected
} = this.props;
const { socketOpen } = this.state;
// Only debounce when page changes to avoid unecessary debouncing
const shouldDebounce = VideoService.isPaginationEnabled()
&& prevProps.currentVideoPageIndex !== currentVideoPageIndex;
this.updateStreams(streams, shouldDebounce);
if (isMeteorConnected && socketOpen) this.updateStreams(streams, shouldDebounce);
if (!prevProps.isUserLocked && isUserLocked) VideoService.lockUser();
// Signaling socket expired its retries and meteor is connected - create
// a new signaling socket instance from scratch
if (!socketOpen
&& isMeteorConnected
&& this.ws == null) {
this.ws = this.openWs();
}
}
componentWillUnmount() {
this._isMounted = false;
VideoService.updatePeerDictionaryReference({});
this.ws.onmessage = null;
this.ws.onopen = null;
this.ws.onclose = null;
window.removeEventListener('online', this.openWs);
window.removeEventListener('offline', this.onWsClose);
window.removeEventListener('beforeunload', VideoProvider.onBeforeUnload);
VideoService.exitVideo();
Object.keys(this.webRtcPeers).forEach((stream) => {
this.stopWebRTCPeer(stream, false);
});
this.terminateWs();
}
// Close websocket connection to prevent multiple reconnects from happening
openWs() {
const ws = new ReconnectingWebSocket(
VideoService.getAuthenticatedURL(), [], {
connectionTimeout: WS_CONN_TIMEOUT,
debug: WS_DEBUG,
maxRetries: WS_MAX_RETRIES,
maxEnqueuedMessages: 0,
}
);
ws.onopen = this.onWsOpen;
ws.onclose = this.onWsClose;
ws.onmessage = this.onWsMessage;
return ws;
}
terminateWs() {
if (this.ws) {
this.clearWSHeartbeat();
this.ws.close();
this._isMounted = false;
this.ws = null;
}
}
_updateLastMsgTime() {
this.ws.isAlive = true;
this.ws.lastMsgTime = Date.now();
}
_getTimeSinceLastMsg() {
return Date.now() - this.ws.lastMsgTime;
}
setupWSHeartbeat() {
if (WS_HEARTBEAT_OPTS.interval === 0 || this.ws == null || this.ws.wsHeartbeat) return;
this.ws.isAlive = true;
this.ws.wsHeartbeat = setInterval(() => {
if (this.ws.isAlive === false) {
logger.warn({
logCode: 'video_provider_ws_heartbeat_failed',
}, 'Video provider WS heartbeat failed.');
if (WS_HEARTBEAT_OPTS.reconnectOnFailure) this.ws.reconnect();
return;
}
if (this._getTimeSinceLastMsg() < (
WS_HEARTBEAT_OPTS.interval - WS_HEARTBEAT_OPTS.delay
)) {
return;
}
this.ws.isAlive = false;
this.ping();
}, WS_HEARTBEAT_OPTS.interval);
this.ping();
}
clearWSHeartbeat() {
if (this.ws?.wsHeartbeat) {
clearInterval(this.ws.wsHeartbeat);
this.ws.wsHeartbeat = null;
}
}
onWsMessage(message) {
this._updateLastMsgTime();
const parsedMessage = JSON.parse(message.data);
if (parsedMessage.id === 'pong') return;
@ -244,11 +331,22 @@ class VideoProvider extends Component {
logCode: 'video_provider_onwsclose',
}, 'Multiple video provider websocket connection closed.');
clearInterval(this.pingInterval);
this.clearWSHeartbeat();
VideoService.exitVideo();
// Media is currently tied to signaling state - so if signaling shuts down,
// media will shut down server-side. This cleans up our local state faster
// and notify the state change as failed so the UI rolls back to the placeholder
// avatar UI in the camera container
Object.keys(this.webRtcPeers).forEach((stream) => {
if (this.stopWebRTCPeer(stream, false)) {
notifyStreamStateChange(stream, 'failed');
}
});
this.setState({ socketOpen: false });
if (this.ws && this.ws.retryCount >= WS_MAX_RETRIES) {
this.terminateWs();
}
}
onWsOpen() {
@ -256,14 +354,21 @@ class VideoProvider extends Component {
logCode: 'video_provider_onwsopen',
}, 'Multiple video provider websocket connection opened.');
// Resend queued messages that happened when socket was not connected
while (this.wsQueue.length > 0) {
this.sendMessage(this.wsQueue.pop());
}
this.pingInterval = setInterval(this.ping.bind(this), PING_INTERVAL);
this._updateLastMsgTime();
this.setupWSHeartbeat();
this.setState({ socketOpen: true });
// Resend queued messages that happened when socket was not connected
Object.entries(this.wsQueues).forEach(([stream, queue]) => {
if (this.webRtcPeers[stream]) {
// Peer - send enqueued
while (queue.length > 0) {
this.sendMessage(queue.pop());
}
} else {
// No peer - delete queue
this.wsQueues[stream] = null;
}
});
}
findAllPrivilegedStreams () {
@ -342,8 +447,9 @@ class VideoProvider extends Component {
if (this.connectedToMediaServer()) {
const jsonMessage = JSON.stringify(message);
ws.send(jsonMessage, (error) => {
if (error) {
try {
ws.send(jsonMessage);
} catch (error) {
logger.error({
logCode: 'video_provider_ws_send_error',
extraInfo: {
@ -352,15 +458,18 @@ class VideoProvider extends Component {
},
}, 'Camera request failed to be sent to SFU');
}
});
} else if (message.id !== 'stop') {
// No need to queue video stop messages
this.wsQueue.push(message);
const { cameraId } = message;
if (cameraId) {
if (this.wsQueues[cameraId] == null) this.wsQueues[cameraId] = [];
this.wsQueues[cameraId].push(message);
}
}
}
connectedToMediaServer() {
return this.ws.readyState === WebSocket.OPEN;
return this.ws && this.ws.readyState === ReconnectingWebSocket.OPEN;
}
processOutboundIceQueue(peer, role, stream) {
@ -494,17 +603,20 @@ class VideoProvider extends Component {
this.clearRestartTimers(stream);
}
this.destroyWebRTCPeer(stream);
return this.destroyWebRTCPeer(stream);
}
destroyWebRTCPeer(stream) {
let stopped = false;
const peer = this.webRtcPeers[stream];
const isLocal = VideoService.isLocalStream(stream);
const role = VideoService.getRole(isLocal);
if (peer) {
if (peer && peer.bbbVideoStream) {
if (typeof peer.inactivationHandler === 'function') {
peer.bbbVideoStream.removeListener('inactive', peer.inactivationHandler);
}
peer.bbbVideoStream.stop();
}
@ -512,14 +624,19 @@ class VideoProvider extends Component {
peer.dispose();
}
delete this.outboundIceQueues[stream];
delete this.webRtcPeers[stream];
stopped = true;
} else {
logger.warn({
logCode: 'video_provider_destroywebrtcpeer_no_peer',
extraInfo: { cameraId: stream, role },
}, 'Trailing camera destroy request.');
}
delete this.outboundIceQueues[stream];
delete this.wsQueues[stream];
return stopped;
}
_createPublisher(stream, peerOptions) {
@ -618,6 +735,7 @@ class VideoProvider extends Component {
},
trace: TRACE_LOGS,
networkPriorities: NETWORK_PRIORITY ? { video: NETWORK_PRIORITY } : undefined,
gatheringTimeout: GATHERING_TIMEOUT,
};
try {
@ -661,10 +779,6 @@ class VideoProvider extends Component {
cameraId: stream,
role,
sdpOffer: offer,
meetingId: this.info.meetingId,
voiceBridge: this.info.voiceBridge,
userId: this.info.userId,
userName: this.info.userName,
bitrate,
record: VideoService.getRecord(),
mediaServer: VideoService.getMediaServerAdapter(),
@ -678,8 +792,8 @@ class VideoProvider extends Component {
},
}, `Camera offer generated. Role: ${role}`);
this.sendMessage(message);
this.setReconnectionTimeout(stream, isLocal, false);
this.sendMessage(message);
return;
}).catch(error => {
@ -738,7 +852,7 @@ class VideoProvider extends Component {
}
_onWebRTCError(error, stream, isLocal) {
const { intl } = this.props;
const { intl, streams } = this.props;
const { name: errorName, message: errorMessage } = error;
const errorLocale = intlClientErrors[errorName]
|| intlClientErrors[errorMessage]
@ -763,11 +877,16 @@ class VideoProvider extends Component {
// If it's a viewer, set the reconnection timeout. There's a good chance
// no local candidate was generated and it wasn't set.
const peer = this.webRtcPeers[stream];
const stillExists = streams.some(({ stream: streamId }) => streamId === stream);
if (stillExists) {
const isEstablishedConnection = peer && peer.started;
this.setReconnectionTimeout(stream, isLocal, isEstablishedConnection);
}
// second argument means it will only try to reconnect if
// it's a viewer instance (see stopWebRTCPeer restarting argument)
this.stopWebRTCPeer(stream, true);
this.stopWebRTCPeer(stream, stillExists);
}
}
@ -917,16 +1036,7 @@ class VideoProvider extends Component {
return; // Skip if the stream is already attached
}
// Conditions to safely attach a stream to a video element in all browsers:
// 1 - Peer exists
// 2 - It hasn't been attached yet
// 3a - If the stream is a local one (webcam sharer), we can just attach it
// (no need to wait for server confirmation)
// 3b - If the stream is a remote one, the safest (*ahem* Safari) moment to
// do so is waiting for the server to confirm that media has flown out of it
// towards the remote end.
const isAbleToAttach = peer && !peer.attached && (peer.started || isLocal);
if (isAbleToAttach) {
if (VideoProvider.isAbleToAttach(peer)) {
this.attach(peer, video);
peer.attached = true;
@ -1075,7 +1185,7 @@ class VideoProvider extends Component {
}
handleSFUError(message) {
const { intl } = this.props;
const { intl, streams } = this.props;
const { code, reason, streamId } = message;
const isLocal = VideoService.isLocalStream(streamId);
const role = VideoService.getRole(isLocal);
@ -1093,15 +1203,22 @@ class VideoProvider extends Component {
if (isLocal) {
// The publisher instance received an error from the server. There's no reconnect,
// stop it.
VideoService.stopVideo(streamId);
VideoService.notify(intl.formatMessage(intlSFUErrors[code] || intlSFUErrors[2200]));
VideoService.stopVideo(streamId);
} else {
this.stopWebRTCPeer(streamId, true);
const peer = this.webRtcPeers[streamId];
const stillExists = streams.some(({ stream }) => streamId === stream);
if (stillExists) {
const isEstablishedConnection = peer && peer.started;
this.setReconnectionTimeout(streamId, isLocal, isEstablishedConnection);
}
this.stopWebRTCPeer(streamId, stillExists);
}
}
replacePCVideoTracks (streamId, mediaStream) {
let replaced = false;
replacePCVideoTracks(streamId, mediaStream) {
const peer = this.webRtcPeers[streamId];
const videoElement = this.getVideoElement(streamId);
@ -1111,26 +1228,24 @@ class VideoProvider extends Component {
const newTracks = mediaStream.getVideoTracks();
if (pc) {
try {
pc.getSenders().forEach((sender, index) => {
if (sender.track && sender.track.kind === 'video') {
const trackReplacers = pc.getSenders().map(async (sender, index) => {
if (sender.track == null || sender.track.kind !== 'video') return false;
const newTrack = newTracks[index];
if (newTrack == null) return;
sender.replaceTrack(newTrack);
replaced = true;
}
});
if (newTrack == null) return false;
try {
await sender.replaceTrack(newTrack);
return true;
} catch (error) {
logger.error({
logger.warn({
logCode: 'video_provider_replacepc_error',
extraInfo: { errorMessage: error.message, cameraId: streamId },
}, `Failed to replace peer connection tracks: ${error.message}`);
return false;
}
}
if (replaced) {
peer.localStream = mediaStream;
});
Promise.all(trackReplacers).then(() => {
this.attach(peer, videoElement);
});
}
}

View File

@ -25,6 +25,7 @@ export default withTracker(({ swapLayout, ...rest }) => {
totalNumberOfStreams,
isUserLocked: VideoService.isUserLocked(),
currentVideoPageIndex: VideoService.getCurrentVideoPageIndex(),
isMeteorConnected: Meteor.status().connected,
...rest,
};
})(VideoProviderContainer);

View File

@ -131,6 +131,7 @@ const JoinVideoButton = ({
key: 'advancedVideo',
label: intl.formatMessage(intlMessages.advancedVideo),
onClick: () => handleOpenAdvancedOptions(),
dataTest: 'advancedVideoSettingsButton',
},
);
}
@ -154,6 +155,7 @@ const JoinVideoButton = ({
trigger={(
<ButtonEmoji
emoji="device_list_selector"
data-test="videoDropdownMenu"
hideLabel
label={intl.formatMessage(intlMessages.videoSettings)}
rotate

View File

@ -26,7 +26,7 @@ const VideoListItem = (props) => {
makeDragOperations, dragging, draggingOver, isRTL
} = props;
const [videoIsReady, setVideoIsReady] = useState(false);
const [videoDataLoaded, setVideoDataLoaded] = useState(false);
const [isStreamHealthy, setIsStreamHealthy] = useState(false);
const [isMirrored, setIsMirrored] = useState(VideoService.mirrorOwnWebcam(user?.userId));
const [isVideoSqueezed, setIsVideoSqueezed] = useState(false);
@ -41,7 +41,7 @@ const VideoListItem = (props) => {
const videoTag = useRef();
const videoContainer = useRef();
const shouldRenderReconnect = !isStreamHealthy && videoIsReady;
const videoIsReady = isStreamHealthy && videoDataLoaded;
const { animations } = Settings.application;
const talking = voiceUser?.talking;
@ -49,14 +49,11 @@ const VideoListItem = (props) => {
const { streamState } = e.detail;
const newHealthState = !isStreamStateUnhealthy(streamState);
e.stopPropagation();
if (newHealthState !== isStreamHealthy) {
setIsStreamHealthy(newHealthState);
}
};
const handleSetVideoIsReady = () => {
setVideoIsReady(true);
const onLoadedData = () => {
setVideoDataLoaded(true);
window.dispatchEvent(new Event('resize'));
/* used when re-sharing cameras after leaving a breakout room.
@ -71,10 +68,10 @@ const VideoListItem = (props) => {
onVideoItemMount(videoTag.current);
subscribeToStreamStateChange(cameraId, onStreamStateChange);
resizeObserver.observe(videoContainer.current);
videoTag?.current?.addEventListener('loadeddata', handleSetVideoIsReady);
videoTag?.current?.addEventListener('loadeddata', onLoadedData);
return () => {
videoTag?.current?.removeEventListener('loadeddata', handleSetVideoIsReady);
videoTag?.current?.removeEventListener('loadeddata', onLoadedData);
resizeObserver.disconnect();
};
}, []);
@ -96,10 +93,10 @@ const VideoListItem = (props) => {
// 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 (videoIsReady) {
if (videoDataLoaded) {
playElement(videoTag.current);
}
}, [videoIsReady]);
}, [videoDataLoaded]);
// component will unmount
useEffect(() => () => {
@ -130,7 +127,7 @@ const VideoListItem = (props) => {
<UserAvatarVideo
user={user}
voiceUser={voiceUser}
unhealthyStream={shouldRenderReconnect}
unhealthyStream={videoDataLoaded && !isStreamHealthy}
squeezed={false}
/>
<Styled.BottomBar>
@ -158,7 +155,7 @@ const VideoListItem = (props) => {
>
<UserAvatarVideo
user={user}
unhealthyStream={shouldRenderReconnect}
unhealthyStream={videoDataLoaded && !isStreamHealthy}
squeezed
/>
{renderSqueezedButton()}
@ -213,7 +210,7 @@ const VideoListItem = (props) => {
<Styled.VideoContainer>
<Styled.Video
mirrored={isMirrored}
unhealthyStream={shouldRenderReconnect}
unhealthyStream={videoDataLoaded && !isStreamHealthy}
data-test={isMirrored ? 'mirroredVideoContainer' : 'videoContainer'}
ref={videoTag}
autoPlay
@ -229,8 +226,6 @@ const VideoListItem = (props) => {
: (isVideoSqueezed)
? renderWebcamConnectingSqueezed()
: renderWebcamConnecting()}
{shouldRenderReconnect && <Styled.Reconnecting animations={animations} />}
</Styled.Content>
);
};

View File

@ -109,29 +109,6 @@ const LoadingText = styled(TextElipsis)`
font-size: 100%;
`;
const Reconnecting = styled.div`
position: absolute;
height: 100%;
width: 100%;
display: flex;
font-size: 2.5rem;
z-index: 1;
align-items: center;
justify-content: center;
background-color: transparent;
color: ${colorWhite};
&::before {
font-family: 'bbb-icons' !important;
content: "\\e949";
/* ascii code for the ellipsis character */
display: inline-block;
${({ animations }) => animations && css`
animation: ${rotate360} 2s infinite linear;
`}
}
`;
const VideoContainer = styled.div`
display: flex;
justify-content: center;
@ -180,7 +157,6 @@ export default {
Content,
WebcamConnecting,
LoadingText,
Reconnecting,
VideoContainer,
Video,
TopBar,

View File

@ -90,6 +90,7 @@ const UserActions = (props) => {
label: intl.formatMessage(intlMessages.mirrorLabel),
description: intl.formatMessage(intlMessages.mirrorDesc),
onClick: () => onHandleMirror(),
dataTest: 'mirrorWebcamBtn',
});
if (numOfStreams > 2) {
@ -98,6 +99,7 @@ const UserActions = (props) => {
label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]),
onClick: () => onHandleVideoFocus(cameraId),
dataTest: 'FocusWebcamBtn',
});
}
@ -107,6 +109,7 @@ const UserActions = (props) => {
label: intl.formatMessage(intlMessages[`${isPinnedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isPinnedIntlKey}Desc`]),
onClick: () => VideoService.toggleVideoPin(userId, pinned),
dataTest: 'pinWebcamBtn',
});
}

View File

@ -91,6 +91,7 @@ export default function Whiteboard(props) {
const prevSvgUri = usePrevious(svgUri);
const language = mapLanguage(Settings?.application?.locale?.toLowerCase() || 'en');
const [currentTool, setCurrentTool] = React.useState(null);
const [currentStyle, setCurrentStyle] = React.useState({});
const [isMoving, setIsMoving] = React.useState(false);
const [isPanning, setIsPanning] = React.useState(shortcutPanning);
const [panSelected, setPanSelected] = React.useState(isPanning);
@ -110,12 +111,6 @@ export default function Whiteboard(props) {
}
};
const setSafeCurrentTool = (tool) => {
if (isMountedRef.current) {
setCurrentTool(tool);
}
};
const toggleOffCheck = (evt) => {
const clickedElement = evt.target;
const panBtnClicked = clickedElement?.getAttribute('data-test') === 'panButton'
@ -594,8 +589,6 @@ export default function Whiteboard(props) {
const onMount = (app) => {
const menu = document.getElementById('TD-Styles')?.parentElement;
setSafeCurrentTool('select');
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.addEventListener('wheel', handleWheelEvent, { capture: true });
@ -825,6 +818,16 @@ export default function Whiteboard(props) {
setIsPanning(false);
}
if (reason && reason.includes('ui:toggled_is_loading')) {
e?.patchState(
{
appState: {
currentStyle,
},
},
);
}
e?.patchState(
{
appState: {
@ -832,6 +835,10 @@ export default function Whiteboard(props) {
},
},
);
if ((panSelected || isPanning)) {
e.isForcePanning = isPanning;
}
};
const onUndo = (app) => {
@ -880,6 +887,11 @@ export default function Whiteboard(props) {
if (!isFirstCommand){
setHistory(app.history);
}
if (command?.id?.includes('style')) {
setCurrentStyle({ ...currentStyle, ...command?.after?.appState?.currentStyle });
}
const changedShapes = command.after?.document?.pages[app.currentPageId]?.shapes;
if (!isMounting && app.currentPageId !== curPageId) {
// can happen then the "move to page action" is called, or using undo after changing a page
@ -968,10 +980,6 @@ export default function Whiteboard(props) {
const size = ((height < SMALL_HEIGHT) || (width < SMALL_WIDTH))
? TOOLBAR_SMALL : TOOLBAR_LARGE;
if ((panSelected || isPanning) && tldrawAPI) {
tldrawAPI.isForcePanning = isPanning;
}
if (hasWBAccess || isPresenter) {
if (((height < SMALLEST_HEIGHT) || (width < SMALLEST_WIDTH))) {
tldrawAPI?.setSetting('dockPosition', 'bottom');
@ -1015,6 +1023,7 @@ export default function Whiteboard(props) {
size,
darkTheme,
menuOffset,
panSelected,
}}
/>
</Cursors>

View File

@ -101,6 +101,9 @@ class PanToolInjector extends React.Component {
}
}}
hideLabel
{...{
panSelected,
}}
/>,
container,
);

View File

@ -72,6 +72,14 @@ const TldrawGlobalStyle = createGlobalStyle`
margin: ${borderSize} ${borderSizeLarge} 0px ${borderSizeLarge};
}
`}
${({ hasWBAccess, isPresenter, panSelected }) => (hasWBAccess || isPresenter) && panSelected && `
[id^="TD-PrimaryTools-"] {
&:hover > div,
&:focus > div {
background-color: var(--colors-hover) !important;
}
}
`}
${({ darkTheme }) => darkTheme && `
#TD-TopPanel-Undo,
#TD-TopPanel-Redo,
@ -113,10 +121,12 @@ const PanTool = styled(Button)`
}
}
${({ panSelected }) => !panSelected && `
&:hover,
&:focus {
background-color: var(--colors-hover);
background-color: var(--colors-hover) !important;
}
`}
`;
export default {

View File

@ -29,6 +29,7 @@ class AudioBroker extends BaseBroker {
// signalCandidates
// traceLogs
// networkPriority
// gatheringTimeout
Object.assign(this, options);
}
@ -85,6 +86,7 @@ class AudioBroker extends BaseBroker {
trace: this.traceLogs,
networkPriorities: this.networkPriority ? { audio: this.networkPriority } : undefined,
mediaStreamFactory: this.mediaStreamFactory,
gatheringTimeout: this.gatheringTimeout,
};
const peerRole = this.role === 'sendrecv' ? this.role : 'recvonly';

View File

@ -38,6 +38,7 @@ class ScreenshareBroker extends BaseBroker {
// signalCandidates,
// traceLogs
// networkPriority
// gatheringTimeout
Object.assign(this, options);
}
@ -189,6 +190,7 @@ class ScreenshareBroker extends BaseBroker {
configuration: this.populatePeerConfiguration(),
trace: this.traceLogs,
networkPriorities: this.networkPriority ? { video: this.networkPriority } : undefined,
gatheringTimeout: this.gatheringTimeout,
};
this.webRtcPeer = new WebRtcPeer('sendonly', options);
this.webRtcPeer.iceQueue = [];
@ -248,6 +250,7 @@ class ScreenshareBroker extends BaseBroker {
onicecandidate: this.signalCandidates ? this.onIceCandidate.bind(this) : null,
configuration: this.populatePeerConfiguration(),
trace: this.traceLogs,
gatheringTimeout: this.gatheringTimeout,
};
this.webRtcPeer = new WebRtcPeer('recvonly', options);

View File

@ -2,7 +2,10 @@ import logger from '/imports/startup/client/logger';
import { notifyStreamStateChange } from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
import { SFU_BROKER_ERRORS } from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors';
const PING_INTERVAL_MS = 15000;
const WS_HEARTBEAT_OPTS = {
interval: 15000,
delay: 3000,
};
class BaseBroker {
static assembleError(code, reason) {
@ -21,13 +24,14 @@ class BaseBroker {
this.sfuComponent = sfuComponent;
this.ws = null;
this.webRtcPeer = null;
this.pingInterval = null;
this.wsHeartbeat = null;
this.started = false;
this.signallingTransportOpen = false;
this.logCodePrefix = `${this.sfuComponent}_broker`;
this.peerConfiguration = {};
this.onbeforeunload = this.onbeforeunload.bind(this);
this._onWSError = this._onWSError.bind(this);
window.addEventListener('beforeunload', this.onbeforeunload);
}
@ -63,18 +67,18 @@ class BaseBroker {
// To be implemented by inheritors
}
openWSConnection () {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.wsUrl);
_onWSMessage(message) {
this._updateLastMsgTime();
this.onWSMessage(message);
}
this.ws.onmessage = this.onWSMessage.bind(this);
onWSMessage(message) {
// To be implemented by inheritors
}
this.ws.onclose = () => {
// 1301: "WEBSOCKET_DISCONNECTED",
this.onerror(BaseBroker.assembleError(1301));
};
_onWSError(error) {
let normalizedError;
this.ws.onerror = (error) => {
logger.error({
logCode: `${this.logCodePrefix}_websocket_error`,
extraInfo: {
@ -85,26 +89,103 @@ class BaseBroker {
if (this.signallingTransportOpen) {
// 1301: "WEBSOCKET_DISCONNECTED", transport was already open
this.onerror(BaseBroker.assembleError(1301));
normalizedError = BaseBroker.assembleError(1301);
} else {
// 1302: "WEBSOCKET_CONNECTION_FAILED", transport errored before establishment
const normalized1302 = BaseBroker.assembleError(1302);
this.onerror(normalized1302);
return reject(normalized1302);
normalizedError = BaseBroker.assembleError(1302);
}
this.onerror(normalizedError);
return normalizedError;
}
openWSConnection () {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.wsUrl);
this.ws.onmessage = this._onWSMessage.bind(this);
this.ws.onclose = () => {
// 1301: "WEBSOCKET_DISCONNECTED",
this.onerror(BaseBroker.assembleError(1301));
};
this.ws.onerror = (error) => reject(this._onWSError(error));
this.ws.onopen = () => {
this.pingInterval = setInterval(this.ping.bind(this), PING_INTERVAL_MS);
this.setupWSHeartbeat();
this.signallingTransportOpen = true;
return resolve();
};
});
}
closeWs() {
this.clearWSHeartbeat();
if (this.ws !== null) {
this.ws.onclose = function (){};
this.ws.close();
}
}
_updateLastMsgTime() {
this.ws.isAlive = true;
this.ws.lastMsgTime = Date.now();
}
_getTimeSinceLastMsg() {
return Date.now() - this.ws.lastMsgTime;
}
setupWSHeartbeat() {
if (WS_HEARTBEAT_OPTS.interval === 0 || this.ws == null) return;
this.ws.isAlive = true;
this.wsHeartbeat = setInterval(() => {
if (this.ws.isAlive === false) {
logger.warn({
logCode: `${this.logCodePrefix}_ws_heartbeat_failed`,
}, `WS heartbeat failed (${this.sfuComponent})`);
this.closeWs();
this._onWSError(new Error('HeartbeatFailed'));
return;
}
if (this._getTimeSinceLastMsg() < (
WS_HEARTBEAT_OPTS.interval - WS_HEARTBEAT_OPTS.delay
)) {
return;
}
this.ws.isAlive = false;
this.ping();
}, WS_HEARTBEAT_OPTS.interval);
this.ping();
}
clearWSHeartbeat() {
if (this.wsHeartbeat) {
clearInterval(this.wsHeartbeat);
}
}
sendMessage (message) {
const jsonMessage = JSON.stringify(message);
try {
this.ws.send(jsonMessage);
} catch (error) {
logger.error({
logCode: `${this.logCodePrefix}_ws_send_error`,
extraInfo: {
errorName: error.name,
errorMessage: error.message,
sfuComponent: this.sfuComponent,
},
}, `Failed to send WebSocket message (${this.sfuComponent})`);
}
}
ping () {
@ -266,15 +347,7 @@ class BaseBroker {
this.webRtcPeer.peerConnection.onconnectionstatechange = null;
}
if (this.ws !== null) {
this.ws.onclose = function (){};
this.ws.close();
}
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
this.closeWs();
this.disposePeer();
this.started = false;

View File

@ -29,8 +29,12 @@ export default class WebRtcPeer extends EventEmitter2 {
this._outboundCandidateQueue = [];
this._inboundCandidateQueue = [];
this._waitForGatheringPromise = null;
this._waitForGatheringTimeout = null;
this._handleIceCandidate = this._handleIceCandidate.bind(this);
this._handleSignalingStateChange = this._handleSignalingStateChange.bind(this);
this._gatheringTimeout = this.options.gatheringTimeout;
this._assignOverrides();
}
@ -139,6 +143,48 @@ export default class WebRtcPeer extends EventEmitter2 {
}
}
waitForGathering(timeout = 0) {
if (timeout <= 0) return Promise.resolve();
if (this.isPeerConnectionClosed()) throw new Error('PeerConnection is closed');
if (this.peerConnection.iceGatheringState === 'complete') return Promise.resolve();
if (this._waitForGatheringPromise) return this._waitForGatheringPromise;
this._waitForGatheringPromise = new Promise((resolve) => {
this.once('candidategatheringdone', resolve);
this._waitForGatheringTimeout = setTimeout(() => {
this._emitCandidateGatheringDone();
}, timeout);
});
return this._waitForGatheringPromise;
}
_setRemoteDescription(rtcSessionDescription) {
if (this.isPeerConnectionClosed()) {
this.logger.error('BBB::WebRtcPeer::_setRemoteDescription - peer connection closed');
throw new Error('Peer connection is closed');
}
this.logger.debug('BBB::WebRtcPeer::_setRemoteDescription - setting remote description', rtcSessionDescription);
return this.peerConnection.setRemoteDescription(rtcSessionDescription);
}
_setLocalDescription(rtcSessionDescription) {
if (this.isPeerConnectionClosed()) {
this.logger.error('BBB::WebRtcPeer::_setLocalDescription - peer connection closed');
throw new Error('Peer connection is closed');
}
if (typeof this._gatheringTimeout === 'number' && this._gatheringTimeout > 0) {
this.logger.debug('BBB::WebRtcPeer::_setLocalDescription - setting description with gathering timer', rtcSessionDescription, this._gatheringTimeout);
return this.peerConnection.setLocalDescription(rtcSessionDescription)
.then(() => this.waitForGathering(this._gatheringTimeout));
}
this.logger.debug('BBB::WebRtcPeer::_setLocalDescription- setting description', rtcSessionDescription);
return this.peerConnection.setLocalDescription(rtcSessionDescription);
}
// Public method can be overriden via options
mediaStreamFactory() {
if (this.videoStream || this.audioStream) {
@ -194,19 +240,23 @@ export default class WebRtcPeer extends EventEmitter2 {
}
getLocalStream() {
if (this.localStream) {
return this.localStream;
}
if (this.peerConnection) {
this.localStream = new MediaStream();
if (this.localStream == null) this.localStream = new MediaStream();
const senders = this.peerConnection.getSenders();
const oldTracks = this.localStream.getTracks();
senders.forEach(({ track }) => {
if (track) {
if (track && !oldTracks.includes(track)) {
this.localStream.addTrack(track);
}
});
oldTracks.forEach((oldTrack) => {
if (!senders.some(({ track }) => track && track.id === oldTrack.id)) {
this.localStream.removeTrack(oldTrack);
}
});
return this.localStream;
}
@ -280,10 +330,10 @@ export default class WebRtcPeer extends EventEmitter2 {
switch (this.mode) {
case 'recvonly': {
const useAudio = this.mediaConstraints
&& ((typeof this.mediaConstraints.audio === 'boolean')
&& ((typeof this.mediaConstraints.audio === 'boolean' && this.mediaConstraints.audio)
|| (typeof this.mediaConstraints.audio === 'object'));
const useVideo = this.mediaConstraints
&& ((typeof this.mediaConstraints.video === 'boolean')
&& ((typeof this.mediaConstraints.video === 'boolean' && this.mediaConstraints.video)
|| (typeof this.mediaConstraints.video === 'object'));
if (useAudio) {
@ -330,7 +380,7 @@ export default class WebRtcPeer extends EventEmitter2 {
return this.peerConnection.createOffer()
.then((offer) => {
this.logger.debug('BBB::WebRtcPeer::generateOffer - created offer', offer);
return this.peerConnection.setLocalDescription(offer);
return this._setLocalDescription(offer);
})
.then(() => {
this._processEncodingOptions();
@ -346,14 +396,7 @@ export default class WebRtcPeer extends EventEmitter2 {
sdp,
});
if (this.isPeerConnectionClosed()) {
this.logger.error('BBB::WebRtcPeer::processAnswer - peer connection closed');
throw new Error('Peer connection is closed');
}
this.logger.debug('BBB::WebRtcPeer::processAnswer - setting remote description');
return this.peerConnection.setRemoteDescription(answer);
return this._setRemoteDescription(answer);
}
processOffer(sdp) {
@ -362,18 +405,11 @@ export default class WebRtcPeer extends EventEmitter2 {
sdp,
});
if (this.isPeerConnectionClosed()) {
this.logger.error('BBB::WebRtcPeer::processOffer - peer connection closed');
throw new Error('Peer connection is closed');
}
this.logger.debug('BBB::WebRtcPeer::processOffer - setting remote description', offer);
return this.peerConnection.setRemoteDescription(offer)
return this._setRemoteDescription(offer)
.then(() => this.peerConnection.createAnswer())
.then((answer) => {
this.logger.debug('BBB::WebRtcPeer::processOffer - created answer', answer);
return this.peerConnection.setLocalDescription(answer);
return this._setLocalDescription(answer);
})
.then(() => {
const localDescription = this.getLocalSessionDescriptor();
@ -404,6 +440,12 @@ export default class WebRtcPeer extends EventEmitter2 {
this._outboundCandidateQueue = [];
this.candidateGatheringDone = false;
if (this._waitForGatheringPromise) this._waitForGatheringPromise = null;
if (this._waitForGatheringTimeout) {
clearTimeout(this._waitForGatheringTimeout);
this._waitForGatheringTimeout = null;
}
} catch (error) {
this.logger.trace('BBB::WebRtcPeer::dispose - failed', error);
}

View File

@ -264,9 +264,18 @@ public:
enabled: true
kurento:
wsUrl: HOST
cameraWsOptions:
# Valid for video-provider. Time (ms) before its WS connection times out
# and tries to reconnect.
wsConnectionTimeout: 4000
# maxRetries: max reconnection retries
maxRetries: 7
# debug: console trace logging for video-provider's ws
debug: false
heartbeat:
interval: 15000
delay: 3000
reconnectOnFailure: true
# Time in milis to wait for the browser to return a gUM call (used in video-preview)
gUMTimeout: 20000
# Controls whether ICE candidates should be signaled to bbb-webrtc-sfu.
@ -295,11 +304,13 @@ public:
bitrate: 1500
mediaTimeouts:
maxConnectionAttempts: 2
# Base screen media timeout (send|recv)
baseTimeout: 30000
# Max timeout: used as the max camera subscribe reconnection timeout. Each
# Base screen media timeout (send|recv) - first connections
baseTimeout: 20000
# Base screen media timeout (send|recv) - re-connections
baseReconnectionTimeout: 8000
# Max timeout: used as the max camera subscribe connection timeout. Each
# subscribe reattempt increases the reconnection timer up to this
maxTimeout: 60000
maxTimeout: 25000
timeoutIncreaseFactor: 1.5
constraints:
video:

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Es ist ein Fehler aufgetreten",
"app.error.fallback.presentation.description": "Er wurde protokolliert. Bitte versuchen, die Seite neu zu laden.",
"app.error.fallback.presentation.reloadButton": "Neu laden",
"app.guest.waiting": "Auf Erlaubnis zur Konferenzteilnahme wird gewartet",
"app.guest.errorSeeConsole": "Fehler: weitere Details in der Konsole.",
"app.guest.noModeratorResponse": "Keine Antwort vom Moderator.",
"app.guest.noSessionToken": "Kein Konferenz-Token erhalten.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Παρουσιάστηκε σφάλμα",
"app.error.fallback.presentation.description": "Καταγράφτηκε. Παρακαλούμε δοκιμάστε να ανανεώσετε τη σελίδα.",
"app.error.fallback.presentation.reloadButton": "Επαναφόρτωση",
"app.guest.waiting": "Αναμονή για έγκριση συμμετοχής",
"app.guest.errorSeeConsole": "Σφάλμα: δείτε περισσότερες πληροφορίες στην κονσόλα.",
"app.guest.noModeratorResponse": "Καμία απάντηση από διαχειριστή.",
"app.guest.noSessionToken": "Δεν έχει ληφθεί το αναγνωριστικό συνεδρίας.",

View File

@ -235,6 +235,8 @@
"app.presentation.presentationToolbar.fitToWidth": "Fit to width",
"app.presentation.presentationToolbar.fitToPage": "Fit to page",
"app.presentation.presentationToolbar.goToSlide": "Slide {0}",
"app.presentation.presentationToolbar.hideToolsDesc": "Hide Toolbars",
"app.presentation.presentationToolbar.showToolsDesc": "Show Toolbars",
"app.presentation.placeholder": "There is no currently active presentation",
"app.presentationUploder.title": "Presentation",
"app.presentationUploder.message": "As a presenter you have the ability to upload any office document or PDF file. We recommend PDF file for best results. Please ensure that a presentation is selected using the circle checkbox on the left hand side.",
@ -767,7 +769,6 @@
"app.error.fallback.presentation.title": "An error occurred",
"app.error.fallback.presentation.description": "It has been logged. Please try reloading the page.",
"app.error.fallback.presentation.reloadButton": "Reload",
"app.guest.waiting": "Waiting for approval to join",
"app.guest.errorSeeConsole": "Error: more details in the console.",
"app.guest.noModeratorResponse": "No response from Moderator.",
"app.guest.noSessionToken": "No session Token received.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Ha ocurrido un error",
"app.error.fallback.presentation.description": "Esto se ha registrado. Por favor, intente volver a cargar la página.",
"app.error.fallback.presentation.reloadButton": "Recargar",
"app.guest.waiting": "Esperando aprobación para unirse",
"app.guest.errorSeeConsole": "Error: más detalles en la consola.",
"app.guest.noModeratorResponse": "Sin respuesta del moderador.",
"app.guest.noSessionToken": "No se recibió ningún token de sesión.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Ocurrió un error",
"app.error.fallback.presentation.description": "Ha sido registrado. Por favor, intente volver a cargar la página.",
"app.error.fallback.presentation.reloadButton": "Recargar",
"app.guest.waiting": "Esperando aprobación para unirse",
"app.guest.errorSeeConsole": "Error: más detalles en la consola.",
"app.guest.noModeratorResponse": "Sin respuesta del moderador.",
"app.guest.noSessionToken": "No se recibió ningún token de sesión.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Esines viga",
"app.error.fallback.presentation.description": "Viga on registreeritud. Palun proovi leht uuesti laadida.",
"app.error.fallback.presentation.reloadButton": "Laadi uuesti",
"app.guest.waiting": "Ootame ühendamiseks nõusolekut",
"app.guest.errorSeeConsole": "Viga: täpsemad üksikasjad konsoolil.",
"app.guest.noModeratorResponse": "Pole vastust moderaatorilt.",
"app.guest.noSessionToken": "Pole kätte saanud sessioonitõendit.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Errore bat gertatu da",
"app.error.fallback.presentation.description": "Saioa hasi da. Saiatu orria berriro kargatzen.",
"app.error.fallback.presentation.reloadButton": "Birkargatu",
"app.guest.waiting": "Sartzeko onarpenaren zain",
"app.guest.errorSeeConsole": "Errorea: xehetasun gehiago kontsolan",
"app.guest.noModeratorResponse": "Moderatzaileak ez du erantzuten.",
"app.guest.noSessionToken": "Ez da saio-tokenik jaso.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "خطایی رخ داد",
"app.error.fallback.presentation.description": "این مورد ثبت شد. لطفا صفحه را دوباره بارگیری کنید.",
"app.error.fallback.presentation.reloadButton": "بارگیری دوباره",
"app.guest.waiting": "در انتظار تأیید برای پیوستن",
"app.guest.errorSeeConsole": "خطا: جزئیات بیشتر در کنسول.",
"app.guest.noModeratorResponse": "هیچ پاسخی از مدیر وجود ندارد",
"app.guest.noSessionToken": "هیچ توکن جلسه‌ای دریافت نشد.",

View File

@ -122,7 +122,7 @@
"app.userList.menu.removeConfirmation.label": "Retirer l'utilisateur ({0})",
"app.userlist.menu.removeConfirmation.desc": "Empêcher cet utilisateur de rejoindre la réunion à nouveau.",
"app.userList.menu.muteUserAudio.label": "Rendre l'utilisateur silencieux",
"app.userList.menu.unmuteUserAudio.label": "Autoriser l'utilisateur à parler",
"app.userList.menu.unmuteUserAudio.label": "Permettre l'activation du microphone",
"app.userList.menu.webcamPin.label": "Épingler la caméra de ce participant",
"app.userList.menu.webcamUnpin.label": "Désépingler la caméra de ce participant",
"app.userList.menu.giveWhiteboardAccess.label" : "Donner l'accès au tableau blanc",
@ -334,9 +334,9 @@
"app.poll.secretPoll.label" : "Sondage Anonyme",
"app.poll.secretPoll.isSecretLabel": "Le sondage est anonyme - vous ne pourrez pas voir les réponses individuelles.",
"app.poll.questionErr": "Vous devez soumettre une question.",
"app.poll.optionErr": "Entrez une option de sondage",
"app.poll.optionErr": "Renseignez une option de sondage",
"app.poll.startPollDesc": "Commencer le sondage",
"app.poll.showRespDesc": "Affiche la configuration de la réponse",
"app.poll.showRespDesc": "Affiche la configuration des réponses",
"app.poll.addRespDesc": "Ajoute une option de réponse au sondage",
"app.poll.deleteRespDesc": "Supprime l'option {0}",
"app.poll.t": "Vrai",
@ -363,7 +363,7 @@
"app.poll.liveResult.usersTitle": "Utilisateurs",
"app.poll.liveResult.responsesTitle": "Réponse",
"app.poll.liveResult.secretLabel": "Ceci est un sondage anonyme. Les réponses individuelles ne seront pas affichées.",
"app.poll.removePollOpt": "L'option a été retirée du sondage {0} ",
"app.poll.removePollOpt": "L'option a été retirée du sondage {0}",
"app.poll.emptyPollOpt": "Vide",
"app.polling.pollingTitle": "Options du sondage",
"app.polling.pollQuestionTitle": "Question du sondage",
@ -373,10 +373,10 @@
"app.polling.responseSecret": "Sondage anonyme - le présentateur ne vois pas qui répond.",
"app.polling.responseNotSecret": "Sondage normal - le présentateur peut voir votre réponse.",
"app.polling.pollAnswerLabel": "Réponse au sondage {0}",
"app.polling.pollAnswerDesc": "Choisir cette option pour voter {0}",
"app.polling.pollAnswerDesc": "Choisir cette option afin de voter pour {0}",
"app.failedMessage": "Problème de connexion au serveur, toutes nos excuses.",
"app.downloadPresentationButton.label": "Télécharger la présentation d'origine",
"app.connectingMessage": "Connexion...",
"app.connectingMessage": "Connexion en cours...",
"app.waitingMessage": "Déconnecté. Essayez de vous reconnecter dans {0} secondes...",
"app.retryNow": "Réessayer maintenant",
"app.muteWarning.label": "Cliquez sur {0} pour réactiver votre micro.",
@ -391,7 +391,7 @@
"app.navBar.settingsDropdown.fullscreenDesc": "Passer le menu de paramétrage en plein écran",
"app.navBar.settingsDropdown.settingsDesc": "Modifier les paramètres généraux",
"app.navBar.settingsDropdown.aboutDesc": "Afficher les informations du client",
"app.navBar.settingsDropdown.leaveSessionDesc": "Quitter la conférence",
"app.navBar.settingsDropdown.leaveSessionDesc": "Quitter la réunion",
"app.navBar.settingsDropdown.exitFullscreenDesc": "Quitter le mode plein écran",
"app.navBar.settingsDropdown.hotkeysLabel": "Raccourcis clavier",
"app.navBar.settingsDropdown.hotkeysDesc": "Liste des raccourcis clavier disponibles",
@ -400,20 +400,20 @@
"app.navBar.settingsDropdown.helpDesc": "Renvoie l'utilisateur vers des tutoriels vidéos (ouvre un nouvel onglet)",
"app.navBar.settingsDropdown.endMeetingDesc": "Interrompt la réunion en cours",
"app.navBar.settingsDropdown.endMeetingLabel": "Mettre fin à la réunion",
"app.navBar.userListToggleBtnLabel": "Affichage de la liste des utilisateurs",
"app.navBar.toggleUserList.ariaLabel": "Affichage des utilisateurs et des messages",
"app.navBar.userListToggleBtnLabel": "Masquer ou afficher la liste des utilisateurs",
"app.navBar.toggleUserList.ariaLabel": "Afficher les utilisateurs ou les messages",
"app.navBar.toggleUserList.newMessages": "avec notification des nouveaux messages",
"app.navBar.toggleUserList.newMsgAria": "Nouveau message de {0}",
"app.navBar.recording": "Cette session est enregistrée",
"app.navBar.recording.on": "Enregistrement en cours",
"app.navBar.recording.off": "Pas d'enregistrement en cours",
"app.navBar.emptyAudioBrdige": "Le microphone n'est pas actif. Partagez votre microphone pour ajouter du son à cet enregistrement.",
"app.navBar.emptyAudioBrdige": "Le microphone n'est pas actif. Activez votre microphone pour ajouter du son à cet enregistrement.",
"app.leaveConfirmation.confirmLabel": "Quitter",
"app.leaveConfirmation.confirmDesc": "Vous déconnecte de la conférence",
"app.leaveConfirmation.confirmDesc": "Vous déconnecte de la réunion",
"app.endMeeting.title": "Mettre fin à {0}",
"app.endMeeting.description": "Cette action mettra fin à la séance pour {0} utilisateurs(s) actif(s). Êtes-vous sûr de vouloir mettre fin à cette séance ?",
"app.endMeeting.noUserDescription": "Êtes-vous sûr de vouloir mettre fin à la séance ?",
"app.endMeeting.contentWarning": "Les messages de discussion, les notes partagées, le contenu du tableau blanc et les documents partagés lors de cette séance ne seront plus accessibles directement ",
"app.endMeeting.contentWarning": "Les messages de discussion, les notes partagées, le contenu du tableau blanc et les documents partagés lors de cette séance ne seront plus directement accessibles",
"app.endMeeting.yesLabel": "Mettre fin à la session pour tous les utilisateurs",
"app.endMeeting.noLabel": "Non",
"app.about.title": "À propos",
@ -423,7 +423,7 @@
"app.about.confirmLabel": "OK",
"app.about.confirmDesc": "OK",
"app.about.dismissLabel": "Annuler",
"app.about.dismissDesc": "Fermer l'information client",
"app.about.dismissDesc": "Fermer la note à propos du client",
"app.mobileAppModal.title": "Ouvrir l'application mobile de BigBlueButton",
"app.mobileAppModal.description": "L'application mobile de BigBlueButton est-elle installée sur votre appareil ?",
"app.mobileAppModal.openApp": "Oui, ouvrir l'application maintenant",
@ -435,7 +435,7 @@
"app.mobileAppModal.userConnectedWithSameId": "L'utilisateur {0} vient de se connecter en utilisant le même identifiant que vous",
"app.actionsBar.changeStatusLabel": "Changer de statut",
"app.actionsBar.muteLabel": "Rendre silencieux",
"app.actionsBar.unmuteLabel": "Autoriser à parler",
"app.actionsBar.unmuteLabel": "Permettre l'activation du microphone",
"app.actionsBar.camOffLabel": "Caméra éteinte",
"app.actionsBar.raiseLabel": "Lever la main",
"app.actionsBar.label": "Barre d'actions",
@ -456,7 +456,7 @@
"app.submenu.application.languageLabel": "Langue de l'application",
"app.submenu.application.languageOptionLabel": "Choisir la langue",
"app.submenu.application.noLocaleOptionLabel": "Aucune langue d'application détectée",
"app.submenu.application.paginationEnabledLabel": "Pagination de la vidéo",
"app.submenu.application.paginationEnabledLabel": "Mise en page de la vidéo",
"app.submenu.application.layoutOptionLabel": "Type de mise en page",
"app.submenu.application.pushLayoutLabel": "Appliquer la mise en page",
"app.submenu.application.localeDropdown.af": "Afrikaans",
@ -526,15 +526,15 @@
"app.submenu.notification.audioAlertLabel": "Alerte sonore",
"app.submenu.notification.pushAlertLabel": "Message d'alerte",
"app.submenu.notification.messagesLabel": "Fil de discussion",
"app.submenu.notification.userJoinLabel": "L'utilisateur a rejoint la réunion",
"app.submenu.notification.userLeaveLabel": "L'utilisateur a quitté la réunion",
"app.submenu.notification.guestWaitingLabel": "Invité en attente d'approbation pour accéder",
"app.submenu.notification.userJoinLabel": "L'utilisateur rejoint la réunion",
"app.submenu.notification.userLeaveLabel": "L'utilisateur quitte la réunion",
"app.submenu.notification.guestWaitingLabel": "Invité en attente d'approbation",
"app.submenu.audio.micSourceLabel": "Choix du micro",
"app.submenu.audio.speakerSourceLabel": "Choix du haut-parleur",
"app.submenu.audio.streamVolumeLabel": "Volume de votre flux audio",
"app.submenu.video.title": "Vidéo",
"app.submenu.video.videoSourceLabel": "Choix de la source pour l'affichage",
"app.submenu.video.videoOptionLabel": "Choisir la source pour l'affichage",
"app.submenu.video.videoSourceLabel": "Choix de la source vidéo",
"app.submenu.video.videoOptionLabel": "Choisir la source vidéo",
"app.submenu.video.videoQualityLabel": "Qualité de la vidéo",
"app.submenu.video.qualityOptionLabel": "Choisissez la qualité de la vidéo",
"app.submenu.video.participantsCamLabel": "Voir les webcams des participants",
@ -547,7 +547,7 @@
"app.settings.main.cancel.label.description": "Annule les changements et ferme le menu des paramètres",
"app.settings.main.save.label": "Enregistrer",
"app.settings.main.save.label.description": "Enregistre les changements et ferme le menu des paramètres",
"app.settings.dataSavingTab.label": "Économies de données",
"app.settings.dataSavingTab.label": "Gestion du débit",
"app.settings.dataSavingTab.webcam": "Activer les webcams des autres participants",
"app.settings.dataSavingTab.screenShare": "Activer le partage d'écran des autres participants",
"app.settings.dataSavingTab.description": "Pour économiser votre bande passante, ajustez l'affichage actuel.",
@ -558,8 +558,8 @@
"app.statusNotifier.raisedHandDesc": "{0} ont levé la main",
"app.statusNotifier.raisedHandDescOneUser": "{0} a levé la main",
"app.statusNotifier.and": "et",
"app.switch.onLabel": "ON",
"app.switch.offLabel": "OFF",
"app.switch.onLabel": "Allumé",
"app.switch.offLabel": "Éteint",
"app.talkingIndicator.ariaMuteDesc" : "Sélectionner pour rendre l'utilisateur silencieux",
"app.talkingIndicator.isTalking" : "{0} est en train de parler",
"app.talkingIndicator.moreThanMaxIndicatorsTalking" : "{0}+ parlent",
@ -567,16 +567,16 @@
"app.talkingIndicator.wasTalking" : "{0} a cessé de parler",
"app.actionsBar.actionsDropdown.actionsLabel": "Actions",
"app.actionsBar.actionsDropdown.presentationLabel": "Charger/Gérer les documents de présentation",
"app.actionsBar.actionsDropdown.initPollLabel": "Préparer un sondage",
"app.actionsBar.actionsDropdown.initPollLabel": "Lancer un sondage",
"app.actionsBar.actionsDropdown.desktopShareLabel": "Partager votre écran",
"app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Cesser le partage d'écran",
"app.actionsBar.actionsDropdown.presentationDesc": "Charger votre présentation",
"app.actionsBar.actionsDropdown.initPollDesc": "Préparer un sondage",
"app.actionsBar.actionsDropdown.initPollDesc": "Lancer un sondage",
"app.actionsBar.actionsDropdown.desktopShareDesc": "Partager votre écran avec les autres participants",
"app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Cesser de partager votre écran ",
"app.actionsBar.actionsDropdown.pollBtnLabel": "Lancer un sondage",
"app.actionsBar.actionsDropdown.pollBtnLabel": "Démarrer un sondage",
"app.actionsBar.actionsDropdown.pollBtnDesc": "Affiche/cache le volet de sondage",
"app.actionsBar.actionsDropdown.saveUserNames": "Sauvegarder les noms d'utilisateur",
"app.actionsBar.actionsDropdown.saveUserNames": "Sauvegarder les noms des utilisateurs",
"app.actionsBar.actionsDropdown.createBreakoutRoom": "Créer des salles pour les groupes de travail",
"app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "Créer des groupes de travail pour scinder la réunion en cours",
"app.actionsBar.actionsDropdown.captionsLabel": "Écrire des sous-titres SME",
@ -587,20 +587,20 @@
"app.actionsBar.actionsDropdown.selectRandUserDesc": "Sélectionne aléatoirement un utilisateur parmi les participants disponibles",
"app.actionsBar.actionsDropdown.propagateLayoutLabel": "Diffuser la mise en page",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Définir votre statut",
"app.actionsBar.emojiMenu.awayLabel": "Éloigné",
"app.actionsBar.emojiMenu.awayDesc": "Passer votre statut à « éloigné »",
"app.actionsBar.emojiMenu.awayLabel": "Absent",
"app.actionsBar.emojiMenu.awayDesc": "Passer votre statut à « absent »",
"app.actionsBar.emojiMenu.raiseHandLabel": "Lever la main",
"app.actionsBar.emojiMenu.lowerHandLabel": "Abaisser la main",
"app.actionsBar.emojiMenu.raiseHandDesc": "Lever la main pour poser une question",
"app.actionsBar.emojiMenu.neutralLabel": "Indécis",
"app.actionsBar.emojiMenu.neutralDesc": "Passer votre statut à « indécis »",
"app.actionsBar.emojiMenu.confusedLabel": "Désorienté",
"app.actionsBar.emojiMenu.confusedDesc": "Passer votre statut à « désorienté »",
"app.actionsBar.emojiMenu.confusedLabel": "Troublé",
"app.actionsBar.emojiMenu.confusedDesc": "Passer votre statut à « troublé »",
"app.actionsBar.emojiMenu.sadLabel": "Triste",
"app.actionsBar.emojiMenu.sadDesc": "Passer votre statut à « triste »",
"app.actionsBar.emojiMenu.happyLabel": "Ravi",
"app.actionsBar.emojiMenu.happyDesc": "Passer votre statut à « ravi »",
"app.actionsBar.emojiMenu.noneLabel": "Effacer votre statut",
"app.actionsBar.emojiMenu.noneLabel": "Effacer le statut",
"app.actionsBar.emojiMenu.noneDesc": "Effacer votre statut",
"app.actionsBar.emojiMenu.applauseLabel": "Applaudissements",
"app.actionsBar.emojiMenu.applauseDesc": "Passer votre statut à « applaudissements »",
@ -609,30 +609,30 @@
"app.actionsBar.emojiMenu.thumbsDownLabel": "Défavorable",
"app.actionsBar.emojiMenu.thumbsDownDesc": "Passer votre statut à « défavorable »",
"app.actionsBar.currentStatusDesc": "statut actuel {0}",
"app.actionsBar.captions.start": "Démarrer l'affichage des sous-titres",
"app.actionsBar.captions.start": "Démarrer l'affichage des sous-titres SEM",
"app.actionsBar.captions.stop": "Arrêter l'affichage des sous-titres SEM",
"app.audioNotification.audioFailedError1001": "WebSocket déconnecté (erreur 1001)",
"app.audioNotification.audioFailedError1002": "Échec de la connexion WebSocket (erreur 1002)",
"app.audioNotification.audioFailedError1003": "Version du navigateur non supportée (erreur 1003)",
"app.audioNotification.audioFailedError1003": "Version du navigateur incompatible (erreur 1003)",
"app.audioNotification.audioFailedError1004": "Échec lors de l'appel (raison={0}) (erreur 1004)",
"app.audioNotification.audioFailedError1005": "L'appel s'est terminé de façon inattendue (erreur 1005)",
"app.audioNotification.audioFailedError1006": "Délai d'appel dépassé (erreur 1006)",
"app.audioNotification.audioFailedError1007": "Échec de la connexion (erreur ICE 1007)",
"app.audioNotification.audioFailedError1008": "Échec du transfert (erreur 1008)",
"app.audioNotification.audioFailedError1009": "impossible de récupérer les informations du serveur STUN/TURN (erreur 1009)",
"app.audioNotification.audioFailedError1010": "Délai dépassé durant la négociation (erreur ICE 1010)",
"app.audioNotification.audioFailedError1010": "Délai dépassé durant la négociation pour la connexion (erreur ICE 1010)",
"app.audioNotification.audioFailedError1011": "Délai d'attente de connexion dépassé (erreur ICE 1011)",
"app.audioNotification.audioFailedError1012": "Connexion fermée (erreur ICE 1012)",
"app.audioNotification.audioFailedMessage": "Votre connexion en mode audio a échoué",
"app.audioNotification.mediaFailedMessage": "getUserMicMedia a échoué car seules les origines sécurisées sont autorisées",
"app.audioNotification.deviceChangeFailed": "Le changement d'appareil audio a échoué. Vérifiez que l'appareil est bien installé et disponible.",
"app.audioNotification.mediaFailedMessage": "getUserMicMedia a échoué car seules les sources sécurisées sont autorisées",
"app.audioNotification.deviceChangeFailed": "Le changement d'équipement audio a échoué. Vérifiez que l'appareil est bien installé et disponible.",
"app.audioNotification.closeLabel": "Fermer",
"app.audioNotificaion.reconnectingAsListenOnly": "Le microphone est verrouillé pour les participants, vous êtes connecté en mode écoute uniquement.",
"app.audioNotificaion.reconnectingAsListenOnly": "Le microphone est verrouillé pour les participants, vous êtes connecté uniquement en mode auditeur.",
"app.breakoutJoinConfirmation.title": "Rejoindre le groupe de travail",
"app.breakoutJoinConfirmation.message": "Voulez-vous rejoindre la séance",
"app.breakoutJoinConfirmation.confirmDesc": "Rejoignez le groupe de travail",
"app.breakoutJoinConfirmation.dismissLabel": "Annuler",
"app.breakoutJoinConfirmation.dismissDesc": "Fermer et refuser de rejoindre le groupe de travail",
"app.breakoutJoinConfirmation.dismissDesc": "Ferme et rejette l'intégration au groupe de travail",
"app.breakoutJoinConfirmation.freeJoinMessage": "Choisissez une groupe de travail à rejoindre",
"app.breakoutTimeRemainingMessage": "Temps restant pour le groupe de travail : {0}",
"app.breakoutWillCloseMessage": "Le temps s'est écoulé. La groupe de travail fermera bientôt",
@ -641,12 +641,12 @@
"app.breakout.dropdown.options": "Options des groupes de travail",
"app.breakout.dropdown.manageUsers": "Gestion des utilisateurs",
"app.calculatingBreakoutTimeRemaining": "Calcul du temps restant...",
"app.audioModal.ariaTitle": "Fenêtre modale pour joindre la réunion en audio",
"app.audioModal.microphoneLabel": "Microphone",
"app.audioModal.ariaTitle": "Choix du mode audio pour rejoindre la réunion",
"app.audioModal.microphoneLabel": "Parler et écouter",
"app.audioModal.listenOnlyLabel": "Écoute seule",
"app.audioModal.microphoneDesc": "Rejoint la réunion audio en utilisant un micro",
"app.audioModal.listenOnlyDesc": "Rejoint la réunion audio en écoute seule",
"app.audioModal.audioChoiceLabel": "Comment désirez-vous rejoindre la réunion audio ?",
"app.audioModal.microphoneDesc": "Rejoint la réunion en utilisant un micro",
"app.audioModal.listenOnlyDesc": "Rejoint la réunion en écoute seule",
"app.audioModal.audioChoiceLabel": "Comment désirez-vous rejoindre la réunion ?",
"app.audioModal.iOSBrowser": "Audio / Vidéo non pris en charge",
"app.audioModal.iOSErrorDescription": "Actuellement l'audio et la vidéo ne sont pas pris en charge par Chrome sur iOS.",
"app.audioModal.iOSErrorRecommendation": "Nous recommandons d'utiliser Safari iOS.",
@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Une erreur s'est produite",
"app.error.fallback.presentation.description": "Cela a été consigné. Essayez de recharger la page s'il vous plaît.",
"app.error.fallback.presentation.reloadButton": "Recharger",
"app.guest.waiting": "En attente de l'approbation pour rejoindre la séance",
"app.guest.errorSeeConsole": "Erreur: plus de détail dans la console",
"app.guest.noModeratorResponse": "Aucune réponse du modérateur.",
"app.guest.noSessionToken": "Aucun jeton de session reçu.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Produciuse un erro",
"app.error.fallback.presentation.description": "Accedeu. Tente volver cargar a páxina.",
"app.error.fallback.presentation.reloadButton": "Recargar",
"app.guest.waiting": "Agardando a aprobación para unirse",
"app.guest.errorSeeConsole": "Erro: máis detalles na consola.",
"app.guest.noModeratorResponse": "Non hai resposta do moderador.",
"app.guest.noSessionToken": "Non se recibiu ningún testemuño de sesión.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Hiba lépett fel",
"app.error.fallback.presentation.description": "Naplóztuk. Kérjük, próbáld újra betölteni az oldalt.",
"app.error.fallback.presentation.reloadButton": "Újratöltése",
"app.guest.waiting": "Várakozás jóváhagyásra",
"app.guest.errorSeeConsole": "Hiba: további részletek a konzolban.",
"app.guest.noModeratorResponse": "A szervező elfoglalt. Kérlek, kíséreld meg a csatlakozást kicsit később!",
"app.guest.noSessionToken": "Nem érkezett munkamenet Token.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Տեղի է ունեցել սխալ",
"app.error.fallback.presentation.description": "Գործողությունը գրանցվել է Խնդրում ենք վերբեռնել /Refresh/ էջը",
"app.error.fallback.presentation.reloadButton": "Վերբեռնել",
"app.guest.waiting": "Մուտքի թույլտվության սպասում",
"app.guest.errorSeeConsole": "Ошибка: Более подробная информация в консоли.",
"app.guest.noModeratorResponse": "Մոդերատորից պատասխան չի ստացվել",
"app.guest.noSessionToken": "Սեանսի թոքեն չի ստացվել",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "エラーが発生しました",
"app.error.fallback.presentation.description": "記録されました。ページを再読み込みしてください。",
"app.error.fallback.presentation.reloadButton": "再読み込み",
"app.guest.waiting": "主催者の承認待ち",
"app.guest.errorSeeConsole": "エラー:詳細はコンソールに表示。",
"app.guest.noModeratorResponse": "司会者からの反応がありません。",
"app.guest.noSessionToken": "会議の参加許可が得られていません。",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Произошла ошибка",
"app.error.fallback.presentation.description": "Действие было зарегистрировано. Пожалуйста, попробуйте перезагрузить страницу",
"app.error.fallback.presentation.reloadButton": "Перезагрузить",
"app.guest.waiting": "Ожидание одобрения входа",
"app.guest.errorSeeConsole": "Ошибка: Более подробная информация в консоли.",
"app.guest.noModeratorResponse": "Нет ответа от Модератора.",
"app.guest.noSessionToken": "Токен вебинара не получен.",

View File

@ -767,7 +767,6 @@
"app.error.fallback.presentation.title": "Bir sorun çıktı",
"app.error.fallback.presentation.description": "Oturum açıldı. Lütfen sayfayı yeniden yüklemeyi deneyin.",
"app.error.fallback.presentation.reloadButton": "Yeniden yükle",
"app.guest.waiting": "Katılma onayı bekleniyor",
"app.guest.errorSeeConsole": "Hata: Konsoldan ayrıntılı bilgi alabilirsiniz.",
"app.guest.noModeratorResponse": "Sorumlu yanıt vermedi.",
"app.guest.noSessionToken": "Herhangi bir oturum kodu alınmadı.",

3
bigbluebutton-tests/gns3/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
NPDC
bbb-dev-ca.crt
bbb-dev-ca.key

View File

@ -0,0 +1 @@
git clone --branch master --depth 1 https://github.com/BrentBaccala/NPDC NPDC

View File

@ -35,11 +35,10 @@ Some special names are defined. Requesting a device name starting with `testcli
## Usage
1. You'll need several tools from Brent Baccala's NPDC repository on github, which is a submodule in the NPDC directory, so run this command to fetch it:
1. You'll need several tools from Brent Baccala's NPDC repository on github, so run this command to fetch it:
```
git submodule init
git submodule update
./NPDC.placeholder.sh
```
1. Read, understand, and run the `install-gns3.sh` script in `NPDC/GNS3`

View File

@ -28,9 +28,9 @@ $ npm test
You can also run a single test suite and limit the execution to only one browser:
```bash
$ npx playwright test chat --browser=firefox
$ npx playwright test chat --project="firefox"
or
$ npm test chat -- --browser=firefox
$ npm test chat -- --project="firefox" # or "chromium" for example
```
#### Additional commands

View File

@ -0,0 +1,50 @@
const chromiumConfig = {
name: 'Chromium',
use: {
browserName: 'chromium',
launchOptions: {
args: [
'--no-sandbox',
'--ignore-certificate-errors',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
'--allow-file-access-from-files',
],
},
},
};
const firefoxConfig = {
name: 'Firefox',
use: {
browserName: 'firefox',
launchOptions: {
args: [
'--quiet',
'--use-test-media-devices',
],
firefoxUserPrefs: {
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true,
}
},
},
};
const webkitConfig = {
name: 'WebKit',
use: {
browserName: 'webkit',
launchOptions: {
args: [
'--no-sandbox',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
]
},
},
};
exports.chromiumConfig = chromiumConfig;
exports.firefoxConfig = firefoxConfig;
exports.webkitConfig = webkitConfig;

View File

@ -336,6 +336,7 @@ exports.connectionStatusItemUser = 'div[data-test="connectionStatusItemUser"]';
exports.connectionStatusLinkToSettings = `${networkDataContainer} span[role="button"]`;
exports.dataSavingWebcams = 'input[data-test="dataSavingWebcams"]';
exports.connectionStatusOfflineUser = 'div[data-test="offlineUser"]';
exports.videoUploadRateData = 'div[data-test="videoUploadRateData"]';
exports.connectionDataContainer = networkDataContainer;
exports.avatarsWrapperAvatar = 'div[data-test="avatarsWrapperAvatar"]';
exports.guestPolicyLabel = 'li[data-test="guestPolicyLabel"]';
@ -398,7 +399,19 @@ exports.startSharingWebcam = 'button[data-test="startSharingWebcam"]';
exports.webcamConnecting = 'div[data-test="webcamConnecting"]';
exports.webcamContainer = 'video[data-test="videoContainer"]';
exports.webcamVideoItem = 'div[data-test="webcamVideoItem"]';
exports.videoDropdownMenu = 'button[data-test="videoDropdownMenu"]';
exports.advancedVideoSettingsBtn = 'li[data-test="advancedVideoSettingsButton"]';
exports.mirrorWebcamBtn = 'li[data-test="mirrorWebcamBtn"]';
exports.focusWebcamBtn = 'li[data-test="focusWebcamBtn"]';
exports.pinWebcamBtn = 'li[data-test="pinWebcamBtn"]';
exports.webcamFullscreenButton = 'button[data-test="webcamFullscreenButton"]';
exports.selectDefaultBackground = 'button[data-test="selectDefaultBackground"]';
exports.selectCustomBackground = 'button[data-test="selectCustomBackground"]';
exports.removeCustomBackground = 'button[data-test="removeCustomBackground"]';
exports.inputBackgroundButton = 'button[data-test="inputBackgroundButton"]';
exports.noneBackgroundButton = 'button[data-test="noneBackgroundButton"]';
exports.videoQualitySelector = 'select[id="setQuality"]';
exports.webcamItemTalkingUser = 'div[data-test="webcamItemTalkingUser"]';
exports.webcamSettingsModal = 'div[data-test="webcamSettingsModal"]';
exports.dropdownWebcamButton = 'div[data-test="dropdownWebcamButton"]';
@ -458,7 +471,7 @@ exports.cameraDock = 'video[data-test="videoContainer"]';
// Font size
exports.increaseFontSize = 'button[data-test="increaseFontSize"]';
exports.descreaseFontSize = 'button[data-test="decreaseFontSize"]';
exports.decreaseFontSize = 'button[data-test="decreaseFontSize"]';
// Learning dashboard
exports.learningDashboard = 'li[data-test="learningDashboard"]';
@ -477,3 +490,12 @@ exports.pollYesNoAnswer = 'div[role="cell"]:nth-child(5)';
exports.pollUserResponseQuestion = 'div[role="rowgroup"] div:nth-child(6) div';
exports.pollUserResponseAnswer = 'div[role="cell"]:nth-child(6)';
exports.pollTotal = 'div[role="cell"]:nth-child(2)';
exports.meetingStatusActiveDashboard = 'span[data-test="meetingStatusActiveDashboard"]';
exports.meetingDurationTimeDashboard = 'p[data-test="meetingDurationTimeDashboard"]';
exports.userNameDashboard = 'td[data-test="userLabelDashboard"] button';
exports.userWebcamTimeDashboard = 'td[data-test="userWebcamTimeDashboard"]';
exports.userRaiseHandDashboard = 'td[data-test="userRaiseHandDashboard"]';
exports.userStatusDashboard = 'td[data-test="userStatusDashboard"]';
exports.userActivityScoreDashboard = 'td[data-test="userActivityScoreDashboard"]';
exports.activityScorePanelDashboard = 'button[data-test="activityScorePanelDashboard"]';
exports.downloadSessionLearningDashboard = 'button[data-test="downloadSessionDataDashboard"]';

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

@ -19,6 +19,10 @@ function checkElementLengthEqualTo([element, count]) {
return document.querySelectorAll(element).length == count;
}
function getElementLength(element) {
return document.querySelectorAll(element).length;
}
// Text
async function checkTextContent(baseContent, checkData) {
if (typeof checkData === 'string') checkData = new Array(checkData);
@ -38,5 +42,6 @@ function constructClipObj(wbBox) {
exports.checkElement = checkElement;
exports.checkElementLengthEqualTo = checkElementLengthEqualTo;
exports.getElementLength = getElementLength;
exports.checkTextContent = checkTextContent;
exports.constructClipObj = constructClipObj;

View File

@ -5,7 +5,8 @@ const { expect } = require("@playwright/test");
const Page = require("../core/page");
const { sleep } = require("../core/helpers");
const { ELEMENT_WAIT_EXTRA_LONG_TIME } = require("../core/constants");
const { openPoll, timeInSeconds } = require("./util");
const { openPoll, timeInSeconds, rowFilter } = require("./util");
const { checkTextContent } = require('../core/util');
class LearningDashboard extends MultiUsers {
constructor(browser, context) {
@ -93,6 +94,8 @@ class LearningDashboard extends MultiUsers {
//Checks
await this.dashboardPage.reloadPage();
const activityScore = await rowFilter(this.dashboardPage, 'tr', /Attendee/, e.userActivityScoreDashboard);
await expect(activityScore).toHaveText(/2/, { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME });
await this.dashboardPage.waitAndClick(e.pollPanel);
await this.dashboardPage.hasText(e.pollTotal, '4', ELEMENT_WAIT_EXTRA_LONG_TIME);
@ -112,6 +115,70 @@ class LearningDashboard extends MultiUsers {
await this.dashboardPage.hasText(e.pollUserResponseQuestion, 'User response?');
await this.dashboardPage.hasText(e.pollUserResponseAnswer, e.answerMessage);
}
async basicInfos() {
// Meeting Status check
await this.dashboardPage.hasText(e.meetingStatusActiveDashboard, 'Active');
await this.dashboardPage.reloadPage();
// Meeting Time Duration check
const timeLocator = this.dashboardPage.getLocator(e.meetingDurationTimeDashboard);
const timeContent = await (timeLocator).textContent();
const array = timeContent.split(':').map(Number);
const firstTime = array[1] * 3600 + array[2] * 60 + array[3];
await sleep(5000);
await this.dashboardPage.reloadPage();
const timeContentGreater = await (timeLocator).textContent();
const arrayGreater = timeContentGreater.split(':').map(Number);
const secondTime = arrayGreater[1] * 3600 + arrayGreater[2] * 60 + arrayGreater[3];
await expect(secondTime).toBeGreaterThan(firstTime);
}
async overview() {
await this.modPage.waitAndClick(e.joinVideo);
await this.modPage.waitAndClick(e.startSharingWebcam);
await this.modPage.waitAndClick(e.raiseHandBtn);
await this.dashboardPage.reloadPage();
// User Name check
const userNameCheck = await rowFilter(this.dashboardPage, 'tr', /Moderator/, e.userNameDashboard);
await expect(userNameCheck).toHaveText(/Moderator/, { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME });
// Webcam Time check
const webcamCheck = await rowFilter(this.dashboardPage, 'tr', /Moderator/, e.userWebcamTimeDashboard);
await expect(webcamCheck).toHaveText(/00/, { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME });
// Raise Hand check
const raiseHandCheck = await rowFilter(this.dashboardPage, 'tr', /Moderator/, e.userRaiseHandDashboard);
await expect(raiseHandCheck).toHaveText(/1/, { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME });
// Current Status check
const userStatusCheck = await rowFilter(this.dashboardPage, 'tr', /Moderator/, e.userStatusDashboard);
await expect(userStatusCheck).toHaveText(/Online/, { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME });
}
async downloadSessionLearningDashboard(testInfo) {
await this.modPage.waitAndClick(e.optionsButton);
await this.modPage.waitAndClick(e.logout);
await this.modPage.waitAndClick('button');
const downloadSessionLocator = this.dashboardPage.getLocator(e.downloadSessionLearningDashboard);
const dataCSV = await this.dashboardPage.handleDownload(downloadSessionLocator, testInfo);
const dataToCheck = [
'Moderator',
'Activity Score',
'Talk time',
'Webcam Time',
'Messages',
'Emojis',
'Poll Votes',
'Raise Hands',
'Left',
'Join',
'Duration',
]
await checkTextContent(dataCSV.content, dataToCheck);
}
}
exports.LearningDashboard = LearningDashboard;

View File

@ -15,12 +15,24 @@ test.describe.serial('Learning Dashboard', async () => {
await learningDashboard.writeOnPublicChat();
});
test('Meeting Duration Time', async() => {
test('User Time On Meeting', async() => {
await learningDashboard.userTimeOnMeeting();
});
test('Polls', async ({ context })=> {
test('Polls', async ({ context }) => {
await learningDashboard.initUserPage(true, context);
await learningDashboard.polls();
});
test('Basic Infos', async () => {
await learningDashboard.basicInfos();
});
test('Overview', async () => {
await learningDashboard.overview();
});
test('Download Session Learning Dashboard', async ({ context }, testInfo) => {
await learningDashboard.downloadSessionLearningDashboard(testInfo);
});
});

View File

@ -15,5 +15,11 @@ function timeInSeconds(locator){
return hours * 3600 + minutes * 60 + seconds;
}
async function rowFilter(testPage, locator, role, selector) {
const locatorToFilter = await testPage.getLocator(locator);
return locatorToFilter.filter({ hasText: role }).locator(selector);
}
exports.openPoll = openPoll;
exports.timeInSeconds = timeInSeconds;
exports.rowFilter = rowFilter;

View File

@ -135,8 +135,8 @@ class Options extends Page {
// Decreasing font size
await openSettings(this);
await this.waitAndClick(e.descreaseFontSize);
await this.waitAndClick(e.descreaseFontSize);
await this.waitAndClick(e.decreaseFontSize);
await this.waitAndClick(e.decreaseFontSize);
await this.waitAndClick(e.modalConfirmButton);
await this.fontSizeCheck(e.chatButton, '12px');

View File

@ -1,4 +1,5 @@
require('dotenv').config();
const { chromiumConfig, firefoxConfig, webkitConfig } = require('./core/browsersConfig');
const CI = process.env.CI === 'true';
const DEBUG_MODE = process.env.DEBUG_MODE === 'true';
@ -20,45 +21,9 @@ const config = {
video: 'on',
},
projects: [
{
name: 'Chromium',
use: {
browserName: 'chromium',
launchOptions: {
args: [
'--no-sandbox',
'--ignore-certificate-errors',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
]
},
},
},
{
name: 'Firefox',
use: {
browserName: 'firefox',
launchOptions: {
firefoxUserPrefs: {
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true,
}
},
},
},
{
name: 'WebKit',
use: {
browserName: 'webkit',
launchOptions: {
args: [
'--no-sandbox',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
]
},
},
},
chromiumConfig,
firefoxConfig,
webkitConfig,
],
};

View File

@ -49,6 +49,12 @@ class SharedNotes extends MultiUsers {
await this.editMessage(notesLocator);
const editedMessage = '!Hello';
await expect(notesLocator).toContainText(editedMessage, { timeout: ELEMENT_WAIT_TIME });
const wbBox = await this.modPage.getElementBoundingBox(e.etherpadFrame);
await expect(this.modPage.page).toHaveScreenshot('sharednotes-1.png', {
maxDiffPixels: 10,
clip: wbBox,
});
}
async formatMessage() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -4,11 +4,11 @@ const Page = require('../core/page');
const e = require('../core/elements');
const { waitAndClearDefaultPresentationNotification } = require('../notifications/util');
const { sleep } = require('../core/helpers');
const { checkTextContent, checkElementLengthEqualTo } = require('../core/util');
const { checkAvatarIcon, checkIsPresenter, checkMutedUsers } = require('./util');
const { getNotesLocator } = require('../sharednotes/util');
const { checkTextContent } = require('../core/util');
const { getSettings } = require('../core/settings');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { ELEMENT_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
class MultiUsers {
constructor(browser, context) {
@ -233,6 +233,32 @@ class MultiUsers {
await this.userPage2.wasRemoved(e.selectedUserName);
}
async pinningWebcams() {
await this.modPage.shareWebcam();
await this.modPage2.shareWebcam();
await this.userPage.shareWebcam();
await this.modPage.page.waitForFunction(
checkElementLengthEqualTo,
[e.webcamVideoItem, 3],
{ timeout: ELEMENT_WAIT_TIME },
);
// Pin first webcam (Mod2)
await this.modPage.waitAndClick(`:nth-match(${e.dropdownWebcamButton}, 3)`);
await this.modPage.waitAndClick(`:nth-match(${e.pinWebcamBtn}, 2)`);
await this.modPage.hasText(`:nth-match(${e.dropdownWebcamButton}, 1)`, this.modPage2.username);
await this.modPage2.hasText(`:nth-match(${e.dropdownWebcamButton}, 1)`, this.modPage2.username);
await this.userPage.hasText(`:nth-match(${e.dropdownWebcamButton}, 1)`, this.modPage2.username);
// Pin second webcam (user)
await this.modPage.waitAndClick(`:nth-match(${e.dropdownWebcamButton}, 3)`);
await this.modPage.waitAndClick(`:nth-match(${e.pinWebcamBtn}, 3)`);
await this.modPage.hasText(`:nth-match(${e.dropdownWebcamButton}, 1)`, this.userPage.username);
await this.modPage.hasText(`:nth-match(${e.dropdownWebcamButton}, 2)`, this.modPage2.username);
await this.userPage.hasText(`:nth-match(${e.dropdownWebcamButton}, 1)`, this.modPage2.username);
await this.userPage.hasText(`:nth-match(${e.dropdownWebcamButton}, 2)`, this.userPage.username);
await this.modPage2.hasText(`:nth-match(${e.dropdownWebcamButton}, 1)`, this.userPage.username);
await this.modPage2.hasText(`:nth-match(${e.dropdownWebcamButton}, 2)`, this.modPage2.username);
}
async whiteboardAccess() {
await this.modPage.waitForSelector(e.whiteboard);
await this.modPage.waitAndClick(e.userListItem);

View File

@ -1,6 +1,8 @@
const e = require('../core/elements');
const { sleep } = require('../core/helpers');
const { LOOP_INTERVAL, ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { LOOP_INTERVAL, ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } = require('../core/constants');
const { expect } = require('@playwright/test');
const { resolve } = require('path');
// loop 5 times, every LOOP_INTERVAL milliseconds, and check that all
// videos displayed are changing by comparing a hash of their
@ -43,4 +45,26 @@ async function webcamContentCheck(test) {
return check === true;
}
async function checkVideoUploadData(testPage, previousValue, timeout = ELEMENT_WAIT_TIME) {
const locator = testPage.getLocator(e.videoUploadRateData);
await expect(locator).not.toHaveText('0k ↑', { timeout });
const currentValue = await Number((await locator.textContent()).split('k')[0]);
await expect(currentValue).toBeGreaterThan(previousValue);
return currentValue;
}
async function uploadBackgroundVideoImage(testPage) {
const [fileChooser] = await Promise.all([
testPage.page.waitForEvent('filechooser'),
testPage.waitAndClick(e.inputBackgroundButton),
]);
await fileChooser.setFiles(resolve(__dirname, '../core/media/simpsons-background.png'));
const uploadedBackgroundLocator = testPage.getLocator(e.selectCustomBackground);
await expect(uploadedBackgroundLocator).toHaveScreenshot('custom-background-item.png', {
maxDiffPixelRatio: 0.1,
});
}
exports.webcamContentCheck = webcamContentCheck;
exports.checkVideoUploadData = checkVideoUploadData;
exports.uploadBackgroundVideoImage = uploadBackgroundVideoImage;

View File

@ -1,8 +1,9 @@
const { expect } = require('@playwright/test');
const Page = require('../core/page');
const e = require('../core/elements');
const { webcamContentCheck } = require('./util');
const { VIDEO_LOADING_WAIT_TIME } = require('../core/constants');
const { checkVideoUploadData, uploadBackgroundVideoImage, webcamContentCheck } = require('./util');
const { VIDEO_LOADING_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { sleep } = require('../core/helpers');
class Webcam extends Page {
constructor(browser, page) {
@ -13,11 +14,17 @@ class Webcam extends Page {
const { videoPreviewTimeout, skipVideoPreview, skipVideoPreviewOnFirstJoin } = this.settings;
await this.shareWebcam(!(skipVideoPreview || skipVideoPreviewOnFirstJoin), videoPreviewTimeout);
await this.hasElement('video');
await this.hasElement(e.videoDropdownMenu);
await this.waitAndClick(e.leaveVideo);
await this.hasElement(e.joinVideo);
await this.wasRemoved('video');
}
async checksContent() {
const { videoPreviewTimeout, skipVideoPreview, skipVideoPreviewOnFirstJoin } = this.settings;
await this.shareWebcam(!(skipVideoPreview || skipVideoPreviewOnFirstJoin), videoPreviewTimeout);
await this.waitForSelector(e.webcamVideoItem);
await this.wasRemoved(e.webcamConnecting, ELEMENT_WAIT_LONGER_TIME);
const respUser = await webcamContentCheck(this);
await expect(respUser).toBeTruthy();
}
@ -30,6 +37,94 @@ class Webcam extends Page {
await this.hasElement(e.webcamItemTalkingUser);
}
async changeVideoQuality() {
const { videoPreviewTimeout } = this.settings;
const joinWebcamSettingQuality = async (value) => {
await this.waitAndClick(e.joinVideo);
await this.waitForSelector(e.videoQualitySelector);
const langDropdown = await this.page.$(e.videoQualitySelector);
await langDropdown.selectOption({ value });
await this.waitForSelector(e.videoPreview, videoPreviewTimeout);
await this.waitAndClick(e.startSharingWebcam);
await this.waitForSelector(e.webcamConnecting);
await this.waitForSelector(e.leaveVideo, VIDEO_LOADING_WAIT_TIME);
}
await joinWebcamSettingQuality('low');
await this.waitAndClick(e.connectionStatusBtn);
const lowValue = await checkVideoUploadData(this, 0);
await this.waitAndClick(e.closeModal);
await this.waitAndClick(e.leaveVideo);
await joinWebcamSettingQuality('high');
await this.waitAndClick(e.connectionStatusBtn);
await checkVideoUploadData(this, lowValue);
}
async applyBackground() {
await this.waitAndClick(e.joinVideo);
await this.waitForSelector(e.noneBackgroundButton);
await this.waitAndClick(`${e.selectDefaultBackground}[aria-label="Home"]`);
await sleep(1000);
await this.waitAndClick(e.startSharingWebcam);
await this.waitForSelector(e.webcamContainer);
const webcamVideoLocator = await this.getLocator(e.webcamContainer);
await expect(webcamVideoLocator).toHaveScreenshot('webcam-with-home-background.png', {
maxDiffPixelRatio: 0.1,
});
}
async webcamFullscreen() {
await this.shareWebcam();
// get default viewport sizes
const { windowWidth, windowHeight } = await this.page.evaluate(() => { return {
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
}});
await this.waitAndClick(e.webcamFullscreenButton);
// get fullscreen webcam size
const { width, height } = await this.getLocator('video').boundingBox();
await expect(width + 1).toBe(windowWidth); // not sure why there is a difference of 1 pixel
await expect(height).toBe(windowHeight);
}
async managingNewBackground() {
await this.waitAndClick(e.joinVideo);
await this.waitForSelector(e.noneBackgroundButton);
// Upload
await uploadBackgroundVideoImage(this);
// Apply
await this.waitAndClick(e.selectCustomBackground);
await sleep(1000);
await this.waitAndClick(e.startSharingWebcam);
await this.waitForSelector(e.webcamContainer);
const webcamVideoLocator = await this.getLocator(e.webcamContainer);
await expect(webcamVideoLocator).toHaveScreenshot('webcam-with-new-background.png', {
maxDiffPixelRatio: 0.1,
});
// Remove
await this.waitAndClick(e.videoDropdownMenu);
await this.waitAndClick(e.advancedVideoSettingsBtn);
await this.waitAndClick(e.removeCustomBackground);
await this.wasRemoved(e.selectCustomBackground);
}
async keepBackgroundWhenRejoin(context) {
await this.waitAndClick(e.joinVideo);
await this.waitForSelector(e.noneBackgroundButton);
await uploadBackgroundVideoImage(this);
// Create a new page before closing the previous to not close the current context
await context.newPage();
await this.page.close();
const openedPage = await this.getLastTargetPage(context);
await openedPage.init(true, true, { meetingId: this.meetingId });
await openedPage.waitAndClick(e.joinVideo);
await openedPage.hasElement(e.selectCustomBackground);
}
async webcamLayoutStart() {
await this.joinMicrophone();
const { videoPreviewTimeout, skipVideoPreview, skipVideoPreviewOnFirstJoin } = this.settings;

View File

@ -1,4 +1,5 @@
const { test } = require('@playwright/test');
const { MultiUsers } = require('../user/multiusers');
const { Webcam } = require('./webcam');
test.describe.parallel('Webcam @ci', () => {
@ -10,15 +11,54 @@ test.describe.parallel('Webcam @ci', () => {
});
test('Checks content of webcam', async ({ browser, page }) => {
test.fixme(true, 'The test is not as reliable as it should be: getting unexpected failures');
const webcam = new Webcam(browser, page);
await webcam.init(true, true);
await webcam.checksContent();
});
test('Checks webcam talking indicator', async ({ browser, page }) => {
test('Webcam talking indicator', async ({ browser, page }) => {
const webcam = new Webcam(browser, page);
await webcam.init(true, false);
await webcam.talkingIndicator();
});
test('Pinning and unpinning webcams', async ({ browser, context, page }) => {
const webcam = new MultiUsers(browser, context);
await webcam.initModPage(page);
await webcam.initUserPage();
await webcam.initModPage2();
await webcam.pinningWebcams();
});
test('Change video quality', async ({ browser, page }) => {
const webcam = new Webcam(browser, page);
await webcam.init(true, true);
await webcam.changeVideoQuality();
});
test('Webcam fullscreen', async ({ browser, page }) => {
const webcam = new Webcam(browser, page);
await webcam.init(true, true);
await webcam.webcamFullscreen();
});
test.describe('Webcam background', () => {
test('Select one of the default backgrounds', async ({ browser, page }) => {
const webcam = new Webcam(browser, page);
await webcam.init(true, true);
await webcam.applyBackground();
});
test('Managing new background', async ({ browser, page }) => {
const webcam = new Webcam(browser, page);
await webcam.init(true, true);
await webcam.managingNewBackground();
});
test('Keep background when rejoin', async ({ browser, context, page }) => {
const webcam = new Webcam(browser, page);
await webcam.init(true, true);
await webcam.keepBackgroundWhenRejoin(context);
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -15,6 +15,7 @@ mkdir -p artifacts
DOCKER_IMAGE=$(python3 -c 'import yaml; print(yaml.load(open("./.gitlab-ci.yml"), Loader=yaml.SafeLoader)["default"]["image"])')
LOCAL_BUILD=1
if [ "$LOCAL_BUILD" != 1 ] ; then
GIT_REV="${CI_COMMIT_SHA:0:10}"
else
@ -43,7 +44,7 @@ trap 'kill_docker' SIGINT SIGTERM
# -v "$CACHE_DIR/dev":/root/dev
sudo docker run --rm --detach --cidfile $DOCKER_CONTAINER_ID_FILE \
--env GIT_REV=$GIT_REV --env COMMIT_DATE=$COMMIT_DATE --env "LOCAL_BUILD=1" \
--env GIT_REV=$GIT_REV --env COMMIT_DATE=$COMMIT_DATE --env LOCAL_BUILD=$LOCAL_BUILD \
--mount type=bind,src="$PWD",dst=/mnt \
--mount type=bind,src="${PWD}/artifacts,dst=/artifacts" \
-t "$DOCKER_IMAGE" /mnt/build/setup-inside-docker.sh "$PACKAGE_TO_BUILD"

View File

@ -1328,6 +1328,7 @@ Useful tools for development:
| `userdata-bbb_skip_check_audio=` | If set to `true`, the user will not see the "echo test" prompt when sharing audio | `false` |
| `userdata-bbb_skip_check_audio_on_first_join=` | (Introduced in BigBlueButton 2.3) If set to `true`, the user will not see the "echo test" when sharing audio for the first time in the session. If the user stops sharing, next time they try to share audio the echo test window will be displayed, allowing for configuration changes to be made prior to sharing audio again | `false` |
| `userdata-bbb_override_default_locale=` | (Introduced in BigBlueButton 2.3) If set to `de`, the user's browser preference will be ignored - the client will be shown in 'de' (i.e. German) regardless of the otherwise preferred locale 'en' (or other) | `null` |
| `userdata-bbb_hide_presentation_on_join` | (Introduced in BigBlueButton 2.6) If set to `true` it will make the user enter the meeting with presentation minimized (Only for non-presenters), not peremanent. | `false` |
#### Branding parameters

370
docs/docs/data/create.tsx Normal file
View File

@ -0,0 +1,370 @@
import React from 'react';
const createEndpointTableData = [
{
"name": "name",
"required": true,
"type": "String",
"description": (<>A name for the meeting. This is now required as of BigBlueButton 2.4.</>)
},
{
"name": "meetingID",
"required": true,
"type": "String",
"description": (<>A meeting ID that can be used to identify this meeting by the 3rd-party application. <br /><br /> This must be unique to the server that you are calling: different active meetings can not have the same meeting ID. <br /><br /> If you supply a non-unique meeting ID (a meeting is already in progress with the same meeting ID), then if the other parameters in the create call are identical, the create call will succeed (but will receive a warning message in the response). The create call is idempotent: calling multiple times does not have any side effect. This enables a 3rd-party applications to avoid checking if the meeting is running and always call create before joining each user.<br /><br /> Meeting IDs should only contain upper/lower ASCII letters, numbers, dashes, or underscores. A good choice for the meeting ID is to generate a <a href='https://en.wikipedia.org/wiki/Globally_unique_identifier'>GUID</a> value as this all but guarantees that different meetings will not have the same meetingID.</>)
},
{
"name": "attendeePW",
"type": "String",
"description": (<>
<b>[DEPRECATED]</b> The password that the <a href="#join">join</a> URL can later provide as its <code className="language-plaintext highlighter-rouge">password</code> parameter to indicate the user will join as a viewer. If no <code className="language-plaintext highlighter-rouge">attendeePW</code> is provided, the <code className="language-plaintext highlighter-rouge">create</code> call will return a randomly generated <code className="language-plaintext highlighter-rouge">attendeePW</code> password for the meeting.
</>)
},
{
"name": "moderatorPW",
"type": "String",
"description": (<><b>[DEPRECATED]</b> The password that will <a href="#join">join</a> URL can later provide as its <code className="language-plaintext highlighter-rouge">password</code> parameter to indicate the user will as a moderator. if no <code className="language-plaintext highlighter-rouge">moderatorPW</code> is provided, <code className="language-plaintext highlighter-rouge">create</code> will return a randomly generated <code className="language-plaintext highlighter-rouge">moderatorPW</code> password for the meeting.</>)
},
{
"name": "welcome",
"required": false,
"type": "String",
"description": (<>A welcome message that gets displayed on the chat window when the participant joins. You can include keywords (<code className="language-plaintext highlighter-rouge">%%CONFNAME%%</code>, <code className="language-plaintext highlighter-rouge">%%DIALNUM%%</code>, <code className="language-plaintext highlighter-rouge">%%CONFNUM%%</code>) which will be substituted automatically.<br /><br /> This parameter overrides the default <code className="language-plaintext highlighter-rouge">defaultWelcomeMessage</code> in <code className="language-plaintext highlighter-rouge">bigbluebutton.properties</code>.<br /><br /> The welcome message has limited support for HTML formatting. Be careful about copy/pasted HTML from e.g. MS Word, as it can easily exceed the maximum supported URL length when used on a GET request.</>)
},
{
"name": "dialNumber",
"required": false,
"type": "String",
"description": (<>The dial access number that participants can call in using regular phone. You can set a default dial number via <code className="language-plaintext highlighter-rouge">defaultDialAccessNumber</code> in <a href="https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties">bigbluebutton.properties</a></>)
},
{
"name": "voiceBridge",
"required": false,
"type": "String",
"description": (<>Voice conference number for the FreeSWITCH voice conference associated with this meeting. This must be a 5-digit number in the range 10000 to 99999. If you <a href="/2.2/customize.html#add-a-phone-number-to-the-conference-bridge">add a phone number</a> to your BigBlueButton server, This parameter sets the personal identification number (PIN) that FreeSWITCH will prompt for a phone-only user to enter. If you want to change this range, edit FreeSWITCH dialplan and <code className="language-plaintext highlighter-rouge">defaultNumDigitsForTelVoice</code> of <a href="https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties">bigbluebutton.properties</a>.<br /><br />The <code className="language-plaintext highlighter-rouge">voiceBridge</code> number must be different for every meeting.<br /><br />This parameter is optional. If you do not specify a <code className="language-plaintext highlighter-rouge">voiceBridge</code> number, then BigBlueButton will assign a random unused number for the meeting.<br /><br />If do you pass a <code className="language-plaintext highlighter-rouge">voiceBridge</code> number, then you must ensure that each meeting has a unique <code className="language-plaintext highlighter-rouge">voiceBridge</code> number; otherwise, reusing same <code className="language-plaintext highlighter-rouge">voiceBridge</code> number for two different meetings will cause users from one meeting to appear as phone users in the other, which will be very confusing to users in both meetings.</>)
},
{
"name": "maxParticipants",
"required": false,
"type": "Number",
"description": (<>Set the maximum number of users allowed to joined the conference at the same time.</>)
},
{
"name": "logoutURL",
"required": false,
"type": "String",
"description": (<>The URL that the BigBlueButton client will go to after users click the OK button on the You have been logged out message. This overrides the value for <code className="language-plaintext highlighter-rouge">bigbluebutton.web.logoutURL</code> in <a href="https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties">bigbluebutton.properties</a>.</>)
},
{
"name": "record",
"required": false,
"type": "Boolean",
"description": (<>Setting record=true instructs the BigBlueButton server to record the media and events in the session for later playback. The default is false.<br /><br /> In order for a playback file to be generated, a moderator must click the Start/Stop Recording button at least once during the sesssion; otherwise, in the absence of any recording marks, the record and playback scripts will not generate a playback file. See also the <code className="language-plaintext highlighter-rouge">autoStartRecording</code> and <code className="language-plaintext highlighter-rouge">allowStartStopRecording</code> parameters in <a href="https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties">bigbluebutton.properties</a>.</>)
},
{
"name": "duration",
"required": false,
"type": "Number",
"description": (<>The maximum length (in minutes) for the meeting.<br /><br /> Normally, the BigBlueButton server will end the meeting when either (a) the last person leaves (it takes a minute or two for the server to clear the meeting from memory) or when the server receives an <a href="#end">end</a> API request with the associated meetingID (everyone is kicked and the meeting is immediately cleared from memory).<br /><br /> BigBlueButton begins tracking the length of a meeting when it is created. If duration contains a non-zero value, then when the length of the meeting exceeds the duration value the server will immediately end the meeting (equivalent to receiving an end API request at that moment).</>)
},
{
"name": "isBreakout",
"required": false,
"type": "Boolean",
"description": (<>Must be set to <code className="language-plaintext highlighter-rouge">true</code> to create a breakout room.</>)
},
{
"name": "parentMeetingID",
"required": "(required for breakout room)",
"type": "String",
"description": (<>Must be provided when creating a breakout room, the parent room must be running.</>)
},
{
"name": "sequence",
"required": "(required for breakout room)",
"type": "Number",
"description": (<>The sequence number of the breakout room.</>)
},
{
"name": "freeJoin",
"required": "(only breakout room)",
"type": "Boolean",
"description": (<>If set to true, the client will give the user the choice to choose the breakout rooms he wants to join.</>)
},
{
"name": "breakoutRoomsEnabled",
"required": "Optional(Breakout Room)",
"type": "Boolean",
"default": "true",
"description": (<><b>[DEPRECATED]</b> Removed in 2.5, temporarily still handled, please transition to disabledFeatures.<br /><br />If set to false, breakout rooms will be disabled.</>)
},
{
"name": "breakoutRoomsPrivateChatEnabled",
"required": "Optional(Breakout Room)",
"type": "Boolean",
"default": "true",
"description": (<>If set to false, the private chat will be disabled in breakout rooms.</>)
},
{
"name": "breakoutRoomsRecord",
"required": "Optional(Breakout Room)",
"type": "Boolean",
"default": "true",
"description": (<>If set to false, breakout rooms will not be recorded.</>)
},
{
"name": "meta",
"required": false,
"type": "String",
"description": (<>This is a special parameter type (there is no parameter named just <code className="language-plaintext highlighter-rouge">meta</code>).<br /><br /> You can pass one or more metadata values when creating a meeting. These will be stored by BigBlueButton can be retrieved later via the getMeetingInfo and getRecordings calls.<br /><br /> Examples of the use of the meta parameters are <code className="language-plaintext highlighter-rouge">meta_Presenter=Jane%20Doe</code>, <code className="language-plaintext highlighter-rouge">meta_category=FINANCE</code>, and <code className="language-plaintext highlighter-rouge">meta_TERM=Fall2016</code>.</>)
},
{
"name": "moderatorOnlyMessage",
"required": false,
"type": "String",
"description": (<>Display a message to all moderators in the public chat.<br /><br /> The value is interpreted in the same way as the <code className="language-plaintext highlighter-rouge">welcome</code> parameter.</>)
},
{
"name": "autoStartRecording",
"required": false,
"type": "Boolean",
"description": (<>Whether to automatically start recording when first user joins (default <code className="language-plaintext highlighter-rouge">false</code>).<br /><br /> When this parameter is <code className="language-plaintext highlighter-rouge">true</code>, the recording UI in BigBlueButton will be initially active. Moderators in the session can still pause and restart recording using the UI control.&lt;br/<br /> NOTE: Dont pass <code className="language-plaintext highlighter-rouge">autoStartRecording=false</code> and <code className="language-plaintext highlighter-rouge">allowStartStopRecording=false</code> - the moderator wont be able to start recording!</>)
},
{
"name": "allowStartStopRecording",
"required": false,
"type": "Boolean",
"description": (<>Allow the user to start/stop recording. (default true)<br /><br /> If you set both <code className="language-plaintext highlighter-rouge">allowStartStopRecording=false</code> and <code className="language-plaintext highlighter-rouge">autoStartRecording=true</code>, then the entire length of the session will be recorded, and the moderators in the session will not be able to pause/resume the recording.</>)
},
{
"name": "webcamsOnlyForModerator",
"required": false,
"type": "Boolean",
"description": (<>Setting <code className="language-plaintext highlighter-rouge">webcamsOnlyForModerator=true</code> will cause all webcams shared by viewers during this meeting to only appear for moderators (added 1.1)</>)
},
{
"name": "bannerText",
"required": false,
"type": "String",
"description": (<>Will set the banner text in the client. (added 2.0)</>)
},
{
"name": "bannerColor",
"required": false,
"type": "String",
"description": (<>Will set the banner background color in the client. The required format is color hex #FFFFFF. (added 2.0)</>)
},
{
"name": "muteOnStart",
"required": false,
"type": "Boolean",
"description": (<>Setting <code className="language-plaintext highlighter-rouge">true</code> will mute all users when the meeting starts. (added 2.0)</>)
},
{
"name": "allowModsToUnmuteUsers",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will allow moderators to unmute other users in the meeting. (added 2.2)</>)
},
{
"name": "lockSettingsDisableCam",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting <code className="language-plaintext highlighter-rouge">true</code> will prevent users from sharing their camera in the meeting. (added 2.2)</>)
},
{
"name": "lockSettingsDisableMic",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will only allow user to join listen only. (added 2.2)</>)
},
{
"name": "lockSettingsDisablePrivateChat",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will disable private chats in the meeting. (added 2.2)</>)
},
{
"name": "lockSettingsDisablePublicChat",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will disable public chat in the meeting. (added 2.2)</>)
},
{
"name": "lockSettingsDisableNote",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will disable notes in the meeting. (added 2.2)</>)
},
{
"name": "lockSettingsLockOnJoin",
"required": false,
"type": "Boolean",
"default": "true",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">false</code> will not apply lock setting to users when they join. (added 2.2)</>)
},
{
"name": "lockSettingsLockOnJoinConfigurable",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will allow applying of <code className="language-plaintext highlighter-rouge">lockSettingsLockOnJoin</code>.</>)
},
{
"name": "lockSettingsHideViewersCursor",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will prevent viewers to see other viewers cursor when multi-user whiteboard is on. (added 2.5)</>)
},
{
"name": "guestPolicy",
"required": false,
"type": "Enum",
"default": "ALWAYS_ACCEPT",
"description": (<>Will set the guest policy for the meeting. The guest policy determines whether or not users who send a join request with <code className="language-plaintext highlighter-rouge">guest=true</code> will be allowed to join the meeting. Possible values are ALWAYS_ACCEPT, ALWAYS_DENY, and ASK_MODERATOR.</>)
},
{
"name": "keepEvents",
"type": "Boolean",
"deprecated": true,
"description": (<>Removed in 2.3 in favor of <code className="language-plaintext highlighter-rouge">meetingKeepEvents</code> and bigbluebutton.properties <code className="language-plaintext highlighter-rouge">defaultKeepEvents</code>.</>)
},
{
"name": "meetingKeepEvents",
"type": "Boolean",
"default": "false",
"description": (<>Defaults to the value of <code className="language-plaintext highlighter-rouge">defaultKeepEvents</code>. If <code className="language-plaintext highlighter-rouge">meetingKeepEvents</code> is true BigBlueButton saves meeting events even if the meeting is not recorded (added in 2.3)</>)
},
{
"name": "endWhenNoModerator",
"type": "Boolean",
"default": "false",
"description": (<>Default <code className="language-plaintext highlighter-rouge">endWhenNoModerator=false</code>. If <code className="language-plaintext highlighter-rouge">endWhenNoModerator</code> is true the meeting will end automatically after a delay - see <code className="language-plaintext highlighter-rouge">endWhenNoModeratorDelayInMinutes</code> (added in 2.3)</>)
},
{
"name": "endWhenNoModeratorDelayInMinutes",
"type": "Number",
"default": "1",
"description": (<>Defaults to the value of <code className="language-plaintext highlighter-rouge">endWhenNoModeratorDelayInMinutes=1</code>. If <code className="language-plaintext highlighter-rouge">endWhenNoModerator</code> is true, the meeting will be automatically ended after this many minutes (added in 2.2)</>)
},
{
"name": "meetingLayout",
"type": "Enum",
"default": "SMART_LAYOUT",
"description": (<>Will set the default layout for the meeting. Possible values are: CUSTOM_LAYOUT, SMART_LAYOUT, PRESENTATION_FOCUS, VIDEO_FOCUS. (added 2.4)</>)
},
{
"name": "learningDashboardEnabled",
"type": "Boolean",
"default": "true",
"description": (<><b>[DEPRECATED]</b> Removed in 2.5, temporarily still handled, please transition to disabledFeatures.<br /><br />Default <code className="language-plaintext highlighter-rouge">learningDashboardEnabled=true</code>. When this option is enabled BigBlueButton generates a Dashboard where moderators can view a summary of the activities of the meeting. (added 2.4)</>)
},
{
"name": "learningDashboardCleanupDelayInMinutes",
"type": "Number",
"default": "2",
"description": (<>Default <code className="language-plaintext highlighter-rouge">learningDashboardCleanupDelayInMinutes=2</code>. This option set the delay (in minutes) before the Learning Dashboard become unavailable after the end of the meeting. If this value is zero, the Learning Dashboard will keep available permanently. (added 2.4)</>)
},
{
"name": "allowModsToEjectCameras",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will allow moderators to close other users cameras in the meeting. (added 2.4)</>)
},
{
"name": "allowRequestsWithoutSession",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">true</code> will allow users to join meetings without session cookie's validation. (added 2.4.3)</>)
},
{
"name": "virtualBackgroundsDisabled",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<><b>[DEPRECATED]</b> Removed in 2.5, temporarily still handled, please transition to disabledFeatures.<br /><br />Setting to <code className="language-plaintext highlighter-rouge">true</code> will disable Virtual Backgrounds for all users in the meeting. (added 2.4.3)</>)
},
{
"name": "userCameraCap",
"required": false,
"type": "Number",
"default": "3",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">0</code> will disable this threshold. Defines the max number of webcams a single user can share simultaneously. (added 2.4.5)</>)
},
{
"name": "meetingCameraCap",
"required": false,
"type": "Number",
"default": "0",
"description": (<>Setting to <code className="language-plaintext highlighter-rouge">0</code> will disable this threshold. Defines the max number of webcams a meeting can have simultaneously. (added 2.5.0)</>)
},
{
"name": "meetingExpireIfNoUserJoinedInMinutes",
"required": false,
"type": "Number",
"default": "5",
"description": (<>Automatically end meeting if no user joined within a period of time after meeting created. (added 2.5)</>)
},
{
"name": "meetingExpireWhenLastUserLeftInMinutes",
"required": false,
"type": "Number",
"default": "1",
"description": (<>Number of minutes to automatically end meeting after last user left. (added 2.5)<br />Setting to <code className="language-plaintext highlighter-rouge">0</code> will disable this function.</>)
},
{
"name": "groups",
"required": false,
"type": "String",
"description": (<>Pre-defined groups to automatically assign the students to a given breakout room. (added 2.5)<br /><br /><b>Expected value:</b> Json with Array of groups.<br /><b>Group properties:</b><br /><ul><li><code className="language-plaintext highlighter-rouge">id</code> - String with group unique id.</li><li><code className="language-plaintext highlighter-rouge">name</code> - String with name of the group <i>(optional)</i>.</li><li><code className="language-plaintext highlighter-rouge">roster</code> - Array with IDs of the users.</li></ul><br />E.g:<br /><code className="language-json highlighter-rouge">[<br />{"{\"id\":'1',name:'GroupA',roster:['1235']}"}, <br />{"{\"id\":'2',name:'GroupB',roster:['2333','2335']}"},<br />{"{\"id\":'3',roster:[]}"}<br />]</code></>)
},
{
"name": "logo",
"required": false,
"type": "String",
"description": (<>Pass a URL to an image which will then be visible in the area above the participants list if <code>displayBrandingArea</code> is set to <code>true</code> in bbb-html5's configuration</>)
},
{
"name": "disabledFeatures",
"required": false,
"type": "String",
"description": (<>List (comma-separated) of features to disable in a particular meeting. (added 2.5)<br /><br />Available options to disable:<br /><ul><li><code className="language-plaintext highlighter-rouge">breakoutRooms</code>- <b>Breakout Rooms</b> </li><li><code className="language-plaintext highlighter-rouge">captions</code>- <b>Closed Caption</b> </li><li><code className="language-plaintext highlighter-rouge">chat</code>- <b>Chat</b></li><li><code className="language-plaintext highlighter-rouge">downloadPresentationWithAnnotations</code>- <b>Annotated presentation download</b></li><li><code className="language-plaintext highlighter-rouge">externalVideos</code>- <b>Share an external video</b> </li><li><code className="language-plaintext highlighter-rouge">importPresentationWithAnnotationsFromBreakoutRooms</code>- <b>Bring back breakout slides</b></li><li><code className="language-plaintext highlighter-rouge">layouts</code>- <b>Layouts</b> (allow only default layout)</li><li><code className="language-plaintext highlighter-rouge">learningDashboard</code>- <b>Learning Analytics Dashboard</b></li><li><code className="language-plaintext highlighter-rouge">polls</code>- <b>Polls</b> </li><li><code className="language-plaintext highlighter-rouge">screenshare</code>- <b>Screen Sharing</b></li><li><code className="language-plaintext highlighter-rouge">sharedNotes</code>- <b>Shared Notes</b></li><li><code className="language-plaintext highlighter-rouge">virtualBackgrounds</code>- <b>Virtual Backgrounds</b></li><li><code className="language-plaintext highlighter-rouge">customVirtualBackgrounds</code>- <b>Virtual Backgrounds Upload</b></li><li><code className="language-plaintext highlighter-rouge">liveTranscription</code>- <b>Live Transcription</b></li><li><code className="language-plaintext highlighter-rouge">presentation</code>- <b>Presentation</b></li></ul></>)
},
{
"name": "preUploadedPresentationOverrideDefault",
"required": false,
"type": "Boolean",
"default": "true",
"description": (<>If it is true, the <code>default.pdf</code> document is not sent along with the other presentations in the /create endpoint, on the other hand, if that's false, the <code>default.pdf</code> is sent with the other documents. By default it is true.</>)
},
{
"name": "notifyRecordingIsOn",
"required": false,
"type": "Boolean",
"default": "false",
"description": (<>If it is true, a modal will be displayed to collect recording consent from users when meeting recording starts (only if <code className="language-plaintext highlighter-rouge">remindRecordingIsOn=true</code>). By default it is false. (added 2.6)</>)
},
{
"name": "presentationUploadExternalUrl",
"required": false,
"type": "String",
"description": (<>Pass a URL to a specific page in external application to select files for inserting documents into a live presentation. Only works if <code className="language-plaintext highlighter-rouge">presentationUploadExternalDescription</code> is also set. (added 2.6)</>)
},
{
"name": "presentationUploadExternalDescription",
"required": false,
"type": "String",
"description": (<>Message to be displayed in presentation uploader modal describing how to use an external application to upload presentation files. Only works if <code className="language-plaintext highlighter-rouge">presentationUploadExternalUrl</code> is also set. (added 2.6)</>)
}
]
export default createEndpointTableData

View File

@ -0,0 +1,12 @@
import React from 'react';
const deleteRecordingsEndpointTableData = [
{
"name": "recordID",
"required": true,
"type": "String",
"description": (<>A record ID for specify the recordings to delete. It can be a set of record IDs separated by commas.</>)
}
];
export default deleteRecordingsEndpointTableData;

18
docs/docs/data/end.tsx Normal file
View File

@ -0,0 +1,18 @@
import React from 'react';
const endEndpointTableData = [
{
"name": "meetingID",
"required": true,
"type": "String",
"description": (<>The meeting ID that identifies the meeting you are attempting to end.</>)
},
{
"name": "password",
"required": true,
"type": "String",
"description": (<><b>[DEPRECATED]</b> The moderator password for this meeting. You can not end a meeting using the attendee password.</>)
}
];
export default endEndpointTableData;

View File

@ -0,0 +1,12 @@
import React from 'react';
const getMeetingInfoEndpointTableData = [
{
"name": "meetingID",
"required": true,
"type": "String",
"description": (<>The meeting ID that identifies the meeting you are attempting to check on.</>)
}
];
export default getMeetingInfoEndpointTableData;

View File

@ -0,0 +1,12 @@
import React from 'react';
const getRecordingTextTracksEndpointTableData = [
{
"name": "recordID",
"required": true,
"type": "String",
"description": (<>A single recording ID to retrieve the available captions for. (Unlike other recording APIs, you cannot provide a comma-separated list of recordings.)</>)
}
];
export default getRecordingTextTracksEndpointTableData;

View File

@ -0,0 +1,42 @@
import React from 'react';
const getRecordingsEndpointTableData = [
{
"name": "meetingID",
"required": false,
"type": "String",
"description": (<>A meeting ID for get the recordings. It can be a set of meetingIDs separate by commas. If the meeting ID is not specified, it will get ALL the recordings. If a recordID is specified, the meetingID is ignored.</>)
},
{
"name": "recordID",
"required": false,
"type": "String",
"description": (<>A record ID for get the recordings. It can be a set of recordIDs separate by commas. If the record ID is not specified, it will use meeting ID as the main criteria. If neither the meeting ID is specified, it will get ALL the recordings. The recordID can also be used as a wildcard by including only the first characters in the string.</>)
},
{
"name": "state",
"required": false,
"type": "String",
"description": (<>Since version 1.0 the recording has an attribute that shows a state that Indicates if the recording is [processing|processed|published|unpublished|deleted]. The parameter state can be used to filter results. It can be a set of states separate by commas. If it is not specified only the states [published|unpublished] are considered (same as in previous versions). If it is specified as any, recordings in all states are included.</>)
},
{
"name": "meta",
"required": false,
"type": "String",
"description": (<>You can pass one or more metadata values to filter the recordings returned. The format of these parameters is the same as the metadata passed to the <code className="language-plaintext highlighter-rouge">create</code> call. For more information see <a href="https://docs.bigbluebutton.org/dev/api.html#create">the docs for the create call</a>.</>)
},
{
"name": "offset",
"required": false,
"type": "Integer",
"description": (<>The starting index for returned recordings. Number must greater than or equal to 0.</>)
},
{
"name": "limit",
"required": false,
"type": "Integer",
"description": (<>The maximum number of recordings to be returned. Number must be between 1 and 100.</>)
}
];
export default getRecordingsEndpointTableData;

View File

@ -0,0 +1,12 @@
import React from 'react';
const insertDocumentEndpointTableData = [
{
"name": "meetingID",
"required": true,
"type": "String",
"description": (<>The meeting ID that identifies the meeting you want to insert documents.</>)
}
];
export default insertDocumentEndpointTableData;

View File

@ -0,0 +1,12 @@
import React from 'react';
const isMeetingRunningEndpointTableData = [
{
"name": "meetingID",
"required": true,
"type": "String",
"description": (<>The meeting ID that identifies the meeting you are attempting to check on.</>)
}
];
export default isMeetingRunningEndpointTableData;

85
docs/docs/data/join.tsx Normal file
View File

@ -0,0 +1,85 @@
import React from 'react';
const joinEndpointTableData = [
{
"name": "fullName",
"required": true,
"type": "String",
"description": (<>The full name that is to be used to identify this user to other conference attendees.</>)
},
{
"name": "meetingID",
"required": true,
"type": "String",
"description": (<>The meeting ID that identifies the meeting you are attempting to join.</>)
},
{
"name": "password",
"required": true,
"type": "String",
"description": (<><b>[DEPRECATED]</b> This password value is used to determine the role of the user based on whether it matches the moderator or attendee password. Note: This parameter is <b>not</b> required when the role parameter is passed.</>)
},
{
"name": "role",
"required": true,
"type": "String",
"description": (<>Define user role for the meeting. Valid values are MODERATOR or VIEWER (case insensitive). If the role parameter is present and valid, it overrides the password parameter. You must specify either password parameter or role parameter in the join request.</>)
},
{
"name": "createTime",
"required": false,
"type": "String",
"description": (<>Third-party apps using the API can now pass createTime parameter (which was created in the create call), BigBlueButton will ensure it matches the createTime for the session. If they differ, BigBlueButton will not proceed with the join request. This prevents a user from reusing their join URL for a subsequent session with the same meetingID.</>)
},
{
"name": "userID",
"required": false,
"type": "String",
"description": (<>An identifier for this user that will help your application to identify which person this is. This user ID will be returned for this user in the getMeetingInfo API call so that you can check</>)
},
{
"name": "webVoiceConf",
"required": false,
"type": "String",
"description": (<>If you want to pass in a custom voice-extension when a user joins the voice conference using voip. This is useful if you want to collect more info in you Call Detail Records about the user joining the conference. You need to modify your /etc/asterisk/bbb-extensions.conf to handle this new extensions.</>)
},
{
"name": "defaultLayout",
"required": false,
"type": "String",
"description": (<>The layout name to be loaded first when the application is loaded.</>)
},
{
"name": "avatarURL",
"required": false,
"type": "String",
"description": (<>The link for the users avatar to be displayed (default can be enabled/disabled and set with useDefaultAvatar and defaultAvatarURL in bbb-web.properties).</>)
},
{
"name": "redirect",
"required": false,
"type": "String",
"description": (<>The default behaviour of the JOIN API is to redirect the browser to the HTML5 client when the JOIN call succeeds. There have been requests if its possible to embed the HTML5 client in a container page and that the client starts as a hidden DIV tag which becomes visible on the successful JOIN. Setting this variable to FALSE will not redirect the browser but returns an XML instead whether the JOIN call has succeeded or not. The third party app is responsible for displaying the client to the user.</>)
},
{
"name": "joinViaHtml5",
"required": false,
"type": "String",
"description": (<>Set to true to force the HTML5 client to load for the user. (removed in 2.3 since HTML5 is the only client)</>),
"deprecated": true
},
{
"name": "guest",
"required": false,
"type": "String",
"description": (<>Set to true to indicate that the user is a guest, otherwise do NOT send this parameter.</>)
},
{
"name": "excludeFromDashboard",
"required": false,
"type": "String",
"description": (<>If the parameter is passed on JOIN with value `true`, the user will be omitted from being displayed in the Learning Dashboard. The use case is for support agents who drop by to support the meeting / resolve tech difficulties. Added in BBB 2.4</>)
}
];
export default joinEndpointTableData;

View File

@ -0,0 +1,18 @@
import React from 'react';
const publishRecordingsEndpointTableData = [
{
"name": "recordID",
"required": true,
"type": "String",
"description": (<>A record ID for specify the recordings to apply the publish action. It can be a set of record IDs separated by commas.</>)
},
{
"name": "publish",
"required": true,
"type": "String",
"description": (<>The value for publish or unpublish the recording(s). Available values: true or false.</>)
}
];
export default publishRecordingsEndpointTableData;

View File

@ -0,0 +1,30 @@
import React from 'react';
const putRecordingTextTrackEndpointTableData = [
{
"name": "recordID",
"required": true,
"type": "String",
"description": (<>A single recording ID to retrieve the available captions for. (Unlike other recording APIs, you cannot provide a comma-separated list of recordings.)</>)
},
{
"name": "kind",
"required": true,
"type": "String",
"description": (<>Indicates the intended use of the text track. See the <a href="#getrecordingtexttracks">getRecordingTextTracks</a> description for details. Using a value other than one listed in this document will cause an error to be returned.</>)
},
{
"name": "lang",
"required": true,
"type": "String",
"description": (<>Indicates the intended use of the text track. See the <a href="#getrecordingtexttracks">getRecordingTextTracks</a> description for details. Using a value other than one listed in this document will cause an error to be returned.</>)
},
{
"name": "label",
"required": true,
"type": "String",
"description": (<>A human-readable label for the text track. If not specified, the system will automatically generate a label containing the name of the language identified by the lang parameter.</>)
}
];
export default putRecordingTextTrackEndpointTableData;

View File

@ -0,0 +1,18 @@
import React from 'react';
const updateRecordingsEndpointTableData = [
{
"name": "recordID",
"required": true,
"type": "String",
"description": (<>A record ID for specify the recordings to apply the publish action. It can be a set of record IDs separated by commas.</>)
},
{
"name": "meta",
"required": false,
"type": "String",
"description": (<>You can pass one or more metadata values to be updated. The format of these parameters is the same as the metadata passed to the <code className="language-plaintext highlighter-rouge">create</code> call. For more information see <a href="https://docs.bigbluebutton.org/dev/api.html#create">the docs for the create call</a>. When meta_parameter=NOT EMPTY and meta_parameter exists its value is updated, if it doesnt exist, the parameter is added. When meta_parameter=, and meta_parameter exists the key is removed, when it doesnt exist the action is ignored.</>)
}
];
export default updateRecordingsEndpointTableData;

View File

@ -8,6 +8,20 @@ keywords:
- api
---
import APITableComponent from '@site/src/components/APITableComponent';
import createEndpointTableData from '../data/create.tsx';
import deleteRecordingsEndpointTableData from '../data/deleteRecordings.tsx';
import endEndpointTableData from '../data/end.tsx';
import getMeetingInfoEndpointTableData from '../data/getMeetingInfo.tsx';
import getRecordingsEndpointTableData from '../data/getRecordings.tsx';
import getRecordingTextTracksEndpointTableData from '../data/getRecordingTextTracks.tsx';
import insertDocumentEndpointTableData from '../data/insertDocument.tsx';
import isMeetingRunningEndpointTableData from '../data/isMeetingRunning.tsx';
import joinEndpointTableData from '../data/join.tsx';
import publishRecordingsEndpointTableData from '../data/publishRecordings.tsx';
import putRecordingTextTrackEndpointTableData from '../data/putRecordingTextTrack.tsx';
import updateRecordingsEndpointTableData from '../data/updateRecordings.tsx';
## Overview
This document describes the BigBlueButton application programming interface (API).
@ -86,6 +100,8 @@ Updated in 2.6:
- **getRecordings** - **Added:** Added support for pagination using `offset`, `limit`
- **join**: Added `userdata-bbb_hide_presentation_on_join`.
## API Data Types
There are three types in the API.
@ -253,66 +269,9 @@ http&#58;//yourserver.com/bigbluebutton/api/create?[parameters]&checksum=[checks
**Parameters:**
| Param Name | Type | Description |
|---|---|---|
| `name` *(required)* | String | A name for the meeting. This is now required as of BigBlueButton 2.4. |
| `meetingID` *(required)* | String | A meeting ID that can be used to identify this meeting by the 3rd-party application.<br /><br />This must be unique to the server that you are calling: different active meetings can not have the same meeting ID.<br /><br />If you supply a non-unique meeting ID (a meeting is already in progress with the same meeting ID), then if the other parameters in the create call are identical, the create call will succeed (but will receive a warning message in the response). The create call is idempotent: calling multiple times does not have any side effect. This enables a 3rd-party applications to avoid checking if the meeting is running and always call create before joining each user.<br /><br />Meeting IDs should only contain upper/lower ASCII letters, numbers, dashes, or underscores A good choice for the meeting ID is to generate a [GUID](https://en.wikipedia.org/wiki/Globally_unique_identifier) value as this all but guarantees that different meetings will not have the same meetingID. |
| `attendeePW `| String | **[DEPRECATED]** The password that the join URL can later provide as its `password` parameter to indicate the user will join as a viewer. If no `attendeePW` is provided, the `create` call will return a randomly generated `attendeePW` password for the meeting. |
| `moderatorPW` | String | **[DEPRECATED]** The password that will join URL can later provide as its `password` parameter to indicate the user will as a moderator. if no `moderatorPW` is provided, `create` will return a randomly generated `moderatorPW` password for the meeting. |
| `welcome` | String | A welcome message that gets displayed on the chat window when the participant joins. You can include keywords (`%%CONFNAME%%`, `%%DIALNUM%%`, `%%CONFNUM%%`) which will be substituted automatically.<br /><br />This parameter overrides the default `defaultWelcomeMessage` in [bigbluebutton.properties](https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties).<br /><br />The welcome message has limited support for HTML formatting. Be careful about copy/pasted HTML from e.g. MS Word, as it can easily exceed the maximum supported URL length when used on a GET request. |
| `dialNumber` | String | The dial access number that participants can call in using regular phone. You can set a default dial number via `defaultDialAccessNumber` in [bigbluebutton.properties](https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties) |
| `voiceBridge` | String | Voice conference number for the FreeSWITCH voice conference associated with this meeting. This must be a 5-digit number in the range 10000 to 99999. If you [add a phone number](https://docs.bigbluebutton.org/bigbluebutton/administration/customize#add-a-phone-number-to-the-conference-bridge) to your BigBlueButton server, This parameter sets the personal identification number (PIN) that FreeSWITCH will prompt for a phone-only user to enter. If you want to change this range, edit FreeSWITCH dialplan and `defaultNumDigitsForTelVoice` of [bigbluebutton.properties](https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties).<br /><br />The `voiceBridge` number must be different for every meeting.<br /><br />This parameter is optional. If you do not specify a `voiceBridge` number, then BigBlueButton will assign a random unused number for the meeting.<br /><br />If do you pass a `voiceBridge` number, then you must ensure that each meeting has a unique `voiceBridge` number; otherwise, reusing same `voiceBridge` number for two different meetings will cause users from one meeting to appear as phone users in the other, which will be very confusing to users in both meetings. |
| `maxParticipants` | Number | Set the maximum number of users allowed to joined the conference at the same time. |
| `logoutURL` | String | The URL that the BigBlueButton client will go to after users click the OK button on the You have been logged out message. This overrides the value for `bigbluebutton.web.logoutURL` in [bigbluebutton.properties](https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties). |
| `record` | Boolean | Setting `record=true` instructs the BigBlueButton server to record the media and events in the session for later playback. The default is false.<br /><br />In order for a playback file to be generated, a moderator must click the Start/Stop Recording button at least once during the sesssion; otherwise, in the absence of any recording marks, the record and playback scripts will not generate a playback file. See also the `autoStartRecording` and `allowStartStopRecording` parameters in [bigbluebutton.properties](https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-web/grails-app/conf/bigbluebutton.properties). |
| `duration` | Number | The maximum length (in minutes) for the meeting.<br /><br />Normally, the BigBlueButton server will end the meeting when either (a) the last person leaves (it takes a minute or two for the server to clear the meeting from memory) or when the server receives an [end](https://docs.bigbluebutton.org/bigbluebutton/development/api#end) API request with the associated meetingID (everyone is kicked and the meeting is immediately cleared from memory).<br /><br />BigBlueButton begins tracking the length of a meeting when it is created. If duration contains a non-zero value, then when the length of the meeting exceeds the duration value the server will immediately end the meeting (equivalent to receiving an end API request at that moment). |
| `isBreakout` | Boolean | Must be set to `true` to create a breakout room. |
| `parentMeetingID` *(required for breakout room)* | String | Must be provided when creating a breakout room, the parent room must be running. |
| `sequence` *(required for breakout room)* | Number | The sequence number of the breakout room. |
| `freeJoin` *(only breakout room)* | Boolean | If set to true, the client will give the user the choice to choose the breakout rooms he wants to join. |
| `breakoutRoomsEnabled` *Optional(Breakout Room)* | Boolean | **[DEPRECATED]** Removed in 2.5, temporarily still handled, please transition to disabledFeatures.<br /><br />If set to false, breakout rooms will be disabled.<br /><br />*Default: `true`* |
| `breakoutRoomsPrivateChatEnabled` *Optional(Breakout Room)* | Boolean | If set to false, the private chat will be disabled in breakout rooms.<br /><br />*Default: `true`* |
| `breakoutRoomsRecord` *Optional(Breakout Room*) | Boolean | If set to false, breakout rooms will not be recorded.<br /><br />*Default: `true`* |
| `meta` | String | This is a special parameter type (there is no parameter named just `meta`).<br /><br />You can pass one or more metadata values when creating a meeting. These will be stored by BigBlueButton can be retrieved later via the getMeetingInfo and getRecordings calls.<br /><br />Examples of the use of the meta parameters are `meta_Presenter=Jane%20Doe, meta_category=FINANCE`, and `meta_TERM=Fall2016`. |
| `moderatorOnlyMessage` | String | Display a message to all moderators in the public chat.<br /><br />The value is interpreted in the same way as the welcome parameter. |
| `autoStartRecording` | Boolean | Whether to automatically start recording when first user joins. <br /><br />When this parameter is `true`, the recording UI in BigBlueButton will be initially active. Moderators in the session can still pause and restart recording using the UI control. <br /><br />**NOTE:** Dont pass `autoStartRecording=false` and `allowStartStopRecording=false` - the moderator wont be able to start recording! <br /><br />*Default: `false`*|
| `allowStartStopRecording` | Boolean | Allow the user to start/stop recording.<br /><br />If you set both allowStartStopRecording=false and autoStartRecording=true, then the entire length of the session will be recorded, and the moderators in the session will not be able to pause/resume the recording.<br /><br />*Default: `true`*|
| `webcamsOnlyForModerator` | Boolean | Setting `webcamsOnlyForModerator=true` will cause all webcams shared by viewers during this meeting to only appear for moderators (added 1.1) |
| `bannerText` | String | Will set the banner text in the client. (added 2.0) |
| `bannerColor` | String | Will set the banner background color in the client. The required format is color hex #FFFFFF. (added 2.0) |
| `muteOnStart` | Boolean | Setting `true` will mute all users when the meeting starts. (added 2.0) |
| `allowModsToUnmuteUsers` | Boolean | Setting to `true` will allow moderators to unmute other users in the meeting. (added 2.2)<br /><br />*Default: `false`* |
| `lockSettingsDisableCam` | Boolean | Setting `true` will prevent users from sharing their camera in the meeting. (added 2.2)<br /><br />*Default: `false`* |
| `lockSettingsDisableMic` | Boolean | Setting to `true` will only allow user to join listen only. (added 2.2<br /><br />*Default: `false`* |
| `lockSettingsDisablePrivateChat` | Boolean | Setting to `true` will disable private chats in the meeting. (added 2.2)<br /><br />*Default: `false`* |
| `lockSettingsDisablePublicChat` | Boolean | Setting to `true` will disable public chat in the meeting. (added 2.2)<br /><br />*Default: `false`* |
| `lockSettingsDisableNote` | Boolean | Setting to `true` will disable notes in the meeting. (added 2.2) <br /><br />*Default: `false`* |
| `lockSettingsLockOnJoin` | Boolean | Setting to `false` will not apply lock setting to users when they join. (added 2.2) <br /><br />*Default: `true`* |
| `lockSettingsLockOnJoinConfigurable` | Boolean | Setting to `true` will allow applying of `lockSettingsLockOnJoin`. <br /><br />*Default: `false`* |
| `lockSettingsHideViewersCursor` | Boolean | Setting to `true` will prevent viewers to see other viewers cursor when multi-user whiteboard is on. (added 2.5) <br /><br />*Default: `false`* |
| `guestPolicy` | Enum | Will set the guest policy for the meeting. The guest policy determines whether or not users who send a join request with `guest=true` will be allowed to join the meeting. Possible values are ALWAYS_ACCEPT, ALWAYS_DENY, and ASK_MODERATOR. <br /><br />`Default: ALWAYS_ACCEPT` |
| ~~`keepEvents`~~ | Boolean | Removed in 2.3 in favor of `meetingKeepEvents` and bigbluebutton.properties `defaultKeepEvents`. |
| `meetingKeepEvents` | Boolean | Defaults to the value of `defaultKeepEvents`. If `meetingKeepEvents` is true BigBlueButton saves meeting events even if the meeting is not recorded (added in 2.3) <br /><br />*Default: `false`* |
| `endWhenNoModerator` | Boolean | Default `endWhenNoModerator=false`. If `endWhenNoModerator` is true the meeting will end automatically after a delay - see `endWhenNoModeratorDelayInMinutes` (added in 2.3) <br /><br />*Default: `false`* |
| `endWhenNoModeratorDelayInMinutes` | Number | Defaults to the value of `endWhenNoModeratorDelayInMinutes=1`. If `endWhenNoModerator` is true, the meeting will be automatically ended after this many minutes (added in 2.2) <br /><br />*Default: `1`* |
| `meetingLayout` | Enum | Will set the default layout for the meeting. Possible values are: CUSTOM_LAYOUT, SMART_LAYOUT, PRESENTATION_FOCUS, VIDEO_FOCUS. (added 2.4) <br /><br />*Default: `SMART_LAYOUT`* |
| `learningDashboardEnabled` | Boolean | **[DEPRECATED]** Removed in 2.5, temporarily still handled, please transition to `disabledFeatures`.<br /><br />Default `learningDashboardEnabled=true`. When this option is enabled BigBlueButton generates a Dashboard where moderators can view a summary of the activities of the meeting. (added 2.4)<br /><br />*Default: `true`* |
| `learningDashboardCleanupDelayInMinutes` | Number | This option set the delay (in minutes) before the Learning Dashboard become unavailable after the end of the meeting. If this value is zero, the Learning Dashboard will keep available permanently. (added 2.4)<br /><br />*Default: `2`* |
| `allowModsToEjectCameras` | Boolean | Setting to true will allow moderators to close other users cameras in the meeting. (added 2.4)<br /><br />*Default: `false`* |
| `allowRequestsWithoutSession` | Boolean | Setting to true will allow users to join meetings without session cookie's validation. (added 2.4.3)<br /><br />*Default: `false`* |
| `virtualBackgroundsDisabled` | Boolean | **[DEPRECATED]** Removed in 2.5, temporarily still handled, please transition to `disabledFeatures`.<br /><br />Setting to true will disable Virtual Backgrounds for all users in the meeting. (added 2.4.3)<br /><br />*Default: `false`* |
| `userCameraCap` | Number | Setting to `0` will disable this threshold. Defines the max number of webcams a single user can share simultaneously. (added 2.4.5)<br /><br />*Default: `3`* |
| `meetingCameraCap` | Number | Setting to `0` will disable this threshold. Defines the max number of webcams a meeting can have simultaneously. (added 2.5.0) <br /><br />*Default: `0`* |
| `meetingExpireIfNoUserJoinedInMinutes` | Number | Automatically end meeting if no user joined within a period of time after meeting created. (added 2.5) <br /><br />*Default: `5`* |
| `meetingExpireWhenLastUserLeftInMinutes` | Number | Number of minutes to automatically end meeting after last user left. (added 2.5)Setting to `0` will disable this function. <br /><br />*Default: `1`* |
| `groups` | String | Pre-defined groups to automatically assign the students to a given breakout room. (added 2.5) <dl><dt>**Expected value:** Json with Array of groups.</dt><dt>**Group properties:**</dt></dl><ul><li>`id` - String with group unique</li><li>`id.name` - String with name of the group (optional)</li><li>`roster` - Array with IDs of the users.</li></ul>E.g: <br />`[`<br />`{id:'1',name:'GroupA',roster:['1235']},{id:'2',name:'GroupB',roster:['2333','2335']},{id:'3',roster:[]}`<br />`]`|
| `logo` | String | Pass a URL to an image which will then be visible in the area above the participants list if `displayBrandingArea` is set to `true` in bbb-html5's configuration |
| `disabledFeatures` | String | List (comma-separated) of features to disable in a particular meeting. (added 2.5)<br /><br />Available options to disable:<ul><li>`breakoutRooms` - Breakout Rooms</li><li>`captions` - Closed Caption</li><li>`chat` - Chat</li><li>`downloadPresentationWithAnnotations` - Annotated presentation download</li><li>`externalVideos` - Share an external video</li><li>`importPresentationWithAnnotationsFromBreakoutRooms` - Bring back breakout slides</li><li>`layouts` - Layouts (allow only default layout)</li><li>`learningDashboard` - Learning Analytics Dashboard</li><li>`polls` - Polls</li><li>`screenshare` - Screen Sharing</li><li>`sharedNotes` - Shared Notes</li><li>`virtualBackgrounds` - Virtual Backgrounds</li><li>`customVirtualBackgrounds` - Virtual Backgrounds Upload</li><li>`liveTranscription` - Live Transcription</li><li>`presentation` - Presentation</li></ul> |
| `preUploadedPresentationOverrideDefault` | Boolean | If it is true, the `default.pdf` document is not sent along with the other presentations in the /create endpoint, on the other hand, if that's false, the `default.pdf` is sent with the other documents. By default it is true. <br /><br />`Default: true` |
| `notifyRecordingIsOn` | Boolean | If it is true, a modal will be displayed to collect recording consent from users when meeting recording starts (only if `remindRecordingIsOn=true`). By default it is false. (added 2.6) <br /><br />*Default: `false`* |
| `presentationUploadExternalUrl` | String | Pass a URL to a specific page in external application to select files for inserting documents into a live presentation. Only works if `presentationUploadExternalDescription` is also set. (added 2.6) |
| `presentationUploadExternalDescription` | String | Message to be displayed in presentation uploader modal describing how to use an external application to upload presentation files. Only works if `presentationUploadExternalUrl` is also set. (added 2.6) |
```mdx-code-block
<APITableComponent data={createEndpointTableData}/>
```
**Example Requests:**
@ -475,22 +434,9 @@ http&#58;//yourserver.com/bigbluebutton/api/join?[parameters]&checksum=[checksum
**Parameters:**
| Param Name | Type | Description |
--- | --- | --- |
| `fullName` *(required)*| String | The full name that is to be used to identify this user to other conference attendees. |
| `meetingID` *(required)* | String | The meeting ID that identifies the meeting you are attempting to join. |
| `password` *(required)* | String | **[DEPRECATED]** This password value is used to determine the role of the user based on whether it matches the moderator or attendee password. Note: This parameter is not required when the role parameter is passed. |
| `role` *(required)* | String | Define user role for the meeting. Valid values are MODERATOR or VIEWER (case insensitive). If the role parameter is present and valid, it overrides the password parameter. You must specify either password parameter or role parameter in the join request. |
| `createTime` | String | Third-party apps using the API can now pass createTime parameter (which was created in the create call), BigBlueButton will ensure it matches the createTime for the session. If they differ, BigBlueButton will not proceed with the join request. This prevents a user from reusing their join URL for a subsequent session with the same meetingID. |
| `userID` | String | An identifier for this user that will help your application to identify which person this is. This user ID will be returned for this user in the getMeetingInfo API call so that you can check |
| `webVoiceConf` | String | If you want to pass in a custom voice-extension when a user joins the voice conference using voip. This is useful if you want to collect more info in you Call Detail Records about the user joining the conference. You need to modify your /etc/asterisk/bbb-extensions.conf to handle this new extensions. |
| `defaultLayout` | String | The layout name to be loaded first when the application is loaded. |
| `avatarURL` | String | The link for the users avatar to be displayed (default can be enabled/disabled and set with “useDefaultAvatar“ and “defaultAvatarURL“ in bbb-web.properties). |
| `redirect` | String | The default behaviour of the JOIN API is to redirect the browser to the HTML5 client when the JOIN call succeeds. There have been requests if its possible to embed the HTML5 client in a “container” page and that the client starts as a hidden DIV tag which becomes visible on the successful JOIN. Setting this variable to FALSE will not redirect the browser but returns an XML instead whether the JOIN call has succeeded or not. The third party app is responsible for displaying the client to the user. |
| ~~`joinViaHtml5`~~ | String | Set to “true” to force the HTML5 client to load for the user. (removed in 2.3 since HTML5 is the only client) |
| `guest` | String | Set to “true” to indicate that the user is a guest, otherwise do NOT send this parameter. |
| `excludeFromDashboard` | String | If the parameter is passed on JOIN with value `true`, the user will be omitted from being displayed in the Learning Dashboard. The use case is for support agents who drop by to support the meeting / resolve tech difficulties. Added in BBB 2.4 |
```mdx-code-block
<APITableComponent data={joinEndpointTableData}/>
```
**Example Requests:**
@ -525,9 +471,9 @@ https&#58;//yourserver.com/bigbluebutton/api/insertDocument?[parameters]&checksu
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`meetingID` *(required)*|String|The meeting ID that identifies the meeting you want to insert documents.|
```mdx-code-block
<APITableComponent data={insertDocumentEndpointTableData}/>
```
**Example Requests:**
@ -575,9 +521,9 @@ http&#58;//yourserver.com/bigbluebutton/api/isMeetingRunning?[parameters]&checks
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`meetingID` *(required)*|String|The meeting ID that identifies the meeting you are attempting to check on.|
```mdx-code-block
<APITableComponent data={isMeetingRunningEndpointTableData}/>
```
**Example Requests:**
@ -604,11 +550,9 @@ Use this to forcibly end a meeting and kick all participants out of the meeting.
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`meetingID` *(required)*|String|The meeting ID that identifies the meeting you are attempting to end.|
|`password` *(required)*|String|**[DEPRECATED]** The moderator password for this meeting. You can not end a meeting using the attendee password.|
```mdx-code-block
<APITableComponent data={endEndpointTableData}/>
```
**Example Requests:**
@ -650,10 +594,9 @@ Resource URL:
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`meetingID` *(required)*|String|The meeting ID that identifies the meeting you are attempting to check on.|
```mdx-code-block
<APITableComponent data={getMeetingInfoEndpointTableData}/>
```
**Example Requests:**
@ -804,15 +747,9 @@ http&#58;//yourserver.com/bigbluebutton/api/getRecordings?[parameters]&checksum=
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`meetingID`|String|A meeting ID for get the recordings. It can be a set of meetingIDs separate by commas. If the meeting ID is not specified, it will get ALL the recordings. If a recordID is specified, the meetingID is ignored.|
|`recordID`|String|A record ID for get the recordings. It can be a set of recordIDs separate by commas. If the record ID is not specified, it will use meeting ID as the main criteria. If neither the meeting ID is specified, it will get ALL the recordings. The recordID can also be used as a wildcard by including only the first characters in the string.|
|`state`|String|Since version 1.0 the recording has an attribute that shows a state that Indicates if the recording is [processing\|processed\|published\|unpublished\|deleted]. The parameter state can be used to filter results. It can be a set of states separate by commas. If it is not specified only the states [published\|unpublished] are considered (same as in previous versions). If it is specified as “any”, recordings in all states are included.|
|`meta`|String|You can pass one or more metadata values to filter the recordings returned. The format of these parameters is the same as the metadata passed to the `create` call. For more information see [the docs for the create call](https://docs.bigbluebutton.org/dev/api.html#create).|
|`offset`|Integer|The starting index for returned recordings. Number must greater than or equal to 0.|
|`limit`|Integer|The maximum number of recordings to be returned. Number must be between 1 and 100.|
```mdx-code-block
<APITableComponent data={getRecordingsEndpointTableData}/>
```
**Example Requests:**
@ -925,10 +862,9 @@ Publish and unpublish recordings for a given recordID (or set of record IDs).
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`recordID` *(required)*|String|A record ID for specify the recordings to apply the publish action. It can be a set of record IDs separated by commas.|
|`publish` *(required)*|String|The value for publish or unpublish the recording(s). Available values: true or false.|
```mdx-code-block
<APITableComponent data={publishRecordingsEndpointTableData}/>
```
**Example Requests:**
@ -954,7 +890,9 @@ http&#58;//yourserver.com/bigbluebutton/api/deleteRecordings?[parameters]&checks
**Parameters:**
{% include api_table.html endpoint="deleteRecordings" %}
```mdx-code-block
<APITableComponent data={deleteRecordingsEndpointTableData}/>
```
**Example Requests:**
@ -980,9 +918,9 @@ Update metadata for a given recordID (or set of record IDs). Available since ver
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`recordID` *(required)*|String|A record ID for specify the recordings to delete. It can be a set of record IDs separated by commas.|
```mdx-code-block
<APITableComponent data={updateRecordingsEndpointTableData}/>
```
**Example Requests:**
@ -1007,9 +945,9 @@ Get a list of the caption/subtitle files currently available for a recording. It
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`recordID` *(required)*|String|A single recording ID to retrieve the available captions for. (Unlike other recording APIs, you cannot provide a comma-separated list of recordings.)|
```mdx-code-block
<APITableComponent data={getRecordingTextTracksEndpointTableData}/>
```
**Example Response:**
@ -1095,13 +1033,9 @@ This API is asynchronous. It can take several minutes for the uploaded file to b
**Parameters:**
|Param Name|Type|Description|
|:----|:----|:----|
|`recordID` *(required)*|String|A single recording ID to retrieve the available captions for. (Unlike other recording APIs| you cannot provide a comma-separated list of recordings.)|
|`kind` *(required)*|String|Indicates the intended use of the text track. See the getRecordingTextTracks description for details. Using a value other than one listed in this document will cause an error to be returned.|
|`lang` *(required)*|String|Indicates the intended use of the text track. See the getRecordingTextTracks description for details. Using a value other than one listed in this document will cause an error to be returned.|
|`label` *(required)*|String|A human-readable label for the text track. If not specified| the system will automatically generate a label containing the name of the language identified by the lang parameter.|
```mdx-code-block
<APITableComponent data={putRecordingTextTrackEndpointTableData}/>
```
**POST Body:** <br />If the request has a body, the Content-Type header must specify multipart/form-data. The following parameters may be encoded in the post body.

View File

@ -552,7 +552,7 @@ sudo service bbb-web start
If you need to revert back your original production `bbb-web` just run the following command. (Don't forget to stop bbb-web service before doing it)
```bash
sudo mv /usr/share/bbb-web /usr/share/bbb-web-dev && /usr/share/bbb-web-old /usr/share/bbb-web
sudo mv /usr/share/bbb-web /usr/share/bbb-web-dev && mv /usr/share/bbb-web-old /usr/share/bbb-web
```
Your compiled code will be under the `/usr/share/bbb-web-dev` directory and you can safely run the original production ``bbb-web`.

View File

@ -149,9 +149,9 @@ Learn more about [how to enable generating MP4 (h264 video) output](https://docs
#### Change of parameters naming
In 2.5 we had the hidePresentation which was responsible for disabling presentation Area, and it was configured in the join call. Now we have a new disabled feature which is responsible for that. it is called `disabledFeatures=presentation`, and it is configured in the create call, for more details see the [docs](https://docs.bigbluebutton.org/dev/api.html#create).
In 2.5 we had the hidePresentation which was responsible for disabling presentation Area, and it was configured in the join call. Now we have a new disabled feature which is responsible for that. it is called `disabledFeatures=presentation`, and it is configured in the create call, for more details see the [docs](https://docs.bigbluebutton.org/2.6/development/api#create).
There is another parameter renamed in 2.6, it is `swapLayout`, or `userdata-bbb_auto_swap_layout` in the join call. Now, this parameter is set to `hidePresentationOnJoin` or `userdata-bbb_hide_presentation_on_join` in the join call, and it does essentially the same thing: it starts meeting with presentation minimized.
There is another parameter renamed in 2.6, it is `swapLayout`, or `userdata-bbb_auto_swap_layout` in the join call. Now, this parameter is set to `hidePresentationOnJoin` or `userdata-bbb_hide_presentation_on_join` in the join call, and it does essentially the same thing: it starts meeting with presentation minimized. And lastly, we've got another way to configure it: which is to set `public.layout.hidePresentationOnJoin: true` in the override settings file: `/etc/bigbluebutton/bbb-html5.yml`
In brief:
@ -205,6 +205,7 @@ Under the hood, BigBlueButton 2.6 installs on Ubuntu 20.04 64-bit, and the follo
For full details on what is new in BigBlueButton 2.6, see the release notes. Recent releases:
- [2.6.1](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.6.1)
- [2.6.0](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.6.0)
- [rc.9](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.6.0-rc.9)
- [rc.8](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.6.0-rc.8)

View File

@ -68,7 +68,7 @@ Major features in this release include:
* **Faster Screen Sharing** - Screen sharing now is faster, works across all browsers (using a Java application that launches outside the browser), and captures the cursor (see [video](https://www.youtube.com/watch?v=xTFuEvmEqB0)).
* **Greenlight** - Administrators can install a new front-end, called Greenlight, that makes it easy for users to quickly creating meetings, invite others, and manage recordings on a BigBlueButton server. Using Docker, administrators can [install](/greenlight/v2/overview) on the BigBlueButton server itself (no need for a separate server). Greenlight is written in Rails 5 and can be easily customized by any rails developer (see [source on GitHub](https://github.com/bigbluebutton/greenlight)).
* **Greenlight** - Administrators can install a new front-end, called Greenlight, that makes it easy for users to quickly creating meetings, invite others, and manage recordings on a BigBlueButton server. Using Docker, administrators can [install](/greenlight/v3/install) on the BigBlueButton server itself (no need for a separate server). Greenlight is written in Rails 5 and can be easily customized by any rails developer (see [source on GitHub](https://github.com/bigbluebutton/greenlight)).
* **Ubuntu 16.04 packages** - This release installs on Ubuntu 16.04 64-bit (the most recent long-term support release from Canonical) and uses `systemd` for new start/stop scripts for individual components.

View File

@ -448,7 +448,7 @@ Since BigBlueButton is controlled by its [API](/development/api), there isn't an
The most common way to use BigBlueButton is to use an existing application that has a plugin. See [list of integrations](https://bigbluebutton.org/integrations/).
BigBlueButton also comes with an easy-to-use front-end called [Greenlight](/greenlight/v2/overview).
BigBlueButton also comes with an easy-to-use front-end called [Greenlight](/greenlight/v3/install).
### Where do I schedule a meeting in BigBlueButton?
@ -528,7 +528,7 @@ Absolutely. To see an example of this, check out [GreenLight on our pool of demo
If you are using Sakai, Moodle, Drupal, Joomla, Wordpress or other systems that already have a [BigBlueButton integration](https://bigbluebutton.org/support), then installing the integration provides the easiest way to enable your users to access BigBlueButton sessions.
Alternatively, you can set up [Greenlight](/greenlight/v2/overview) to be able to easily manage classrooms.
Alternatively, you can set up [Greenlight](/greenlight/v3/install) to be able to easily manage classrooms.
#### How do I integrate BigBlueButton with my own server

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