Merge branch 'v2.6.x-release' into new-polling

This commit is contained in:
Anton Georgiev 2023-03-28 12:46:26 -04:00 committed by GitHub
commit 18df0d3429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 3578 additions and 537 deletions

View File

@ -5,11 +5,11 @@ on:
- 'develop'
- 'v2.[5-9].x-release'
- 'v[3-9].*.x-release'
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- 'docs/**'
- '**/*.md'
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
jobs:
@ -121,6 +121,7 @@ jobs:
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v focal-26-dev -s bbb-ci.test -j -d /certs/
bbb-conf --salt bbbci
echo "NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt" >> /usr/share/meteor/bundle/bbb-html5-with-roles.conf
sed -i "s/\"minify\": true,/\"minify\": false,/" /usr/share/etherpad-lite/settings.json
bbb-conf --restart
'
- name: Install test dependencies

View File

@ -4,7 +4,6 @@ on:
workflow_dispatch:
push:
branches:
- 'v*'
- 'develop'
paths:
- 'docs/**'

View File

@ -6,17 +6,19 @@ We actively support BigBlueButton through the community forums and through secur
| Version | Supported |
| ------- | ------------------ |
| 2.3.x (or earlier) | :x: |
| 2.4.x   | :white_check_mark: |
| 2.4.x (or earlier) | :x: |
| 2.5.x   | :white_check_mark: |
| 2.6.x   | :x: |
| 2.6.x   | :white_check_mark: |
| 2.7.x   | :x: |
We have released 2.5 to the community and are going to support both 2.4 and 2.5 together for the coming months (while we're actively developing the next release). Also, BigBlueButton 2.3 is now end of life.
We have released 2.6 to the community and are going to support both 2.5 and 2.6 together for the coming months (while we're actively developing the next release). Also, BigBlueButton 2.4 is now end of life.
As such, we recommend that all administrators deploy 2.5 going forward. You'll find [many improvements](https://docs.bigbluebutton.org/2.5/new.html) in this newer version.
As such, we recommend that all administrators deploy 2.6 going forward. You'll find [many improvements](https://docs.bigbluebutton.org/2.6/new.html) in this newer version.
## Reporting a Vulnerability
If you believe you have found a security vunerability in BigBlueButton please let us know directly by e-mailing security@bigbluebutton.org with as much detail as possible.
If you believe you have found a security vunerability in BigBlueButton please let us know directly by
- using GitHub's "Report a vulnerability" functionality on https://github.com/bigbluebutton/bigbluebutton/security/advisories
- or e-mailing security@bigbluebutton.org with as much detail as possible.
Regards,... [BigBlueButton Team](https://docs.bigbluebutton.org/support/faq.html#bigbluebutton-committer)

View File

@ -136,7 +136,8 @@ object RecMeta {
def getRecMeta(metaXml: Elem): Option[RecMeta] = {
val id = getText(metaXml, "id", "unknown")
val state = getText(metaXml, "state", "unknown")
val stateVal = getText(metaXml, "state", "unknown")
val state = if (stateVal.equalsIgnoreCase("available")) "published" else stateVal
val published = getText(metaXml, "published", "true").toString.toBoolean
val startTime = getValLong(metaXml, "start_time", 0)
val endTime = getValLong(metaXml, "end_time", 0)
@ -151,7 +152,11 @@ object RecMeta {
val meetingId = meeting match {
case Some(m) => m.externalId
case None => id
case None =>
meta match {
case Some(m) => m.getOrElse("meetingId", id)
case None => id
}
}
val meetingName = meeting match {
@ -165,7 +170,7 @@ object RecMeta {
val internalMeetingId = meeting match {
case Some(m) => Some(m.id)
case None => None
case None => Some(id)
}
val isBreakout = meeting match {

View File

@ -16,8 +16,6 @@
},
"process": {
"whiteboardTextEncoding": "utf-8",
"maxImageWidth": 2048,
"maxImageHeight": 1536,
"textScaleFactor": 2,
"pointsPerInch": 72,
"pixelsPerInch": 96

View File

@ -582,6 +582,7 @@ class App extends React.Component {
</p>
</div>
<button
data-test="downloadSessionDataDashboard"
type="button"
className="border-2 text-gray-700 border-gray-200 rounded-md px-4 py-2 bg-white focus:outline-none focus:ring ring-offset-2 focus:ring-gray-500 focus:ring-opacity-50"
onClick={this.handleSaveSessionData.bind(this)}

View File

@ -1 +1 @@
git clone --branch v5.0.0-rc.2 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
git clone --branch v5.0.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback

View File

@ -1,6 +1,6 @@
#!/bin/sh
set -ex
RELEASE=4.0.0-rc.2
RELEASE=4.0.0
cat <<MSG
This tool downloads prebuilt packages built on Github Actions
The corresponding source can be browsed at https://github.com/bigbluebutton/bbb-presentation-video/tree/${RELEASE}

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.6.0-rc.9
BIGBLUEBUTTON_RELEASE=2.6.1

View File

@ -5,8 +5,8 @@ const calculateSlideData = (slideData) => {
} = slideData;
// calculating viewBox and offsets for the current presentation
const maxImageWidth = 2048;
const maxImageHeight = 1536;
const maxImageWidth = 1440;
const maxImageHeight = 1080;
const ratio = Math.min(maxImageWidth / width, maxImageHeight / height);
const scaledWidth = width * ratio;

View File

@ -9,7 +9,7 @@ import Modal from '/imports/ui/components/common/modal/fullscreen/component';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import SortList from './sort-user-list/component';
import Styled from './styles';
import Icon from '/imports/ui/components/common/icon/component.jsx';
import Icon from '/imports/ui/components/common/icon/component';
import { isImportSharedNotesFromBreakoutRoomsEnabled, isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled } from '/imports/ui/services/features';
import { addNewAlert } from '/imports/ui/components/screenreader-alert/service';
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
@ -195,6 +195,13 @@ const propTypes = {
isBreakoutRecordable: PropTypes.bool,
};
const setPresentationVisibility = (state) => {
const presentationInnerWrapper = document.getElementById('presentationInnerWrapper');
if (presentationInnerWrapper) {
presentationInnerWrapper.style.display = state;
}
}
class BreakoutRoom extends PureComponent {
constructor(props) {
super(props);
@ -263,6 +270,7 @@ class BreakoutRoom extends PureComponent {
allowUserChooseRoomByDefault, captureSharedNotesByDefault,
captureWhiteboardByDefault,
} = this.props;
setPresentationVisibility('none');
this.setRoomUsers();
if (isUpdate) {
const usersToMerge = []
@ -403,7 +411,7 @@ class BreakoutRoom extends PureComponent {
handleDismiss() {
const { mountModal } = this.props;
setPresentationVisibility('block');
return new Promise((resolve) => {
mountModal(null);
@ -414,6 +422,7 @@ class BreakoutRoom extends PureComponent {
}
onCreateBreakouts() {
setPresentationVisibility('block');
const {
createBreakoutRoom,
} = this.props;
@ -505,6 +514,7 @@ class BreakoutRoom extends PureComponent {
}
onUpdateBreakouts() {
setPresentationVisibility('block');
const { users } = this.state;
const leastOneUserIsValid = users.some((user) => user.from !== user.room);

View File

@ -241,6 +241,11 @@ const RoomUserItem = styled.p`
font-size: ${fontSizeSmaller};
}
&:focus {
background-color: ${colorPrimary};
color: ${colorWhite};
}
${({ selected }) => selected && `
background-color: ${colorPrimary};
color: ${colorWhite};

View File

@ -40,7 +40,6 @@ import SidebarNavigationContainer from '../sidebar-navigation/container';
import SidebarContentContainer from '../sidebar-content/container';
import { makeCall } from '/imports/ui/services/api';
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
import DarkReader from 'darkreader';
import Settings from '/imports/ui/services/settings';
import { registerTitleView } from '/imports/utils/dom-utils';
import Notifications from '../notifications/container';
@ -50,6 +49,7 @@ import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
import AudioService from '/imports/ui/components/audio/service';
import NotesContainer from '/imports/ui/components/notes/container';
import DEFAULT_VALUES from '../layout/defaultValues';
import AppService from '/imports/ui/components/app/service';
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
const APP_CONFIG = Meteor.settings.public.app;
@ -446,22 +446,8 @@ class App extends Component {
renderDarkMode() {
const { darkTheme } = this.props;
if (darkTheme && !DarkReader.isEnabled()) {
DarkReader.enable(
{ brightness: 100, contrast: 90 },
{ invert: [Styled.DtfInvert], ignoreInlineStyle: [Styled.DtfCss], ignoreImageAnalysis: [Styled.DtfImages] },
)
logger.info({
logCode: 'dark_mode',
}, 'Dark mode is on.');
}
if (!darkTheme && DarkReader.isEnabled()){
DarkReader.disable();
logger.info({
logCode: 'dark_mode',
}, 'Dark mode is off.');
}
AppService.setDarkTheme(darkTheme);
}
mountPushLayoutEngine() {
@ -564,6 +550,7 @@ class App extends Component {
<SidebarContentContainer isSharedNotesPinned={shouldShowSharedNotes} />
<NavBarContainer main="new" />
<NewWebcamContainer isLayoutSwapped={!presentationIsOpen} />
<Styled.TextMeasure id="text-measure" />
{shouldShowPresentation ? <PresentationAreaContainer darkTheme={darkTheme} presentationIsOpen={presentationIsOpen} /> : null}
{shouldShowScreenshare ? <ScreenshareContainer isLayoutSwapped={!presentationIsOpen} /> : null}
{

View File

@ -3,6 +3,9 @@ import Meetings from '/imports/api/meetings';
import Settings from '/imports/ui/services/settings';
import Auth from '/imports/ui/services/auth/index';
import deviceInfo from '/imports/utils/deviceInfo';
import Styled from './styles';
import DarkReader from 'darkreader';
import logger from '/imports/startup/client/logger';
const getFontSize = () => {
const applicationSettings = Settings.application;
@ -26,9 +29,34 @@ const validIOSVersion = () => {
return true;
};
const setDarkTheme = (value) => {
if (value && !DarkReader.isEnabled()) {
DarkReader.enable(
{ brightness: 100, contrast: 90 },
{ invert: [Styled.DtfInvert], ignoreInlineStyle: [Styled.DtfCss], ignoreImageAnalysis: [Styled.DtfImages] },
)
logger.info({
logCode: 'dark_mode',
}, 'Dark mode is on.');
}
if (!value && DarkReader.isEnabled()){
DarkReader.disable();
logger.info({
logCode: 'dark_mode',
}, 'Dark mode is off.');
}
}
const isDarkThemeEnabled = () => {
return DarkReader.isEnabled()
}
export {
getFontSize,
meetingIsBreakout,
getBreakoutRooms,
validIOSVersion,
setDarkTheme,
isDarkThemeEnabled,
};

View File

@ -66,6 +66,25 @@ const DtfImages = `
svg
`;
const TextMeasure = styled.pre`
white-space: pre;
width: auto;
border: 1px solid red;
padding: 4px;
margin: 0px;
letter-spacing: -0.03em;
opacity: 0;
position: absolute;
top: -500px;
left: 0px;
z-index: 9999;
pointer-events: none;
user-select: none;
alignment-baseline: mathematical;
dominant-baseline: mathematical;
font-family: "Source Code Pro";
`;
export default {
CaptionsWrapper,
ActionsBar,
@ -73,4 +92,5 @@ export default {
DtfInvert,
DtfCss,
DtfImages,
TextMeasure,
};

View File

@ -37,6 +37,22 @@ const intlMessages = defineMessages({
description: 'aria-label used when chat log is empty',
},
});
const updateChatSemantics = () => {
setTimeout(() => {
const msgListItem = document.querySelector('span[data-test="msgListItem"]');
if (msgListItem) {
const virtualizedGridInnerScrollContainer = msgListItem.parentElement;
const virtualizedGrid = virtualizedGridInnerScrollContainer.parentElement;
virtualizedGridInnerScrollContainer.setAttribute('role', 'list');
virtualizedGridInnerScrollContainer.setAttribute('tabIndex', 0);
virtualizedGrid.removeAttribute('tabIndex');
virtualizedGrid.removeAttribute('aria-label');
virtualizedGrid.removeAttribute('aria-readonly');
}
}, 300);
}
class TimeWindowList extends PureComponent {
constructor(props) {
super(props);
@ -82,6 +98,8 @@ class TimeWindowList extends PureComponent {
this.setState({
scrollPosition: scrollProps,
});
updateChatSemantics();
}
componentDidUpdate(prevProps) {
@ -159,15 +177,7 @@ class TimeWindowList extends PureComponent {
this.listRef.forceUpdateGrid();
}
const msgListItem = document.querySelector('span[data-test="msgListItem"]');
if (msgListItem) {
const virtualizedGridInnerScrollContainer = msgListItem.parentElement;
const virtualizedGrid = virtualizedGridInnerScrollContainer.parentElement;
virtualizedGridInnerScrollContainer.setAttribute('role', 'list');
virtualizedGridInnerScrollContainer.setAttribute('tabIndex', '0');
virtualizedGrid.removeAttribute('tabIndex');
virtualizedGrid.removeAttribute('aria-label');
}
updateChatSemantics();
}
handleScrollUpdate(position, target) {

View File

@ -168,11 +168,12 @@ class NavBar extends Component {
amIModerator,
style,
main,
isPinned,
sidebarNavigation,
currentUserId,
} = this.props;
const hasNotification = hasUnreadMessages || hasUnreadNotes;
const hasNotification = hasUnreadMessages || (hasUnreadNotes && !isPinned);
let ariaLabel = intl.formatMessage(intlMessages.toggleUserListAria);
ariaLabel += hasNotification ? (` ${intl.formatMessage(intlMessages.newMessages)}`) : '';

View File

@ -115,6 +115,7 @@ export default withTracker(() => {
}
return {
isPinned: NotesService.isSharedNotesPinned(),
currentUserId: Auth.userID,
meetingId,
presentationTitle: meetingTitle,

View File

@ -713,6 +713,7 @@ class Presentation extends PureComponent {
textAlign: 'center',
display: !presentationIsOpen ? 'none' : 'block',
}}
id={"presentationInnerWrapper"}
>
<Styled.VisuallyHidden id="currentSlideText">{slideContent}</Styled.VisuallyHidden>
{!tldrawIsMounting && currentSlide && this.renderPresentationMenu()}

View File

@ -9,6 +9,7 @@ import BBBMenu from '/imports/ui/components/common/menu/component';
import TooltipContainer from '/imports/ui/components/common/tooltip/container';
import { ACTIONS } from '/imports/ui/components/layout/enums';
import browserInfo from '/imports/utils/browserInfo';
import AppService from '/imports/ui/components/app/service';
const intlMessages = defineMessages({
downloading: {
@ -206,6 +207,13 @@ const PresentationMenu = (props) => {
},
});
// This is a workaround to a conflict of the
// dark mode's styles and the html-to-image lib.
// Issue:
// https://github.com/bubkoo/html-to-image/issues/370
const darkThemeState = AppService.isDarkThemeEnabled();
AppService.setDarkTheme(false);
try {
const { copySvg, getShapes, currentPageId } = tldrawAPI;
const svgString = await copySvg(getShapes(currentPageId).map((shape) => shape.id));
@ -239,6 +247,9 @@ const PresentationMenu = (props) => {
logCode: 'presentation_snapshot_error',
extraInfo: e,
});
} finally {
// Workaround
AppService.setDarkTheme(darkThemeState);
}
},
},

View File

@ -161,17 +161,21 @@ class PresentationToolbar extends PureComponent {
nextSlideHandler(event) {
const {
nextSlide, currentSlideNum, numberOfSlides, podId,
nextSlide, currentSlideNum, numberOfSlides, podId, endCurrentPoll,
} = this.props;
if (event) event.currentTarget.blur();
endCurrentPoll();
nextSlide(currentSlideNum, numberOfSlides, podId);
}
previousSlideHandler(event) {
const { previousSlide, currentSlideNum, podId } = this.props;
const {
previousSlide, currentSlideNum, podId, endCurrentPoll,
} = this.props;
if (event) event.currentTarget.blur();
endCurrentPoll();
previousSlide(currentSlideNum, podId);
}

View File

@ -10,6 +10,7 @@ import { UsersContext } from '/imports/ui/components/components-data/users-conte
import Auth from '/imports/ui/services/auth';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import { isPollingEnabled } from '/imports/ui/services/features';
import { CurrentPoll } from '/imports/api/polls';
const PresentationToolbarContainer = (props) => {
const usingUsersContext = useContext(UsersContext);
@ -21,6 +22,10 @@ const PresentationToolbarContainer = (props) => {
const handleToggleFullScreen = (ref) => FullscreenService.toggleFullScreen(ref);
const endCurrentPoll = () => {
if (CurrentPoll.findOne({ meetingId: Auth.meetingID })) makeCall('stopPoll');
};
if (userIsPresenter && !layoutSwapped) {
// Only show controls if user is presenter and layout isn't swapped
@ -28,6 +33,7 @@ const PresentationToolbarContainer = (props) => {
<PresentationToolbar
{...props}
amIPresenter={userIsPresenter}
endCurrentPoll={endCurrentPoll}
{...{
handleToggleFullScreen,
}}
@ -78,6 +84,7 @@ PresentationToolbarContainer.propTypes = {
previousSlide: PropTypes.func.isRequired,
skipToSlide: PropTypes.func.isRequired,
layoutSwapped: PropTypes.bool,
endCurrentPoll: PropTypes.func.isRequired,
};
PresentationToolbarContainer.defaultProps = {

View File

@ -179,6 +179,7 @@ class ZoomTool extends PureComponent {
aria-describedby="zoomOutDescription"
aria-label={zoomOutAriaLabel}
label={intl.formatMessage(intlMessages.zoomOutLabel)}
data-test="zoomOutBtn"
icon="substract"
onClick={() => { }}
disabled={(zoomValue <= minBound) || !isMeteorConnected}
@ -198,6 +199,7 @@ class ZoomTool extends PureComponent {
size="md"
onClick={() => this.resetZoom()}
label={intl.formatMessage(intlMessages.resetZoomLabel)}
data-test="resetZoomButton"
hideLabel
/>
<div id="resetZoomDescription" hidden>

View File

@ -497,10 +497,22 @@ class PresentationUploader extends Component {
...filteredPresentations,
...filteredPropPresentations,
];
let hasUploading
merged.forEach(d => {
if (!d.upload?.done || !d.conversion?.done) {
hasUploading = true;
}})
this.hasError = false;
return this.setState({
presentations: merged,
});
if (hasUploading) {
return this.setState({
presentations: merged,
});
} else {
return this.setState({
presentations: merged,
disableActions: false,
});
}
}
const { presentations } = this.state;

View File

@ -86,6 +86,12 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue,
const questionRegex = /.*?\?/gm;
const question = safeMatch(questionRegex, content, '');
if (question?.length > 0) {
const urlRegex = /\bhttps?:\/\/\S+\b/g;
const hasUrl = safeMatch(urlRegex, question[0], '');
if (hasUrl.length > 0) question.pop();
}
const doubleQuestionRegex = /\?{2}/gm;
const doubleQuestion = safeMatch(doubleQuestionRegex, content, false);

View File

@ -63,6 +63,9 @@ class TextInput extends PureComponent {
maxLength={maxLength}
onChange={(e) => this.handleOnChange(e)}
onKeyDown={(e) => this.handleOnKeyDown(e)}
onPaste={(e) => { e.stopPropagation(); }}
onCut={(e) => { e.stopPropagation(); }}
onCopy={(e) => { e.stopPropagation(); }}
placeholder={placeholder}
value={message}
/>

View File

@ -228,6 +228,13 @@ const VirtualBgSelector = ({
lastActivityDate: Date.now(),
},
});
const { filename, data, uniqueId } = background;
_virtualBgSelected(
EFFECT_TYPES.IMAGE_TYPE,
filename,
0,
{ file: data, uniqueId },
);
};
const onError = (error) => {
@ -354,6 +361,7 @@ const VirtualBgSelector = ({
type: 'delete',
uniqueId,
});
_virtualBgSelected(EFFECT_TYPES.NONE_TYPE);
}}
/>
</Styled.ButtonWrapper>

View File

@ -119,36 +119,36 @@ const renderGuestUserItem = (
</Styled.UserContentContainer>
<Styled.ButtonContainer key={`userlist-btns-${userId}`}>
{ isGuestLobbyMessageEnabled ? (
<Styled.WaitingUsersButton
key={`userbtn-message-${userId}`}
color="primary"
size="lg"
key={`userbtn-accept-${userId}`}
size="md"
aria-label={intl.formatMessage(intlMessages.accept)}
ghost
label={intl.formatMessage(intlMessages.privateMessageLabel)}
hideLabel
icon="add"
onClick={handleAccept}
data-test="acceptGuest"
/>
{ isGuestLobbyMessageEnabled ? (
<Styled.WaitingUsersButtonMsg
key={`userbtn-message-${userId}`}
size="lg"
aria-label={intl.formatMessage(intlMessages.privateMessageLabel)}
ghost
hideLabel
onClick={privateMessageVisible}
data-test="privateMessageGuest"
/>
) : null}
|
<Styled.WaitingUsersButton
key={`userbtn-accept-${userId}`}
color="primary"
size="lg"
ghost
label={intl.formatMessage(intlMessages.accept)}
onClick={handleAccept}
data-test="acceptGuest"
/>
|
<Styled.WaitingUsersButton
<Styled.WaitingUsersButtonDeny
key={`userbtn-deny-${userId}`}
color="danger"
size="lg"
aria-label={intl.formatMessage(intlMessages.deny)}
ghost
label={intl.formatMessage(intlMessages.deny)}
hideLabel
onClick={handleDeny}
data-test="denyGuest"
size="sm"
icon="close"
/>
</Styled.ButtonContainer>
</Styled.ListItem>

View File

@ -24,7 +24,6 @@ const ListItem = styled.div`
flex-direction: row;
align-items: center;
border-radius: 5px;
cursor: pointer;
${({ animations }) => animations && `
transition: all .3s;
@ -40,9 +39,6 @@ const ListItem = styled.div`
outline: none;
}
&:hover {
background-color: ${listItemBgHover};
}
flex-shrink: 0;
`;
@ -65,6 +61,7 @@ const UserName = styled.p`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: initial;
`;
const ButtonContainer = styled.div`
@ -73,12 +70,15 @@ const ButtonContainer = styled.div`
align-items: center;
color: ${colorPrimary};
& > button {
padding: 0 .25rem 0 .25rem;
padding: ${mdPaddingY};
font-size: ${fontSizeBase};
border-radius: 50%;
}
`;
const WaitingUsersButton = styled(Button)`
font-weight: 400;
color: ${colorPrimary};
&:focus {
background-color: ${listItemBgHover} !important;
@ -87,6 +87,42 @@ const WaitingUsersButton = styled(Button)`
}
&:hover {
color: ${colorPrimary};
background-color: ${listItemBgHover} !important;
}
`;
const WaitingUsersButtonMsg = styled(Button)`
font-weight: 400;
color: ${colorPrimary};
&:after {
font-family: 'bbb-icons';
content: "\\E910";
}
&:focus {
background-color: ${listItemBgHover} !important;
box-shadow: inset 0 0 0 ${borderSize} ${itemFocusBorder}, inset 1px 0 0 1px ${itemFocusBorder} ;
outline: none;
}
&:hover {
color: ${colorPrimary};
background-color: ${listItemBgHover} !important;
}
`;
const WaitingUsersButtonDeny = styled(Button)`
font-weight: 400;
color: #ff0e0e;
&:focus {
background-color: ${listItemBgHover} !important;
box-shadow: inset 0 0 0 ${borderSize} ${itemFocusBorder}, inset 1px 0 0 1px ${itemFocusBorder} ;
outline: none;
}
&:hover {
color: #ff0e0e;
background-color: ${listItemBgHover} !important;
}
`;
@ -205,6 +241,8 @@ export default {
UserName,
ButtonContainer,
WaitingUsersButton,
WaitingUsersButtonDeny,
WaitingUsersButtonMsg,
PendingUsers,
NoPendingUsers,
MainTitle,

View File

@ -9,7 +9,11 @@ import Cursors from './cursors/container';
import Settings from '/imports/ui/services/settings';
import logger from '/imports/startup/client/logger';
import KEY_CODES from '/imports/utils/keyCodes';
import { presentationMenuHeight } from '/imports/ui/stylesheets/styled-components/general';
import {
presentationMenuHeight,
styleMenuOffset,
styleMenuOffsetSmall
} from '/imports/ui/stylesheets/styled-components/general';
import Styled from './styles';
import PanToolInjector from './pan-tool-injector/component';
import {
@ -64,6 +68,7 @@ export default function Whiteboard(props) {
hasMultiUserAccess,
tldrawAPI,
setTldrawAPI,
isIphone,
} = props;
const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1);
const rDocument = React.useRef({
@ -897,7 +902,7 @@ export default function Whiteboard(props) {
const webcams = document.getElementById('cameraDock');
const dockPos = webcams?.getAttribute('data-position');
if (currentTool && !isPanning) tldrawAPI?.selectTool(currentTool);
if (currentTool && !isPanning && !tldrawAPI?.isForcePanning) tldrawAPI?.selectTool(currentTool);
const editableWB = (
<Styled.EditableWBWrapper onKeyDown={handleOnKeyDown}>
@ -961,6 +966,19 @@ export default function Whiteboard(props) {
}
}
const menuOffsetValues = {
true: {
true: `${styleMenuOffsetSmall}`,
false: `${styleMenuOffset}`,
},
false: {
true: `-${styleMenuOffsetSmall}`,
false: `-${styleMenuOffset}`,
},
};
const menuOffset = menuOffsetValues[isRTL][isIphone];
return (
<>
<Cursors
@ -982,7 +1000,7 @@ export default function Whiteboard(props) {
isPresenter,
size,
darkTheme,
isRTL,
menuOffset,
}}
/>
</Cursors>
@ -1007,6 +1025,7 @@ export default function Whiteboard(props) {
Whiteboard.propTypes = {
isPresenter: PropTypes.bool.isRequired,
isIphone: PropTypes.bool.isRequired,
removeShapes: PropTypes.func.isRequired,
initDefaultPages: PropTypes.func.isRequired,
persistShape: PropTypes.func.isRequired,

View File

@ -25,6 +25,7 @@ import Auth from '/imports/ui/services/auth';
import PresentationToolbarService from '../presentation/presentation-toolbar/service';
import { layoutSelect } from '../layout/context';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import deviceInfo from '/imports/utils/deviceInfo';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const WHITEBOARD_CONFIG = Meteor.settings.public.whiteboard;
@ -91,6 +92,7 @@ export default withTracker(({
}) => {
const shapes = getShapes(whiteboardId, curPageId, intl);
const curPres = getCurrentPres();
const { isIphone } = deviceInfo;
shapes['slide-background-shape'] = {
assetId: `slide-background-asset-${curPageId}`,
@ -135,6 +137,7 @@ export default withTracker(({
notifyNotAllowedChange,
notifyShapeNumberExceeded,
darkTheme,
isIphone,
};
})(WhiteboardContainer);

View File

@ -12,10 +12,10 @@ const TldrawGlobalStyle = createGlobalStyle`
display: none;
}
`}
${({ isRTL }) => `
${({ menuOffset }) => `
#TD-StylesMenu {
position: relative;
right: ${isRTL ? '7rem' : '-7rem'};
right: ${menuOffset};
}
`}
#TD-PrimaryTools-Image {
@ -23,6 +23,7 @@ const TldrawGlobalStyle = createGlobalStyle`
}
#slide-background-shape div {
pointer-events: none;
user-select: none;
}
div[dir*="ltr"]:has(button[aria-expanded*="false"][aria-controls*="radix-"]) {
pointer-events: none;
@ -80,6 +81,11 @@ const TldrawGlobalStyle = createGlobalStyle`
}
}
`}
${({ isPresenter }) => (!isPresenter) && `
#presentationInnerWrapper div{
cursor: default !important;
}
`}
`;
const EditableWBWrapper = styled.div`

View File

@ -240,33 +240,9 @@ const getFontStyle = (style) => {
https://github.com/tldraw/tldraw/blob/55a8831a6b036faae0dfd77d6733a8f585f5ae23/packages/tldraw/src/state/shapes/shared/getTextSize.ts */
const getMeasurementDiv = (font) => {
// A div used for measurement
document.getElementById('__textMeasure')?.remove();
const pre = document.getElementById('text-measure');
pre.style.font = font;
const pre = document.createElement('pre');
pre.id = '__textMeasure';
Object.assign(pre.style, {
whiteSpace: 'pre',
width: 'auto',
border: '1px solid transparent',
padding: '4px',
margin: '0px',
letterSpacing: '-0.03em',
opacity: '0',
position: 'absolute',
top: '-500px',
left: '0px',
zIndex: '9999',
pointerEvents: 'none',
userSelect: 'none',
alignmentBaseline: 'mathematical',
dominantBaseline: 'mathematical',
font,
});
pre.tabIndex = -1;
document.body.appendChild(pre);
return pre;
}
@ -278,16 +254,13 @@ const getTextSize = (text, style, padding) => {
}
const melm = getMeasurementDiv(font);
melm.textContent = text;
if (!melm) {
// We're in SSR
return [10, 10];
}
if (!melm.parent) document.body.appendChild(melm);
melm.textContent = text;
// In tests, offsetWidth and offsetHeight will be 0
const width = melm.offsetWidth || 1;
const height = melm.offsetHeight || 1;

View File

@ -86,6 +86,9 @@ const toastIconSm = '1.2rem';
const presentationMenuHeight = '45px';
const styleMenuOffset = '6.25rem';
const styleMenuOffsetSmall = '5rem';
export {
borderSizeSmall,
borderSize,
@ -165,4 +168,6 @@ export {
toastIconMd,
toastIconSm,
presentationMenuHeight,
styleMenuOffset,
styleMenuOffsetSmall,
};

View File

@ -17,6 +17,10 @@ import {
const GlobalStyle = createGlobalStyle`
// BBBMenu
@media ${smallOnly} {
.MuiPopover-root {
top: 0 !important;
}
.MuiPaper-root.MuiMenu-paper.MuiPopover-paper {
top: 0 !important;
left: 0 !important;

View File

@ -5,8 +5,8 @@
"app.chat.errorMaxMessageLength": "پیام خیلی طولانی است، از حداکثر {0} کاراکتر بیشتر است",
"app.chat.disconnected": "ارتباط شما قطع شده است، امکان ارسال پیام وجود ندارد",
"app.chat.locked": "گفنگو قفل شده است، امکان ارسال هیچ پیامی وجود ندارد",
"app.chat.inputLabel": "ورودی پیام برای گفتگو {0}",
"app.chat.emojiButtonLabel": "انتخاب کننده شکلک",
"app.chat.inputLabel": "ورودی پیام برای گفتگوی {0}",
"app.chat.emojiButtonLabel": "انتخابکننده شکلک",
"app.chat.inputPlaceholder": "پیام {0}",
"app.chat.titlePublic": "گفتگوی عمومی",
"app.chat.titlePrivate": "گفتگوی خصوصی با {0}",
@ -15,13 +15,13 @@
"app.chat.hideChatLabel": "پنهان‌سازی {0}",
"app.chat.moreMessages": "ادامه پیام‌ها در پایین",
"app.chat.dropdown.options": "گزینه‌های گفتگو",
"app.chat.dropdown.clear": "پاکسازی",
"app.chat.dropdown.clear": "پاکسازی",
"app.chat.dropdown.copy": "کپی",
"app.chat.dropdown.save": "ذخیره",
"app.chat.label": "گفتگو",
"app.chat.offline": "آفلاین",
"app.chat.pollResult": "نتایج نظرسنجی",
"app.chat.breakoutDurationUpdated": "زمان جلسه زیرمجموعه اکنون {0} دقیقه است",
"app.chat.breakoutDurationUpdated": "زمان جلسه جانبی اکنون {0} دقیقه است",
"app.chat.breakoutDurationUpdatedModerator": "زمان اتاق‌های جانبی اکنون {0} دقیقه است و آگاه‌سازی ارسال شده است.",
"app.chat.emptyLogLabel": "سابقه گفتگو خالی است",
"app.chat.clearPublicChatMessage": "سابقه گفتگوی عمومی توسط مدیر حذف گردید",
@ -29,11 +29,11 @@
"app.chat.one.typing": "{0} در حال نوشتن است",
"app.chat.two.typing": "{0} و {1} در حال نوشتن هستند",
"app.chat.copySuccess": "رونوشت گفتگو کپی شد",
"app.chat.copyErr": "کپی رونوشت گفتگو با خطا مواجه شد!",
"app.chat.copyErr": "کپی رونوشت گفتگو با خطا مواجه شد",
"app.emojiPicker.search": "جستجو",
"app.emojiPicker.notFound": "هیچ شکلکی پیدا نشد",
"app.emojiPicker.skintext": "رنگ پوست پیش‌فرض خود را انتخاب کنید",
"app.emojiPicker.clear": "پاکسازی",
"app.emojiPicker.clear": "پاکسازی",
"app.emojiPicker.categories.label": "دسته‌بندی شکلک‌ها",
"app.emojiPicker.categories.people": "مردم و بدن",
"app.emojiPicker.categories.nature": "حیوانات و طبیعت",
@ -43,7 +43,7 @@
"app.emojiPicker.categories.objects": "اشیاء",
"app.emojiPicker.categories.symbols": "نمادها",
"app.emojiPicker.categories.flags": "پرچم‌ها",
"app.emojiPicker.categories.recent": "اغلب استفاده می‌شود",
"app.emojiPicker.categories.recent": "پراستفاده",
"app.emojiPicker.categories.search": "نتایج جستجو",
"app.emojiPicker.skintones.1": "رنگ پوست پیش‌فرض",
"app.emojiPicker.skintones.2": "رنگ پوست روشن",
@ -54,7 +54,7 @@
"app.captions.label": "زیرنویس‌ها",
"app.captions.menu.close": "بستن",
"app.captions.menu.start": "شروع",
"app.captions.menu.ariaStart": "آغاز نوشتن زیرنویس‌ها",
"app.captions.menu.ariaStart": "شروع نوشتن زیرنویس‌ها",
"app.captions.menu.ariaStartDesc": "ویرایشگر زیرنویس‌ها را باز کرده و پنجره را می‌بندد",
"app.captions.menu.select": "انتخاب زبان موجود",
"app.captions.menu.ariaSelect": "زبان زیرنویس‌ها",
@ -69,7 +69,7 @@
"app.captions.hide": "پنهان‌سازی زیرنویس‌ها",
"app.captions.ownership": "برعهده گرفتن",
"app.captions.ownershipTooltip": "شما به عنوان صاحب زیرنویس‌های {0} منسوب خواهید شد",
"app.captions.dictationStart": "آغاز نوشتن کلمات",
"app.captions.dictationStart": "شروع نوشتن کلمات",
"app.captions.dictationStop": "توقف نوشتن کلمات",
"app.captions.dictationOnDesc": "تشخیص گفتار را روشن می‌کند",
"app.captions.dictationOffDesc": "تشخیص گفتار را خاموش می‌کند",
@ -78,7 +78,7 @@
"app.captions.speech.error": "به دلیل ناسازگاری مرورگر یا مدتی سکوت، تشخیص گفتار متوقف شد",
"app.confirmation.skipConfirm": "دوباره نپرس",
"app.confirmation.virtualBackground.title": "شروع پس‌زمینه مجازی جدید",
"app.confirmation.virtualBackground.description": "{0} به عنوان پس‌زمینه مجازی اضافه خواهد شد. ادامه؟",
"app.confirmation.virtualBackground.description": "{0} به عنوان پس‌زمینه مجازی اضافه خواهد شد. ادامه می‌دهید؟",
"app.confirmationModal.yesLabel": "بله",
"app.textInput.sendLabel": "ارسال",
"app.title.defaultViewLabel": "نمای پیش‌فرض ارائه",
@ -137,7 +137,7 @@
"app.userList.menu.makePresenter.label": "تغییر نقش به ارائه‌دهنده",
"app.userList.userOptions.manageUsersLabel": "مدیریت کاربران",
"app.userList.userOptions.muteAllLabel": "بستن صدای همه کاربران",
"app.userList.userOptions.muteAllDesc": "بستن صدای همه کاربران حاضر در جلسه",
"app.userList.userOptions.muteAllDesc": "صدای همه کاربران حاضر در جلسه را می‌بندد",
"app.userList.userOptions.clearAllLabel": "پاک‌کردن نماد همه وضعیت‌ها",
"app.userList.userOptions.clearAllDesc": "همه نمادهای وضعیت را از کاربران پاک می‌کند",
"app.userList.userOptions.muteAllExceptPresenterLabel": "بستن صدای همه کاربران به جز ارائه‌دهنده",
@ -194,8 +194,8 @@
"app.meeting.endedMessage": "شما در حال انتقال به صفحه اصلی هستید",
"app.meeting.alertMeetingEndsUnderMinutesSingular": "جلسه تا یک دقیقه دیگر به پایان می‌رسد.",
"app.meeting.alertMeetingEndsUnderMinutesPlural": "جلسه تا {0} دقیقه دیگر به پایان می‌رسد.",
"app.meeting.alertBreakoutEndsUnderMinutesPlural": "جلسه زیرمجموعه تا {0} دقیقه دیگر به پایان می‌رسد.",
"app.meeting.alertBreakoutEndsUnderMinutesSingular": "جلسه زیرمجموعه تا یک دقیقه دیگر به پایان می‌رسد.",
"app.meeting.alertBreakoutEndsUnderMinutesPlural": "اتاق‌های جانبی تا {0} دقیقه دیگر به پایان می‌رسد.",
"app.meeting.alertBreakoutEndsUnderMinutesSingular": "جلسه جانبی تا یک دقیقه دیگر به پایان می‌رسد.",
"app.presentation.hide": "پنهان‌سازی ارائه",
"app.presentation.notificationLabel": "ارائه کنونی",
"app.presentation.downloadLabel": "بارگیری",
@ -299,7 +299,7 @@
"app.presentationUploder.item" : "مورد",
"app.presentationUploder.itemPlural" : "موارد",
"app.presentationUploder.clearErrors": "پاک‌کردن خطاها",
"app.presentationUploder.clearErrorsDesc": "پاک‌کردن بارگذاری‌های ناموفق ارائه",
"app.presentationUploder.clearErrorsDesc": "بارگذاری‌های ناموفق ارائه را پاک می‌کند",
"app.presentationUploder.uploadViewTitle": "بارگذاری ارائه ",
"app.poll.questionAndoptions.label" : "متن سوال نمایش داده شود.\nالف. گزینه نظرسنجی *\nب. گزینه نظرسنجی (اختیاری)\nپ. گزینه نظرسنجی (اختیاری)\nت. گزینه نظرسنجی (اختیاری)\nث. گزینه نظرسنجی (اختیاری)",
"app.poll.customInput.label": "ورودی سفارشی",
@ -315,7 +315,7 @@
"app.poll.customPollTextArea": "پرکردن مقادیر نظرسنجی",
"app.poll.publishLabel": "انتشار نتایج نظرسنجی",
"app.poll.cancelPollLabel": "لغو",
"app.poll.backLabel": "آغاز یک نظرسنجی",
"app.poll.backLabel": "شروع یک نظرسنجی",
"app.poll.closeLabel": "بستن",
"app.poll.waitingLabel": "در انتظار پاسخ‌ها ({0}/{1})",
"app.poll.ariaInputCount": "گزینه نظرسنجی سفارشی {0} از {1}",
@ -330,7 +330,7 @@
"app.poll.responseChoices.label" : "انتخاب‌های پاسخ",
"app.poll.typedResponse.desc" : "یک جعبه متن به کاربران برای پرکردن پاسخشان نمایش داده می شود.",
"app.poll.addItem.label" : "افزودن مورد",
"app.poll.start.label" : "آغاز نظرسنجی",
"app.poll.start.label" : "شروع نظرسنجی",
"app.poll.secretPoll.label" : "نظرسنجی ناشناس",
"app.poll.secretPoll.isSecretLabel": "این نظرسنجی ناشناس است - شما قادر به دیدن پاسخ‌های فردی نخواهید بود.",
"app.poll.questionErr": "ارائه یک سوال الزامی است.",
@ -574,11 +574,11 @@
"app.actionsBar.actionsDropdown.initPollDesc": "آغاز یک نظرسنجی",
"app.actionsBar.actionsDropdown.desktopShareDesc": "صفحه خود را با دیگران به اشتراک بگذارید",
"app.actionsBar.actionsDropdown.stopDesktopShareDesc": "متوقف‌کردن اشتراک‌گذاری صفحه خود با",
"app.actionsBar.actionsDropdown.pollBtnLabel": "آغاز یک نظرسنجی",
"app.actionsBar.actionsDropdown.pollBtnLabel": "شروع یک نظرسنجی",
"app.actionsBar.actionsDropdown.pollBtnDesc": "وضعیت پنجره نظرسنجی را تغییر می‌دهد ",
"app.actionsBar.actionsDropdown.saveUserNames": "ذخیره نام‌های کاربری",
"app.actionsBar.actionsDropdown.createBreakoutRoom": "ایجاد اتاق‌های زیرمجموعه",
"app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "ایجاد اتاق‌های زیرمجموعه برای چندتکه‌کردن جلسه کنونی",
"app.actionsBar.actionsDropdown.createBreakoutRoom": "ایجاد اتاق‌های جانبی",
"app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "ایجاد اتاق‌های جانبی برای چندتکه‌کردن جلسه کنونی",
"app.actionsBar.actionsDropdown.captionsLabel": "نوشتن زیرنویس",
"app.actionsBar.actionsDropdown.captionsDesc": "وضعیت پنجره زیرنویس‌ها را تغییر می‌دهد",
"app.actionsBar.actionsDropdown.takePresenter": "گرفتن نقش ارائه‌دهنده",
@ -609,7 +609,7 @@
"app.actionsBar.emojiMenu.thumbsDownLabel": "نه چندان خوب",
"app.actionsBar.emojiMenu.thumbsDownDesc": "تغییر وضعیت خود به نه چندان خوب",
"app.actionsBar.currentStatusDesc": "وضعیت کنونی {0}",
"app.actionsBar.captions.start": "آغار مشاهده زیرنویس‌ها",
"app.actionsBar.captions.start": "شروع مشاهده زیرنویس‌ها",
"app.actionsBar.captions.stop": "توقف مشاهده زیرنویس‌ها",
"app.audioNotification.audioFailedError1001": "ارتباط وب‌سوکت قطع شد (خطای ۱۰۰۱)",
"app.audioNotification.audioFailedError1002": "امکان برقراری وب‌سوکت وجود ندارد (خطای ۱۰۰۲)",
@ -628,17 +628,17 @@
"app.audioNotification.deviceChangeFailed": "تغییر دستگاه صوتی انجام نشد. بررسی کنید که آیا دستگاه انتخاب‌شده به درستی تنظیم شده و در دسترس باشد",
"app.audioNotification.closeLabel": "بستن",
"app.audioNotificaion.reconnectingAsListenOnly": "میکروفن برای کاربران قفل شده است، شما به عنوان شنونده به جلسه متصل خواهید شد ",
"app.breakoutJoinConfirmation.title": "پیوستن به اتاق زیرمجموعه",
"app.breakoutJoinConfirmation.title": "پیوستن به اتاق جانبی",
"app.breakoutJoinConfirmation.message": "آیا مایل به پیوستن هستید",
"app.breakoutJoinConfirmation.confirmDesc": "شما را به اتاق زیرمجموعه منتقل می‌کند",
"app.breakoutJoinConfirmation.confirmDesc": "شما را به اتاق جانبی منتقل می‌کند",
"app.breakoutJoinConfirmation.dismissLabel": "لغو",
"app.breakoutJoinConfirmation.dismissDesc": "امکان پیوستن به اتاق زیرمجموعه را بسته و رد می‌کند",
"app.breakoutJoinConfirmation.freeJoinMessage": "یک اتاق زیرمجموعه را برای پیوستن انتخاب کنید",
"app.breakoutTimeRemainingMessage": "زمان باقی‌مانده از اتاق زیرمجموعه: {0}",
"app.breakoutWillCloseMessage": "زمان به پایان رسید. اتاق زیرمجموعه به زودی بسته خواهد شد",
"app.breakoutJoinConfirmation.dismissDesc": "امکان پیوستن به اتاق جانبی را بسته و رد می‌کند",
"app.breakoutJoinConfirmation.freeJoinMessage": "یک اتاق جانبی را برای پیوستن انتخاب کنید",
"app.breakoutTimeRemainingMessage": "زمان باقی‌مانده از اتاق جانبی: {0}",
"app.breakoutWillCloseMessage": "زمان به پایان رسید. اتاق جانبی به زودی بسته خواهد شد",
"app.breakout.dropdown.manageDuration": "تغییر مدت زمان",
"app.breakout.dropdown.destroyAll": "اتمام اتاق‌های زیرمجموعه",
"app.breakout.dropdown.options": "گزینه‌های اتاق‌های زیرمجموعه",
"app.breakout.dropdown.destroyAll": "اتمام اتاق‌های جانبی",
"app.breakout.dropdown.options": "گزینه‌های اتاق‌های جانبی",
"app.breakout.dropdown.manageUsers": "مدیریت کاربران",
"app.calculatingBreakoutTimeRemaining": "در حال محاسبه زمان باقی مانده ...",
"app.audioModal.ariaTitle": "پنجره پیوستن به صدا",
@ -804,7 +804,7 @@
"app.userList.guest.denyLabel": "ردکردن",
"app.userList.guest.feedbackMessage": "اقدام اعمال‌شده:",
"app.user-info.title": "جستجوی دایرکتوری",
"app.toast.breakoutRoomEnded": "اتاق زیرمجموعه بسته شد، لطفا دوباره به صدا بپیوندید.",
"app.toast.breakoutRoomEnded": "اتاق جانبی بسته شد، لطفا دوباره به صدا بپیوندید.",
"app.toast.chat.public": "پیام جدید در گفتگوی عمومی",
"app.toast.chat.private": "پیام جدید در گفتگوی خصوصی",
"app.toast.chat.system": "سیستم",
@ -955,7 +955,7 @@
"app.videoPreview.cancelLabel": "لغو",
"app.videoPreview.closeLabel": "بستن",
"app.videoPreview.findingWebcamsLabel": "در جستجوی دوربین‌ها",
"app.videoPreview.startSharingLabel": "آغاز اشتراک‌گذاری",
"app.videoPreview.startSharingLabel": "شروع اشتراک‌گذاری",
"app.videoPreview.stopSharingLabel": "توقف اشتراک‌گذاری",
"app.videoPreview.stopSharingAllLabel": "متوقف‌کردن همه",
"app.videoPreview.sharedCameraLabel": "این دوربین از قبل به اشتراک گذاشته شده است",
@ -1018,13 +1018,13 @@
"app.video.virtualBackground.custom": "از رایانه خود بارگذاری کنید",
"app.video.virtualBackground.remove": "حذف تصویر اضافه شده",
"app.video.virtualBackground.genericError": "جلوه دوربین اعمال نشد. دوباره تلاش کنید.",
"app.video.virtualBackground.camBgAriaDesc": "تنظیم پس‌زمینه مجازی دوربین به {0}",
"app.video.virtualBackground.camBgAriaDesc": "پس‌زمینه مجازی دوربین را روی {0} تنظیم می‌کند",
"app.video.virtualBackground.maximumFileSizeExceeded": "از حداکثر اندازه پرونده بیشتر شده است. ({0} مگابایت)",
"app.video.virtualBackground.typeNotAllowed": "نوع پرونده مجاز نیست.",
"app.video.virtualBackground.errorOnRead": "هنگام خواندن پرونده مشکلی پیش آمد.",
"app.video.virtualBackground.uploaded": "بارگذاری شد",
"app.video.virtualBackground.uploading": "در حال بارگذاری...",
"app.video.virtualBackground.button.customDesc": "افزودن یک تصویر پس‌زمینه مجازی",
"app.video.virtualBackground.button.customDesc": "یک تصویر پس‌زمینه مجازی را اضافه می‌کند",
"app.video.camCapReached": "نمی‌توانید دوربین‌های بیشتری را به اشتراک بگذارید",
"app.video.meetingCamCapReached": "جلسه به حد مجاز دوربین‌های همزمان خود رسیده است",
"app.video.dropZoneLabel": "اینجا رها کنید",
@ -1100,12 +1100,12 @@
"app.videoDock.webcamUnpinDesc": "سنجاق دوربین انتخاب‌شده را بردارید",
"app.videoDock.autoplayBlockedDesc": "ما برای نمایش دوربین کاربران دیگر به شما، به اجازهٔ شما نیازمندیم.",
"app.videoDock.autoplayAllowLabel": "مشاهده دوربین‌ها",
"app.createBreakoutRoom.title": "اتاق‌های زیرمجموعه",
"app.createBreakoutRoom.ariaTitle": "پنهان‌کردن اتاق‌های زیرمجموعه",
"app.createBreakoutRoom.breakoutRoomLabel": "اتاق‌های زیرمجموعه {0}",
"app.createBreakoutRoom.title": "اتاق‌های جانبی",
"app.createBreakoutRoom.ariaTitle": "پنهان‌کردن اتاق‌های جانبی",
"app.createBreakoutRoom.breakoutRoomLabel": "اتاق‌های جانبی {0}",
"app.createBreakoutRoom.askToJoin": "درخواست برای پیوستن",
"app.createBreakoutRoom.generatingURL": "در حال تولید نشانی وب",
"app.createBreakoutRoom.generatingURLMessage": "ما در حال ایجاد یک نشانی وب برای پیوستن به اتاق زیرمجموعه انتخاب‌شده هستیم. شاید چند ثانیه طول بکشد …",
"app.createBreakoutRoom.generatingURLMessage": "ما در حال ایجاد یک نشانی وب برای پیوستن به اتاق جانبی انتخاب‌شده هستیم. شاید چند ثانیه طول بکشد …",
"app.createBreakoutRoom.duration": "مدت زمان {0}",
"app.createBreakoutRoom.room": "اتاق {0}",
"app.createBreakoutRoom.notAssigned": "اختصاص داده نشده ({0})",
@ -1118,23 +1118,23 @@
"app.createBreakoutRoom.numberOfRooms": "تعداد اتاق‌ها",
"app.createBreakoutRoom.durationInMinutes": "مدت زمان (دقیقه)",
"app.createBreakoutRoom.randomlyAssign": "به صورت تصادفی اختصاص بده",
"app.createBreakoutRoom.randomlyAssignDesc": "کاربران را به صورت تصادفی به اتاق‌های زیرمجموعه اختصاص می‌دهد",
"app.createBreakoutRoom.randomlyAssignDesc": "کاربران را به صورت تصادفی به اتاق‌های جانبی اختصاص می‌دهد",
"app.createBreakoutRoom.resetAssignments": "بازنشانی اختصاص‌شده‌ها",
"app.createBreakoutRoom.resetAssignmentsDesc": "همه کاربران اختصاص‌شده به اتاق را بازنشانی کنید",
"app.createBreakoutRoom.endAllBreakouts": "پایان تمام اتاق‌های زیرمجموعه",
"app.createBreakoutRoom.endAllBreakouts": "پایان تمام اتاق‌های جانبی",
"app.createBreakoutRoom.chatTitleMsgAllRooms": "همه اتاق‌ها",
"app.createBreakoutRoom.msgToBreakoutsSent": "پیام به {0} اتاق زیرمجموعه ارسال شد",
"app.createBreakoutRoom.msgToBreakoutsSent": "پیام به {0} اتاق جانبی ارسال شد",
"app.createBreakoutRoom.roomName": "{0} (اتاق - {1})",
"app.createBreakoutRoom.doneLabel": "انجام شد",
"app.createBreakoutRoom.nextLabel": "بعدی",
"app.createBreakoutRoom.minusRoomTime": "کاهش زمان اتاق زیرمجموعه به",
"app.createBreakoutRoom.addRoomTime": "افزایش زمان اتاق زیرمجموعه به ",
"app.createBreakoutRoom.minusRoomTime": "کاهش زمان اتاق جانبی به",
"app.createBreakoutRoom.addRoomTime": "افزایش زمان اتاق جانبی به ",
"app.createBreakoutRoom.addParticipantLabel": "+ افزودن شرکت کننده",
"app.createBreakoutRoom.freeJoin": "اجازه انتخاب اتاق زیرمجموعه به کاربران برای پیوستن",
"app.createBreakoutRoom.captureNotes": "ثبت یادداشت‌های اشتراکی پس از پایان اتاق‌های زیرمجموعه",
"app.createBreakoutRoom.captureSlides": "ثبت تخته‌سفید پس از پایان اتاق‌های زیرمجموعه",
"app.createBreakoutRoom.leastOneWarnBreakout": "شما باید حداقل یک کاربر را در هر اتاق زیرمجموعه قرار دهید.",
"app.createBreakoutRoom.minimumDurationWarnBreakout": "حداقل مدت زمان اتاق زیرمجموعه {0} دقیقه است.",
"app.createBreakoutRoom.freeJoin": "اجازه انتخاب اتاق جانبی به کاربران برای پیوستن",
"app.createBreakoutRoom.captureNotes": "ثبت یادداشت‌های اشتراکی پس از پایان اتاق‌های جانبی",
"app.createBreakoutRoom.captureSlides": "ثبت تخته‌سفید پس از پایان اتاق‌های جانبی",
"app.createBreakoutRoom.leastOneWarnBreakout": "شما باید حداقل یک کاربر را در هر اتاق جانبی قرار دهید.",
"app.createBreakoutRoom.minimumDurationWarnBreakout": "حداقل مدت زمان اتاق جانبی {0} دقیقه است.",
"app.createBreakoutRoom.modalDesc": "نکته: می‌توانید نام یک کاربر را بکشید و رها کنید تا به آن‌ها اتاقی را اختصاص دهید. ",
"app.createBreakoutRoom.roomTime": "{0} دقیقه",
"app.createBreakoutRoom.numberOfRoomsError": "تعداد اتاق‌ها نامعتبر است.",
@ -1143,12 +1143,12 @@
"app.createBreakoutRoom.setTimeInMinutes": "تنظیم مدت زمان به (دقیقه)",
"app.createBreakoutRoom.setTimeLabel": "اعمال",
"app.createBreakoutRoom.setTimeCancel": "لغو",
"app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "مدت زمان اتاق‌های زیرمجموعه نمی‌تواند از زمان جلسه بیشتر باشد.",
"app.createBreakoutRoom.roomNameInputDesc": "به‌روزرسانی نام اتاق‌های زیر مجموعه",
"app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "مدت زمان اتاق‌های جانبی نمی‌تواند از زمان جلسه بیشتر باشد.",
"app.createBreakoutRoom.roomNameInputDesc": "نام اتاق‌های جانبی را به‌روزرسانی می‌کند",
"app.createBreakoutRoom.movedUserLabel": "{0} به اتاق {1} منتقل شد",
"app.updateBreakoutRoom.modalDesc": "برای به‌روزرسانی یا دعوت از یک کاربر، به سادگی آنها را به اتاق مورد نظر بکشید.",
"app.updateBreakoutRoom.cancelLabel": "لغو",
"app.updateBreakoutRoom.title": روزرسانی اتاق‌های زیرمجموعه",
"app.updateBreakoutRoom.title": ه‌روزرسانی اتاق‌های جانبی",
"app.updateBreakoutRoom.confirm": "اعمال",
"app.updateBreakoutRoom.userChangeRoomNotification": "شما به اتاق {0} منتقل شدید.",
"app.smartMediaShare.externalVideo": "ویدیو(های) خارجی",

View File

@ -28,8 +28,8 @@
"app.chat.multi.typing": "Plusieurs utilisateurs sont en train d'écrire",
"app.chat.one.typing": "{0} est en train d'écrire",
"app.chat.two.typing": "{0} et {1} sont en train d'écrire",
"app.chat.copySuccess": "Historique de discussion copié",
"app.chat.copyErr": "La copie de l'historique de discussion a échouée",
"app.chat.copySuccess": "La transcription de la discussion est copiée",
"app.chat.copyErr": "La copie de la transcription de la discussion a échouée",
"app.emojiPicker.search": "Rechercher",
"app.emojiPicker.notFound": "Aucun émoticône trouvé",
"app.emojiPicker.skintext": "Choisissez une teinte par défaut",
@ -51,22 +51,22 @@
"app.emojiPicker.skintones.4": "Ton mat",
"app.emojiPicker.skintones.5": "Ton mat-foncé",
"app.emojiPicker.skintones.6": "Ton foncé",
"app.captions.label": "Sous-titre",
"app.captions.label": "Sous-titres",
"app.captions.menu.close": "Fermer",
"app.captions.menu.start": "Démarrer",
"app.captions.menu.ariaStart": "Démarrer l'écriture des sous-titres",
"app.captions.menu.ariaStart": "Démarrer le sous-titrage",
"app.captions.menu.ariaStartDesc": "Ouvre l'éditeur de sous-titres et ferme la fenêtre de dialogue",
"app.captions.menu.select": "Sélectionnez une langue disponible",
"app.captions.menu.ariaSelect": "Langue des sous-titres",
"app.captions.menu.subtitle": "Veuillez sélectionner une langue et les styles pour les sous-titres de votre réunion.",
"app.captions.menu.title": "Sous-titres codés",
"app.captions.menu.title": "Sous-titres SEM",
"app.captions.menu.fontSize": "Taille",
"app.captions.menu.fontColor": "Couleur du texte",
"app.captions.menu.fontFamily": "Police d'écriture",
"app.captions.menu.backgroundColor": "Couleur du fond",
"app.captions.menu.backgroundColor": "Couleur de l'arrière-plan",
"app.captions.menu.previewLabel": "Prévisualiser",
"app.captions.menu.cancelLabel": "Annuler",
"app.captions.hide": "Cacher les sous-titres",
"app.captions.hide": "Occulter les sous-titres",
"app.captions.ownership": "Prendre le contrôle",
"app.captions.ownershipTooltip": "Vous serez désigné comme propriétaire de {0} sous-titres.",
"app.captions.dictationStart": "Démarrer la dictée",
@ -81,7 +81,7 @@
"app.confirmation.virtualBackground.description": "{0} sera ajouté comme arrière-plan virtuel. Poursuivre?",
"app.confirmationModal.yesLabel": "Oui",
"app.textInput.sendLabel": "Envoyer",
"app.title.defaultViewLabel": "Vue par défaut de la présentation",
"app.title.defaultViewLabel": "Visualiser la présentation par défaut ",
"app.notes.title": "Notes partagées",
"app.notes.titlePinned": "Notes partagées (épinglées)",
"app.notes.pinnedNotification": "Les notes partagées sont maintenant épinglées sur le tableau blanc.",
@ -102,7 +102,7 @@
"app.userList.messagesTitle": "Messages",
"app.userList.notesTitle": "Notes",
"app.userList.notesListItem.unreadContent": "Nouveau contenu disponible dans la section des notes partagées",
"app.userList.captionsTitle": "Sous-titre",
"app.userList.captionsTitle": "Sous-titres",
"app.userList.presenter": "Présentateur",
"app.userList.you": "Vous",
"app.userList.locked": "Verrouillé",
@ -120,19 +120,19 @@
"app.userList.menu.clearStatus.label": "Effacer le statut",
"app.userList.menu.removeUser.label": "Retirer l'utilisateur",
"app.userList.menu.removeConfirmation.label": "Retirer l'utilisateur ({0})",
"app.userlist.menu.removeConfirmation.desc": "Empêcher cet utilisateur de rejoindre de nouveau la réunion.",
"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.webcamPin.label": "Épingler la caméra de ce participant",
"app.userList.menu.webcamUnpin.label": "Ne plus é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",
"app.userList.menu.removeWhiteboardAccess.label": "Supprimer l'accès au tableau blanc",
"app.userList.menu.ejectUserCameras.label": "Eteindre les caméras",
"app.userList.userAriaLabel": "{0} {1} {2} État {3}",
"app.userList.userAriaLabel": "{0} {1} {2} Statut {3}",
"app.userList.menu.promoteUser.label": "Promouvoir comme modérateur",
"app.userList.menu.demoteUser.label": "Rétrograder en participant normal",
"app.userList.menu.unlockUser.label": "Débloquer {0}",
"app.userList.menu.lockUser.label": "Bloquer {0}",
"app.userList.menu.demoteUser.label": "Rétrograder comme spectateur",
"app.userList.menu.unlockUser.label": "Déverrouiller {0}",
"app.userList.menu.lockUser.label": "Verrouiller {0}",
"app.userList.menu.directoryLookup.label": "Recherche dans l'annuaire",
"app.userList.menu.makePresenter.label": "Définir comme présentateur",
"app.userList.userOptions.manageUsersLabel": "Gérer les utilisateurs",
@ -142,8 +142,8 @@
"app.userList.userOptions.clearAllDesc": "Effacer toutes les icônes de statut des utilisateurs",
"app.userList.userOptions.muteAllExceptPresenterLabel": "Rendre silencieux tous les utilisateurs sauf le présentateur",
"app.userList.userOptions.muteAllExceptPresenterDesc": "Rend silencieux tous les utilisateurs de la réunion sauf le présentateur",
"app.userList.userOptions.unmuteAllLabel": "Désactiver l'interdiction de parler pour la réunion",
"app.userList.userOptions.unmuteAllDesc": "Désactive l'interdiction de parler pour la réunion",
"app.userList.userOptions.unmuteAllLabel": "Désactiver le mode silencieux de la réunion",
"app.userList.userOptions.unmuteAllDesc": "Désactive le mode silencieux de la réunion",
"app.userList.userOptions.lockViewersLabel": "Gestion des interactions",
"app.userList.userOptions.lockViewersDesc": "Verrouiller certaines fonctionnalités pour les participants à la réunion",
"app.userList.userOptions.guestPolicyLabel": "Gestion des accès",
@ -152,7 +152,7 @@
"app.userList.userOptions.disableMic": "Les microphones des participants sont désactivés",
"app.userList.userOptions.disablePrivChat": "La discussion privée est désactivée",
"app.userList.userOptions.disablePubChat": "La discussion publique est désactivée",
"app.userList.userOptions.disableNotes": "Les notes partagées sont maintenant verrouillées",
"app.userList.userOptions.disableNotes": "Les notes partagées sont désormais verrouillées",
"app.userList.userOptions.hideUserList": "La liste des utilisateurs est maintenant masquée pour les participants",
"app.userList.userOptions.webcamsOnlyForModerator": "Seuls les modérateurs peuvent voir les webcams des participants (selon les paramètres de verrouillage)",
"app.userList.content.participants.options.clearedStatus": "Tous les statuts d'utilisateurs sont effacés",
@ -162,34 +162,34 @@
"app.userList.userOptions.enablePubChat": "La discussion publique est activée",
"app.userList.userOptions.enableNotes": "Les notes partagées sont maintenant activées",
"app.userList.userOptions.showUserList": "La liste des utilisateurs est maintenant affichée aux participants",
"app.userList.userOptions.enableOnlyModeratorWebcam": "Vous pouvez activer votre webcam maintenant, tout le monde vous verra",
"app.userList.userOptions.savedNames.title": "Liste des utilisateurs de la conférence {0} à {1}",
"app.userList.userOptions.sortedFirstName.heading": "Trié par prénom :",
"app.userList.userOptions.sortedLastName.heading": "Trié par nom :",
"app.userList.userOptions.enableOnlyModeratorWebcam": "Vous pouvez désormais activer votre webcam, tout le monde vous verra",
"app.userList.userOptions.savedNames.title": "Liste des utilisateurs en réunion {0} à {1}",
"app.userList.userOptions.sortedFirstName.heading": "Tri par prénom :",
"app.userList.userOptions.sortedLastName.heading": "Tri par nom :",
"app.userList.userOptions.hideViewersCursor": "Les pointeurs des participants ont été verrouillés ",
"app.userList.userOptions.showViewersCursor": "Les pointeurs des participants sont déverrouillés",
"app.media.label": "Média",
"app.media.autoplayAlertDesc": "Autoriser l'accès",
"app.media.screenshare.start": "Partage d'écran commencé",
"app.media.screenshare.start": "Partage d'écran démarré",
"app.media.screenshare.end": "Partage d'écran terminé",
"app.media.screenshare.endDueToDataSaving": "Le partage d'écran est arrêté pour favoriser l'économie de données.",
"app.media.screenshare.endDueToDataSaving": "Le partage d'écran est arrêté pour économiser le flux de données.",
"app.media.screenshare.unavailable": "Partage d'écran indisponible",
"app.media.screenshare.notSupported": "Le partage d'écran n'est pas pris en charge par ce navigateur.",
"app.media.screenshare.notSupported": "Le partage d'écran n'est pas disponible pour ce navigateur.",
"app.media.screenshare.autoplayBlockedDesc": "Nous avons besoin de votre permission pour vous montrer l'écran du présentateur.",
"app.media.screenshare.autoplayAllowLabel": "Voir l'écran partagé",
"app.screenshare.presenterLoadingLabel": " Votre partage d'écran est en cours de chargement",
"app.screenshare.presenterLoadingLabel": "Votre partage d'écran est en cours de chargement",
"app.screenshare.viewerLoadingLabel": "L'écran du présentateur est en cours de chargement",
"app.screenshare.presenterSharingLabel": "Vous partagez maintenant votre écran",
"app.screenshare.screenshareFinalError": "Code {0}. Ne peut pas partager l'écran.",
"app.screenshare.presenterSharingLabel": "Vous partagez désormais votre écran",
"app.screenshare.screenshareFinalError": "Code {0}. Impossible de partager l'écran.",
"app.screenshare.screenshareRetryError": "Code {0}. Essayez à nouveau de partager l'écran.",
"app.screenshare.screenshareRetryOtherEnvError": "Code {0}. Ne peut pas partager l'écran. Essayez à nouveau en utilisant un navigateur ou appareil différent.",
"app.screenshare.screenshareUnsupportedEnv": "Code {0}. Le navigateur n'est pas pris en charge. Essayez à nouveau en utilisant un navigateur ou appareil différent.",
"app.screenshare.screenshareRetryOtherEnvError": "Code {0}. Impossible de partager l'écran. Essayez à nouveau en utilisant un autre navigateur ou un appareil différent.",
"app.screenshare.screenshareUnsupportedEnv": "Code {0}. Le navigateur n'est pas pris en charge. Essayez à nouveau en utilisant un autre navigateur ou un appareil différent.",
"app.screenshare.screensharePermissionError": "Code {0}. L'autorisation de capturer d'écran doit être accordée.",
"app.meeting.ended": "Cette réunion est terminée",
"app.meeting.meetingTimeRemaining": "Temps de réunion restant : {0}",
"app.meeting.meetingTimeHasEnded": "Le temps s'est écoulé. La réunion sera bientôt close",
"app.meeting.endedByUserMessage": "Cette réunion a été close par {0}",
"app.meeting.endedByNoModeratorMessageSingular": "La réunion est terminée car aucun modérateur n'est présent depuis plus d'une minute.",
"app.meeting.endedByNoModeratorMessageSingular": "La réunion est terminée car aucun modérateur n'est présent depuis plus d'une minute.",
"app.meeting.endedByNoModeratorMessagePlural": "La réunion est terminée car aucun modérateur n'est présent depuis plus de {0} minutes.",
"app.meeting.endedMessage": "Vous serez redirigé vers l'écran d'accueil",
"app.meeting.alertMeetingEndsUnderMinutesSingular": "La réunion se terminera dans une minute.",
@ -199,15 +199,15 @@
"app.presentation.hide": "Masquer la présentation",
"app.presentation.notificationLabel": "Présentation courante",
"app.presentation.downloadLabel": "Télécharger",
"app.presentation.slideContent": "Contenu de la diapositive",
"app.presentation.startSlideContent": "Début du contenu de la diapositive",
"app.presentation.endSlideContent": "Fin du contenu de la diapositive",
"app.presentation.changedSlideContent": "La présentation est désormais à la diapositive : {0}",
"app.presentation.emptySlideContent": "Pas de contenu pour la diapositive actuelle",
"app.presentation.slideContent": "Diaporama",
"app.presentation.startSlideContent": "Le diaporama démarre",
"app.presentation.endSlideContent": "Fin du diaporama",
"app.presentation.changedSlideContent": "La présentation est désormais un diaporama : {0}",
"app.presentation.emptySlideContent": "La diapositive courante n'a pas de contenu",
"app.presentation.options.fullscreen": "Présentation en plein écran",
"app.presentation.options.exitFullscreen": "Sortir du plein écran",
"app.presentation.options.minimize": "Réduire",
"app.presentation.options.snapshot": "Capture de la présentation courante",
"app.presentation.options.snapshot": "Capture de la diapositive courante",
"app.presentation.options.downloading": "Téléchargement en cours...",
"app.presentation.options.downloaded": "La présentation courante a été téléchargée",
"app.presentation.options.downloadFailed": "Impossible de télécharger la présentation courante",
@ -215,24 +215,24 @@
"app.presentation.presentationToolbar.noPrevSlideDesc": "Début de présentation",
"app.presentation.presentationToolbar.selectLabel": "Sélectionnez la diapositive",
"app.presentation.presentationToolbar.prevSlideLabel": "Diapositive précédente",
"app.presentation.presentationToolbar.prevSlideDesc": "Changer la présentation à la diapositive précédente",
"app.presentation.presentationToolbar.prevSlideDesc": "Revenir à la diapositive précédente",
"app.presentation.presentationToolbar.nextSlideLabel": "Diapositive suivante",
"app.presentation.presentationToolbar.nextSlideDesc": "Changer la présentation à la diapositive suivante",
"app.presentation.presentationToolbar.skipSlideLabel": "Passer la diapositive",
"app.presentation.presentationToolbar.skipSlideDesc": "Changer la présentation à une diapositive spécifique",
"app.presentation.presentationToolbar.fitWidthLabel": "Adapté à la largeur",
"app.presentation.presentationToolbar.fitWidthDesc": "Afficher toute la largeur de la diapositive",
"app.presentation.presentationToolbar.nextSlideDesc": "Passer à la diapositive suivante",
"app.presentation.presentationToolbar.skipSlideLabel": "Sauter la diapositive",
"app.presentation.presentationToolbar.skipSlideDesc": "Passer à une diapositive spécifique",
"app.presentation.presentationToolbar.fitWidthLabel": "Ajuster à la largeur",
"app.presentation.presentationToolbar.fitWidthDesc": "Afficher la diapositive sur toute sa largeur",
"app.presentation.presentationToolbar.fitScreenLabel": "Ajuster à l'écran",
"app.presentation.presentationToolbar.fitScreenDesc": "Afficher toute la diapositive",
"app.presentation.presentationToolbar.zoomLabel": "Zoom",
"app.presentation.presentationToolbar.zoomDesc": "Changer le niveau de zoom de la présentation",
"app.presentation.presentationToolbar.zoomDesc": "Changer le niveau de zoom dans la présentation",
"app.presentation.presentationToolbar.zoomInLabel": "Zoomer",
"app.presentation.presentationToolbar.zoomInDesc": "Zoomer dans la présentation",
"app.presentation.presentationToolbar.zoomOutLabel": "Dézoomer",
"app.presentation.presentationToolbar.zoomOutDesc": "Dézoomer de la présentation",
"app.presentation.presentationToolbar.zoomReset": "Réinitialiser le zoom",
"app.presentation.presentationToolbar.zoomIndicator": "Pourcentage de zoom actuel",
"app.presentation.presentationToolbar.fitToWidth": "Adapter à la largeur",
"app.presentation.presentationToolbar.fitToWidth": "Ajuster à la largeur",
"app.presentation.presentationToolbar.fitToPage": "Ajuster à la page",
"app.presentation.presentationToolbar.goToSlide": "Diapositive {0}",
"app.presentation.placeholder": "Il n'y a pas de présentation active actuellement",
@ -252,7 +252,7 @@
"app.presentationUploader.export.notAccessibleWarning": "Probablement non conforme aux règles d'accessibilité",
"app.presentationUploader.currentPresentationLabel": "Présentation courante",
"app.presentationUploder.extraHint": "IMPORTANT: Le volume d'un fichier ne doit pas excéder {0} MB et {1} pages.",
"app.presentationUploder.uploadLabel": "Télécharger",
"app.presentationUploder.uploadLabel": "Téléverser",
"app.presentationUploder.confirmLabel": "Confirmer",
"app.presentationUploder.confirmDesc": "Enregistrez vos modifications et lancez la présentation",
"app.presentationUploder.dismissLabel": "Annuler",
@ -272,19 +272,19 @@
"app.presentationUploder.upload.413": "Le fichier est trop volumineux, il dépasse le maximum de {0} Mo",
"app.presentationUploder.genericError": "Oups, quelque chose s'est mal passé...",
"app.presentationUploder.upload.408": "Le jeton de demande de téléversement a expiré.",
"app.presentationUploder.upload.404": "404 : jeton de téléversement invalide",
"app.presentationUploder.upload.404": "404 : jeton de téléversement non valide",
"app.presentationUploder.upload.401": "La demande d'un jeton de téléversement de présentation a échoué.",
"app.presentationUploder.conversion.conversionProcessingSlides": "Traitement de la page {0} sur {1}",
"app.presentationUploder.conversion.genericConversionStatus": "Conversion du fichier en cours...",
"app.presentationUploder.conversion.generatingThumbnail": "Création des vignettes en cours...",
"app.presentationUploder.conversion.generatedSlides": "Diapositives générées...",
"app.presentationUploder.conversion.generatingSvg": "Création des images SVG en cours...",
"app.presentationUploder.conversion.generatingSvg": "Création d'images SVG en cours...",
"app.presentationUploder.conversion.pageCountExceeded": "Le nombre de pages dépasse le maximum de {0}",
"app.presentationUploder.conversion.invalidMimeType": "Mauvais format du fichier détecté (extension={0}, type de contenu={1})",
"app.presentationUploder.conversion.conversionTimeout": "La diapositive {0} n'a pas pu être traitée au cours des {1} essais",
"app.presentationUploder.conversion.officeDocConversionInvalid": "Échec du traitement du document Office. Veuillez télécharger un PDF à la place.",
"app.presentationUploder.conversion.officeDocConversionInvalid": "Échec du traitement du document Office. Veuillez téléverser un PDF à la place.",
"app.presentationUploder.conversion.officeDocConversionFailed": "Échec du traitement du document office. Veuillez télécharger un PDF à la place.",
"app.presentationUploder.conversion.pdfHasBigPage": "Nous n'avons pas pu convertir le fichier PDF, veuillez essayer de l'optimiser. Taille de page maximum {0}",
"app.presentationUploder.conversion.pdfHasBigPage": "Nous n'avons pas pu convertir le fichier PDF, veuillez essayer de l'optimiser. Taille maximum {0}",
"app.presentationUploder.conversion.timeout": "Oups, la conversion a pris trop de temps",
"app.presentationUploder.conversion.pageCountFailed": "Impossible de déterminer le nombre de pages.",
"app.presentationUploder.conversion.unsupportedDocument": "Extension de fichier non prise en charge",
@ -293,9 +293,9 @@
"app.presentationUploder.tableHeading.filename": "Nom de fichier",
"app.presentationUploder.tableHeading.options": "Options",
"app.presentationUploder.tableHeading.status": "Statut",
"app.presentationUploder.uploading": "Charger {0} {1}",
"app.presentationUploder.uploadStatus": "{0} sur {1} chargements terminés",
"app.presentationUploder.completed": "{0} chargements terminés",
"app.presentationUploder.uploading": "Téléversement en cours {0} {1}",
"app.presentationUploder.uploadStatus": "{0} sur {1} téléversements terminés",
"app.presentationUploder.completed": "{0} téléversements terminés",
"app.presentationUploder.item" : "élément",
"app.presentationUploder.itemPlural" : "éléments",
"app.presentationUploder.clearErrors": "Effacer les erreurs",
@ -306,7 +306,7 @@
"app.poll.customInputInstructions.label": "Saisie personnalisée est activée - Ecrivez la question et la (les) réponse(s) dans le format proposé ou glissez-déposez un fichier texte dans le même format. ",
"app.poll.maxOptionsWarning.label": "Seules le 5 premières réponses peuvent être utilisées!",
"app.poll.pollPaneTitle": "Sondage",
"app.poll.enableMultipleResponseLabel": "Permettre aux participants de répondre plusieurs fois ?",
"app.poll.enableMultipleResponseLabel": "Question à choix multiple ?",
"app.poll.quickPollTitle": "Sondage rapide",
"app.poll.hidePollDesc": "Masque le volet du menu du sondage",
"app.poll.quickPollInstruction": "Sélectionnez une option ci-dessous pour démarrer votre sondage.",
@ -370,7 +370,7 @@
"app.polling.submitLabel": "Soumettre",
"app.polling.submitAriaLabel": "Soumettre votre réponse au sondage",
"app.polling.responsePlaceholder": "Entrez votre réponse",
"app.polling.responseSecret": "Sondage anonyme - le présentateur ne peut pas voir votre réponse.",
"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}",

View File

@ -189,8 +189,8 @@
"app.meeting.meetingTimeRemaining": "Հանդիպման ավարտին մնացել է {0}",
"app.meeting.meetingTimeHasEnded": "Ժամանակը սպառվեց և հանդիպումը շուտով կավարտվի",
"app.meeting.endedByUserMessage": "Այս հանդիպումն ավարտվել է {0}",
"app.meeting.endedByNoModeratorMessageSingular": "Հանդիպումն ավարտվել է մեկ րոպեից ավելի մոդերատորի բացակայության պատճառով",
"app.meeting.endedByNoModeratorMessagePlural": "Հանդիպումն ավարտվել է {0} րոպեից ավելի մոդերատորի բացակայության պատճառով",
"app.meeting.endedByNoModeratorMessageSingular": "Հանդիպումն ավարտվեց, քանի որ մոդերատորը բացակայում է",
"app.meeting.endedByNoModeratorMessagePlural": "Հանդիպումն ավարտվեց, քանի որ մոդերատորը բացակայում է {0}  րոպե",
"app.meeting.endedMessage": "Դուք կուղղորդվեք դեպի գլխավոր էջ",
"app.meeting.alertMeetingEndsUnderMinutesSingular": "Հանդիպումը կավարտվի մեկ րոպեից",
"app.meeting.alertMeetingEndsUnderMinutesPlural": "Հանդիպումը կավարտվի {0} րոպեից",
@ -566,7 +566,7 @@
"app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "{0}+ խոսում էին",
"app.talkingIndicator.wasTalking" : "{0}ը դադարեց խոսել",
"app.actionsBar.actionsDropdown.actionsLabel": "Գործողություններ",
"app.actionsBar.actionsDropdown.presentationLabel": "Ներկայացումների կառավարում",
"app.actionsBar.actionsDropdown.presentationLabel": "Բեռնել/Կառավարել ներկայացումները",
"app.actionsBar.actionsDropdown.initPollLabel": "Սկսել հարցումը",
"app.actionsBar.actionsDropdown.desktopShareLabel": "Ցուցադրել Ձեր էկրանը",
"app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Ընդհատել էկրանի ցուցադրումը",

View File

@ -0,0 +1,122 @@
# GNS3-BBB
Scripts to build a virtual BigBlueButton network in a gns3 project (for testing purposes).
**Prerequisites:** a Ubuntu server with KVM (Kernel Virtual Machine) support and enough CPU and RAM to support the virtual machines in the virtual network
**Note**: gns3 is very picky about matching GUI client and server versions. I typically put dpkg holds on the gns3 packages, since otherwise an apt upgrade on my laptop requires both an apt upgrade on my gns3 server *and* restarting the gns3 server, which implies stopping and restarting all of the running VMs.
**Note**: gns3 uses qemu, which can not run concurrently with VirtualBox. If VirtualBox virtual machines are running. gns3 virtual machines will not start, and vice versa.
**Note:** Once the script has been used to build the virtual network (takes about an hour), the virtual network can be stopped and restarted without having to re-run the script.
## Design
The script will build a gns3 project that looks like this:
![network diagram](README.png)
The network "highjacks" the 128.8.8.0/24 subnet, so it simulates public IP address space. You can set a different public subnet using the `--public-subnet` option to the script.
The DNS domain name is configured to match the bare metal hostname. If the bare metal machine is called `osito`, for example, the virtual machines will be given names like `BigBlueButton.osito` and `focal-260.osito`.
The `BigBlueButton` virtual machine (called `master_gateway` in the script) is named to match the gns3 project name, which is `BigBlueButton` by default. The project name (and the name of the master gateway) can be changed using the `--project` option.
The master gateway, in addition to providing DNS and DHCP service for the 128.8.8.0/24 subnet, also operates a STUN server that presents itself in DNS as `stun.l.google.com`, so that STUN operations, on both the BigBlueButton clients and servers, yield the 128.8.8.0/24 addresses as public addresses. `BigBlueButton` also operates an ACME CA signing service (so that `certbot` works), and mimics `resolver1.opendns.com` (used by `bbb-install` to check that the server can reach itself).
The master gateway also announces the 128.8.8.0/24 subnet to the bare metal machine using OSPF, and implements NAT, so that the bare metal machine can connect to the virtual servers.
The `focal-260-NAT` device announces itself into DHCP/DNS as `focal-260.DOMAIN` and forwards ports 80 and 443 (along with UDP ports) through to `focal-260` itself. Clients can therefore connect to `focal-260.DOMAIN`, just as they would to a typical BBB server. The NAT device itself listens for ssh on port 2222. The `--no-nat` option can be specified to create a server without an associated NAT gateway.
Default operation of the script is to install a server whose name is passed into the script and is used both as the hostname of the server and as the release name to install. Obvious server names include `focal-250`, `focal-25-dev`, and `focal-260`. You can specify the `-r`/`--repository` option to use a repository other than `ubuntu.bigbluebutton.org` (just like the install script). The `--install-script` option allows an alternate install script to be used.
Some special names are defined. Requesting a device name starting with `testclient` creates a test client that connects to NAT4 (overlapping server address space), NAT5 (private address not overlapping server address space), and NAT6 (carrier grade NAT). Likewise, `turn` and `natturn` devices can also be created, just by requesting them by name.
## 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:
```
git submodule init
git submodule update
```
1. Read, understand, and run the `install-gns3.sh` script in `NPDC/GNS3`
1. Upload a current Ubuntu 20 cloud image to the gns3 server using NPDC's `GNS3/upload-image.py`:
`./upload-image.py https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.img`
The most uncommon Python3 package that this script uses is `python3-requests-toolbelt`. `python3-clint` is also recommended, to get a progress bar.
If this step works, then you have REST API access to the GNS3 server.
1. You should now be able to boot an Ubuntu instance with this `NPDC/GNS3` script:
`./ubuntu.py -r 20 -m 1024 --debug`
Double-click on the icon that appears in the GUI to access the instance's console. You should also be able to login using `ssh ubuntu`.
The `--debug` option adds a login with username `ubuntu` and password `ubuntu`.
Login and verify, in particular, that networking is working properly. You should have Internet access.
1. Finally, build the BigBlueButton project in gns3 with `./gns3-bbb.py`
1. Install a server with `./gns3-bbb.py --wait-all focal-260`
The `--wait-all` option will cause the script to wait for BigBlueButton to install while you watch. Without this option, the script will pause to wait for the NAT device to boot before starting the BigBlueButton server, then terminate once the BigBlueButton server has begun its install sequence.
1. You can run tests directly from the bare metal machine. The script created an SSL certificate in its own directory called `bbb-dev-ca.crt` which can be installed and trusted on your web browser.
1. Add another server with `./gns3-bbb.py focal-250`
1. Remove a server and its associated NAT gateway and switch with `./gns3-bbb.py --delete focal-250`
1. `ssh` into the server devices directly.
1. You can `ssh` into a server's NAT gateway with `ssh -p 2222 focal-260`.
1. Since test servers come and go fairly frequently, I find the following stanza useful in my `.ssh/config`:
```
Host BigBlueButton NAT? testclient* focal-*
User ubuntu
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
```
This stops `ssh` from complaining about server host keys changing, which happens every time you delete and rebuild a server.
### Installing test clients
1. Build a GUI image using NPDC's `GNS3/ubuntu.py`:
`./ubuntu.py -r 20 -s $((1024*1024)) -m 1024 --boot-script opendesktop.sh --gns3-appliance`
This step adds the GUI packages to the Ubuntu 20 cloud image and creates a new cloud image used for the test clients. It takes about half an hour.
1. Upload the resulting GUI image to the gns3 server using NPDC's `GNS3/upload-image.py`
1. Add a test client with `./gns3-bbb.py testclient`
1. You can access a testclient's GUI by double-clicking on its icon in the GNS3 GUI.
1. You can `ssh` into a testclient by specifying its NAT gateway as a jump host (`-J`) option to ssh: `ssh -J NAT4 testclient`
### Possible Test Environments
1. UDP ports can be blocked, forcing use of TURN, like this (blocks all servers from bare metal clients):
```
$ ssh BigBlueButton
ubuntu@BigBlueButton:~$ sudo iptables -A FORWARD -p udp -j REJECT
```
or like this (blocks one server from all clients):
```
$ ssh -p 2222 focal-260
ubuntu@focal-260-NAT:~$ sudo iptables -A FORWARD -p udp -j REJECT
```
2. Install a proxy on BigBlueButton and force its use by blocking TCP traffic? (bbb-install issue #583)

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -0,0 +1,25 @@
# This is a minimalist bird configuration that causes the psuedo-Internet
# subnet on ens5 to be announced via OSPF on ens4. This allows the bare
# metal system to pick up a route to the psuedo-Internet subnet. In
# particular, RTP audio and video to proxied servers won't work without
# this, because the server and client are far enough removed (two NAT
# gateways between them) that they don't have any shared address space.
# By advertising the psuedo-Internet 128.8.8.0/24 to the client, that
# ensures that the client can reach one of the addresses advertised
# by the server.
# The Device protocol is not a real routing protocol. It doesn't generate any
# routes and it only serves as a module for getting information about network
# interfaces from the kernel.
protocol device { }
protocol ospf OSPF {
area 0 {
interface "ens*" {
cost 10;
};
};
import none;
export all;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
[Unit]
Description=step-ca
After=syslog.target network.target
[Service]
ExecStart=/usr/bin/step-ca /opt/ca/ca.json
Type=simple
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,95 @@
#!/bin/bash
#
# Install Big Blue Button testing client
#
# This script runs once as 'ubuntu'
# Make some changes to .bashrc, but they won't affect the shell that is already
# running in the GUI, so the user will need to '. ~/.bashrc' there.
#
# We need NODE_EXTRA_CA_CERTS so that the playwright tests can make API calls
# without getting certificate errors.
cat >> ~/.bashrc <<EOF
export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt
export ACTIONS_RUNNER_DEBUG=true
export BBB_URL=https://bbb-ci.test/bigbluebutton/api
export BBB_SECRET=bbbci
EOF
# Which version of the repository should we use for the client test cases
BRANCH=v2.5.x-release
# if these are running, our apt operations may error out unable to get a lock
sudo systemctl stop unattended-upgrades.service
echo Waiting for apt-daily.service and apt-daily-upgrade.service
sudo systemd-run --property="After=apt-daily.service apt-daily-upgrade.service" --wait /bin/true
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt -y upgrade
# git, since we're about to use it
# linux-image-extra-virtual to get snd-aloop module for dummy audio
# v4l2loopback-dkms to get video loopback for dummy webcam
# sudo apt -y install git-core ant ant-contrib openjdk-8-jdk-headless zip unzip linux-image-extra-virtual
sudo apt -y install git-core linux-image-extra-virtual v4l2loopback-dkms
# We don't need the whole git history, like this command would do:
# git clone https://github.com/bigbluebutton/bigbluebutton.git
# so instead we do this to pick up a single revision:
cd
mkdir bigbluebutton-$BRANCH
cd bigbluebutton-$BRANCH
git init
git remote add origin https://github.com/bigbluebutton/bigbluebutton.git
git fetch --depth 1 origin $BRANCH
git checkout FETCH_HEAD
# Focal distributes nodejs 10, which is too old for our playwright test suite. Use nodejs 16.
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs
cd ~/bigbluebutton-$BRANCH/bigbluebutton-tests/playwright
npm install
npx --yes playwright install
# patch playwright's firefox so that it uses the system's root certificate authority
find /home/ubuntu/.cache/ms-playwright -name libnssckbi.so -exec mv {} {}.distrib \; -exec ln -s /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so {} \;
# playwright webkit doesn't have a fake audio device, but Linux does
# no point in enabling this since playwright can't grant permissions to use microphone on webkit (playwright issue #2973)
# sudo modprobe snd-aloop
# echo snd-aloop | sudo tee -a /etc/modules
# this is required to run webkit tests, but conflicts with BBB server dependencies,
# so can't be installed on the same machine as a BBB server
sudo npx playwright install-deps
# still need to either install an .env file or set environment variables in ~/.bashrc
# In addition to the system root CA store in /usr/local/share/ca-certificates (used by curl and others),
# we need to deal with two common browsers that don't use the system store.
# Get Firefox (already installed) to use system's root certificate authority
# Method suggested by https://askubuntu.com/a/1036637/71866
# Earlier this this script, we did something similar to modify playwright's version of firefox.
# This handles the standard system firefox.
sudo mv /usr/lib/firefox/libnssckbi.so /usr/lib/firefox/libnssckbi.so.distrib
sudo dpkg-divert --no-rename --add /usr/lib/firefox/libnssckbi.so
sudo ln -s /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox/libnssckbi.so
# Install chromium and the tools we need to install our certificate into Chromium's private store
sudo DEBIAN_FRONTEND=noninteractive apt -y install chromium-browser libnss3-tools jq
# chromium snap - we now need to install nssdb in ~/snap/chromium/2051/.pki instead of ~/.pki
# NSSDB=/home/ubuntu/.pki/nssdb
for CHROMIUM_SNAP in $(find /home/ubuntu/snap/chromium/ -mindepth 1 -maxdepth 1 -type d); do
NSSDB=$CHROMIUM_SNAP/.pki/nssdb
if [ ! -r $NSSDB ]; then
mkdir --parents $NSSDB
certutil -d sql:$NSSDB -N --empty-password
fi
certutil -d sql:$NSSDB -A -t 'C,,' -n bbb-dev-ca -i /usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt
done

View File

@ -0,0 +1,7 @@
#!/bin/bash
SECRET=$(grep sharedSecret /etc/bigbluebutton/bbb-apps-akka.conf | sed 's/^.*=//')
URL=$(grep bigbluebutton.web.serverURL= /etc/bigbluebutton/bbb-web.properties | sed 's/^.*=//')
echo BBB_URL="$URL/bigbluebutton/api"
echo BBB_SECRET=$SECRET

View File

@ -0,0 +1,43 @@
#!/bin/bash
#
# Install a Big Blue Button testing server on a VM
# if these are running, our apt operations may error out unable to get a lock
sudo systemctl stop unattended-upgrades.service
echo Waiting for apt-daily.service and apt-daily-upgrade.service
sudo systemd-run --property="After=apt-daily.service apt-daily-upgrade.service" --wait /bin/true
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt -y upgrade
DOMAIN=$(hostname --domain)
FQDN=$(hostname --fqdn)
EMAIL="root@$FQDN"
# /bbb-install.sh (the proper version; either 2.4, 2.5 or 2.6) is created by gns3-bbb.py
# INSTALL_OPTIONS and RELEASE get passed in the environment from gns3-bbb.py
#
# INSTALL_OPTIONS can include -w (firewall) -a (api demos; deprecated in 2.6) -r (repository)
sudo /bbb-install.sh -v $RELEASE -s $FQDN -e $EMAIL $INSTALL_OPTIONS
sudo bbb-conf --salt bbbci
echo "NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt" | sudo tee -a /usr/share/meteor/bundle/bbb-html5-with-roles.conf
# bbb-conf --salt doesn't set the shared secret on the web demo
if [ -r /var/lib/tomcat9/webapps/demo/bbb_api_conf.jsp ]; then
sudo sed -i '/salt/s/"[^"]*"/"bbbci"/' /var/lib/tomcat9/webapps/demo/bbb_api_conf.jsp
fi
# if nginx didn't start because of a hash bucket size issue,
# certbot didn't work properly and we need to re-run the entire install script
if systemctl -q is-failed nginx; then
sudo sed -i '/server_names_hash_bucket_size/s/^\(\s*\)# /\1/' /etc/nginx/nginx.conf
sudo /bbb-install.sh -v $RELEASE -s $FQDN -e $EMAIL $INSTALL_OPTIONS
fi
# We can't restart if nginx isn't running. It'll just complain "nginx.service is not active, cannot reload"
# sudo bbb-conf --restart
sudo bbb-conf --stop
sudo bbb-conf --start

View File

@ -0,0 +1,51 @@
const { test, devices } = require('@playwright/test');
const { ScreenShare, MultiUserScreenShare } = require('../screenshare/screenshare');
const { sleep } = require('../core/helpers');
const e = require('../core/elements');
const { getCurrentTCPSessions, killTCPSessions } = require('./util');
const notificationsUtil = require('../notifications/util');
const deepEqual = require('deep-equal');
test.describe.parallel('Connection failure', () => {
// https://docs.bigbluebutton.org/2.6/release-tests.html#sharing-screen-in-full-screen-mode-automated
test('Screen sharer', async ({ browser, browserName, page }) => {
test.skip(browserName === 'firefox' && process.env.DISPLAY === undefined,
"Screenshare tests not able in Firefox browser without desktop");
const screenshare = new ScreenShare(browser, page);
await screenshare.init(true, true);
await screenshare.startSharing();
await killTCPSessions(await getCurrentTCPSessions());
// I'd like to do this:
// await notificationsUtil.checkNotificationText(screenshare, "Code 1101. Try sharing the screen again.");
// but that code only checks the first toast, and there's a bunch of toasts that show up
// on this test. So instead I use xpath, like this: (should we do it this way in checkNotificationText?)
await screenshare.hasElement('//div[@data-test="toastSmallMsg"]/span[contains(text(), "Code 1101. Try sharing the screen again.")]');
});
test('Screen share viewer', async ({ browser, browserName, page, context }) => {
test.skip(browserName === 'firefox' && process.env.DISPLAY === undefined,
"Screenshare tests not able in Firefox browser without desktop");
const screenshare = new MultiUserScreenShare(browser, context);
await screenshare.initModPage(page);
await screenshare.startSharing(screenshare.modPage);
const tcpModeratorSessions = await getCurrentTCPSessions();
await screenshare.initUserPage(false);
await screenshare.userPage.joinMicrophone();
await screenshare.userPage.hasElement(e.screenShareVideo);
const tcpSessions = await getCurrentTCPSessions();
// Other comparisons, like == or the array includes method, don't do a deep comparison
// and will always return false since the two arrays contain different objects.
const tcpUserSessions = tcpSessions.filter(x => !tcpModeratorSessions.some(e => deepEqual(e,x)));
killTCPSessions(tcpUserSessions);
await screenshare.userPage.hasElement('//div[@data-test="notificationBannerBar" and contains(text(), "Connecting ...")]');
await screenshare.userPage.wasRemoved('//div[@data-test="notificationBannerBar" and contains(text(), "Connecting ...")]');
});
});

View File

@ -0,0 +1,57 @@
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
const process = require('node:process');
const parameters = require('../core/parameters.js');
// Hostname of BBB server that we're going to break connections to
const hostname = new URL(parameters.server).hostname;
// Parse a line of output from the 'ss' program into an object
// with fields local (local IP address and TCP port), remote
// (remote address and port) and pid (localhost process ID)
function parseline(line) {
const fields = line.split(/\s+/);
// These two are like '192.168.8.1:59164'
const local = fields[3].split(/:/);
const remote = fields[4].split(/:/);
// This one is like 'users:(("chrome",pid=3346964,fd=118))'
const process = fields[5];
const pid = parseInt(process.match(/pid=([0-9]+),/)[1]);
return { local: { ip: local[0], port: parseInt(local[1]) },
remote: { ip: remote[0], port: parseInt(remote[1]) },
pid: pid };
}
// return an array of such structures for the current process
// and all of its subprocesses that connect to a given host
async function getCurrentTCPSessions() {
// First, get the process IDs of all of our subprocesses, which include the test browser(s).
// The process IDs will appear in parenthesis after the command names in stdout.
const { stdout } = await exec("pstree -pn " + process.pid);
const processIDs = stdout.matchAll(/\(([0-9]+)\)/g);
// Next, form a regular expression that matches all of those process IDs in ss's output format.
//
// The string matchAll method returned an iterator, which doesn't have a map method,
// so we convert the iterator to a list, then map it to a expression which matches the pid
// field in the output of ss, then join all the expressions together to form a regex.
const foundRE = new RegExp([...processIDs].map(x => 'pid=' + x[1]).join('|'));
// Now get all TCP sessions going to the target host
const { stdout: stdout2 } = await exec("ss -tpnH dst " + hostname);
// Extract those TCP sessions attached to our subprocesses, parse and and return them in an array.
return stdout2.split('\n').filter(x => foundRE.test(x)).map(x => parseline(x));
}
// takes an array of such structures and kills those TCP sessions
async function killTCPSessions(sessions) {
await exec('sudo ss -K dst ' + hostname + ' ' + sessions.map(x => 'sport = ' + x.local.port).join(' or '));
}
exports.getCurrentTCPSessions = getCurrentTCPSessions;
exports.killTCPSessions = killTCPSessions;

View File

@ -21,3 +21,4 @@ exports.VIDEO_LOADING_WAIT_TIME = 15000;
exports.UPLOAD_PDF_WAIT_TIME = CI ? 40000 : 20000;
exports.CUSTOM_MEETING_ID = 'custom-meeting';
exports.PARAMETER_HIDE_PRESENTATION_TOAST = 'userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}';

View File

@ -425,6 +425,12 @@ exports.multiUsersWhiteboardOff = 'button[data-test="turnMultiUsersWhiteboardOff
exports.whiteboardViewBox = 'svg g[clip-path="url(#viewBox)"]';
exports.changeWhiteboardAccess = 'li[data-test="changeWhiteboardAccess"]';
exports.pencil = 'button[data-test="pencilTool"]';
exports.resetZoomButton = 'button[data-test="resetZoomButton"]';
exports.zoomInButton = 'button[data-test="zoomInBtn"]';
exports.zoomOutButton = 'button[data-test="zoomOutBtn"]';
exports.wbPan = 'button[data-test="panButton"]';
exports.wbEraser = 'button[id="TD-PrimaryTools-Eraser"]';
exports.wbArrowShape = 'button[id="TD-PrimaryTools-ArrowTopRight"]';
// About modal
exports.showAboutModalButton = 'li[data-test="aboutModal"]';

View File

@ -27,6 +27,16 @@ async function checkTextContent(baseContent, checkData) {
await expect(check).toBeTruthy();
}
function constructClipObj(wbBox) {
return {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
}
exports.checkElement = checkElement;
exports.checkElementLengthEqualTo = checkElementLengthEqualTo;
exports.checkTextContent = checkTextContent;
exports.constructClipObj = constructClipObj;

View File

@ -3,21 +3,23 @@ const { FocusOnPresentation } = require('./focusOnPresentation');
const { FocusOnVideo } = require('./focusOnVideo');
const { MultiUsers } = require('../user/multiusers');
const { encodeCustomParams } = require('../customparameters/util');
const { PARAMETER_HIDE_PRESENTATION_TOAST } = require('../core/constants');
const hidePresentationToast = encodeCustomParams(PARAMETER_HIDE_PRESENTATION_TOAST);
const CUSTOM_MEETING_ID = 'layout_management_meeting';
const CUSTOM_STYLE = `userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`;
test.describe.parallel('Layout management', () => {
test('Focus on presentation', async ({ browser, context, page }) => {
const focusOnPresentation = new FocusOnPresentation(browser, context);
await focusOnPresentation.initModPage(page, true, { customMeetingId: CUSTOM_MEETING_ID, customParameter: encodeCustomParams(CUSTOM_STYLE) });
await focusOnPresentation.initModPage2(true, context, { customParameter: encodeCustomParams(CUSTOM_STYLE) });
await focusOnPresentation.initModPage(page, true, { customMeetingId: CUSTOM_MEETING_ID, customParameter: hidePresentationToast });
await focusOnPresentation.initModPage2(true, context, { customParameter: hidePresentationToast });
await focusOnPresentation.test();
});
test('Focus on video', async ({ browser, context, page }) => {
const focusOnVideo = new FocusOnVideo(browser, context);
await focusOnVideo.initModPage(page, true, { customMeetingId: CUSTOM_MEETING_ID, customParameter: encodeCustomParams(CUSTOM_STYLE) });
await focusOnVideo.initModPage2(true, context, { customParameter: encodeCustomParams(CUSTOM_STYLE) });
await focusOnVideo.initModPage(page, true, { customMeetingId: CUSTOM_MEETING_ID, customParameter: hidePresentationToast });
await focusOnVideo.initModPage2(true, context, { customParameter: hidePresentationToast });
await focusOnVideo.test();
});
});

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,10 @@
"dependencies": {
"@playwright/test": "^1.28.1",
"axios": "^1.2.0",
"chalk": "^4.1.2",
"deep-equal": "^2.2.0",
"dotenv": "^16.0.0",
"playwright": "^1.28.1",
"chalk": "^4.1.2",
"sha1": "^1.1.1",
"xml2js": "^0.4.23"
}

View File

@ -54,7 +54,8 @@ class Polling extends MultiUsers {
await this.modPage.waitAndClick(e.publishPollingLabel);
await this.modPage.waitForSelector(e.restartPoll);
await this.modPage.hasElement(e.wbTypedText);
await this.modPage.hasElement(e.wbDrawnRectangle);
await this.userPage.hasElement(e.wbDrawnRectangle);
}
async stopPoll() {
@ -80,7 +81,7 @@ class Polling extends MultiUsers {
async pollResultsOnWhiteboard() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await util.startPoll(this.modPage, true);
await this.modPage.hasElement(e.wbTypedText);
await this.modPage.hasElement(e.wbDrawnRectangle);
}
async pollResultsInDifferentPresentation() {
@ -91,7 +92,7 @@ class Polling extends MultiUsers {
await this.modPage.waitAndClick(e.publishPollingLabel);
// Check poll results
await this.modPage.hasElement(e.wbTypedText);
await this.modPage.hasElement(e.wbDrawnRectangle);
}
async manageResponseChoices() {

View File

@ -1,5 +1,6 @@
const { default: test } = require('@playwright/test');
const Page = require('../core/page');
const { MultiUsers } = require('../user/multiusers');
const { startScreenshare } = require('./util');
const e = require('../core/elements');
const { getSettings } = require('../core/settings');
@ -21,4 +22,18 @@ class ScreenShare extends Page {
}
}
class MultiUserScreenShare extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async startSharing(page) {
const { screensharingEnabled } = getSettings();
test.fail(!screensharingEnabled, 'Screensharing is disabled');
await startScreenshare(page);
await page.hasElement(e.isSharingScreen);
}
}
exports.ScreenShare = ScreenShare;
exports.MultiUserScreenShare = MultiUserScreenShare;

View File

@ -0,0 +1,35 @@
const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
class DrawArrow extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbArrowShape);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
await expect(this.modPage.page).toHaveScreenshot('moderator1-arrow.png', screenshotOptions);
await expect(this.modPage2.page).toHaveScreenshot('moderator2-arrow.png', screenshotOptions);
}
}
exports.DrawArrow = DrawArrow;

View File

@ -2,6 +2,7 @@ const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
class DrawEllipse extends MultiUsers {
constructor(browser, context) {
@ -10,31 +11,25 @@ class DrawEllipse extends MultiUsers {
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbShapesButton);
await this.modPage.waitAndClick(e.wbEllipseShape);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-ellipse.png', screenshotOptions);
await expect(this.modPage.page).toHaveScreenshot('moderator1-ellipse.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-ellipse.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-ellipse.png', screenshotOptions);
}
}

View File

@ -2,6 +2,7 @@ const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
class DrawLine extends MultiUsers {
constructor(browser, context) {
@ -10,31 +11,25 @@ class DrawLine extends MultiUsers {
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbShapesButton);
await this.modPage.waitAndClick(e.wbLineShape);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-line.png', screenshotOptions);
await expect(this.modPage.page).toHaveScreenshot('moderator1-line.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-line.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-line.png', screenshotOptions);
}
}

View File

@ -2,6 +2,7 @@ const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
class DrawPencil extends MultiUsers {
constructor(browser, context) {
@ -10,9 +11,16 @@ class DrawPencil extends MultiUsers {
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.waitAndClick(e.wbPencilShape);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbPencilShape);
const moveOptions = { steps: 50 }; // to slow down
await this.modPage.page.mouse.move(wbBox.x + 0.2 * wbBox.width, wbBox.y + 0.2 * wbBox.height);
await this.modPage.page.mouse.down();
@ -21,22 +29,9 @@ class DrawPencil extends MultiUsers {
await this.modPage.page.mouse.move(wbBox.x + 0.8 * wbBox.width, wbBox.y + 0.4 * wbBox.height, moveOptions);
await this.modPage.page.mouse.up();
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-pencil.png', screenshotOptions);
await expect(this.modPage.page).toHaveScreenshot('moderator1-pencil.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-pencil.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-pencil.png', screenshotOptions);
}
}

View File

@ -2,6 +2,7 @@ const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
class DrawRectangle extends MultiUsers {
constructor(browser, context) {
@ -10,31 +11,25 @@ class DrawRectangle extends MultiUsers {
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbShapesButton);
await this.modPage.waitAndClick(e.wbRectangleShape);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-rectangle.png', screenshotOptions);
await expect(this.modPage.page).toHaveScreenshot('moderator1-rectangle.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-rectangle.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-rectangle.png', screenshotOptions);
}
}

View File

@ -2,6 +2,7 @@ const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
class DrawStickyNote extends MultiUsers {
constructor(browser, context) {
@ -10,9 +11,16 @@ class DrawStickyNote extends MultiUsers {
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.waitAndClick(e.wbStickyNoteShape);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbStickyNoteShape);
await this.modPage.page.mouse.click(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.press('A');
@ -24,21 +32,8 @@ class DrawStickyNote extends MultiUsers {
await this.modPage.hasText(e.wbTypedStickyNote, 'AB');
await this.modPage2.hasText(e.wbTypedStickyNote, 'AB');
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-sticky.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-sticky.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage.page).toHaveScreenshot('moderator1-sticky.png', screenshotOptions);
await expect(this.modPage2.page).toHaveScreenshot('moderator2-sticky.png', screenshotOptions);
}
}

View File

@ -2,6 +2,7 @@ const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
class DrawText extends MultiUsers {
constructor(browser, context) {
@ -10,9 +11,16 @@ class DrawText extends MultiUsers {
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.waitAndClick(e.wbTextShape);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbTextShape);
await this.modPage.page.mouse.click(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.press('A');
@ -21,21 +29,8 @@ class DrawText extends MultiUsers {
await this.modPage.press('B');
await this.modPage.page.mouse.click(wbBox.x + 0.6 * wbBox.width, wbBox.y + 0.6 * wbBox.height);
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-text.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-text.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage.page).toHaveScreenshot('moderator1-text.png', screenshotOptions);
await expect(this.modPage2.page).toHaveScreenshot('moderator2-text.png', screenshotOptions);
}
}

View File

@ -2,6 +2,7 @@ const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
class DrawTriangle extends MultiUsers {
constructor(browser, context) {
@ -10,31 +11,25 @@ class DrawTriangle extends MultiUsers {
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbShapesButton);
await this.modPage.waitAndClick(e.wbTriangleShape);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-triangle.png', screenshotOptions);
await expect(this.modPage.page).toHaveScreenshot('moderator1-triangle.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-triangle.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-triangle.png', screenshotOptions);
}
}

View File

@ -0,0 +1,48 @@
const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
const defaultZoomLevel = '100%';
const zoomedInZoomLevel = '125%';
const maxZoomLevel = '400%';
class Eraser extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
await this.modPage.waitAndClick(e.wbShapesButton);
await this.modPage.waitAndClick(e.wbLineShape);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
await this.modPage.waitAndClick(e.wbEraser);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await expect(this.modPage.page).toHaveScreenshot('moderator1-eraser1.png', screenshotOptions);
await expect(this.modPage.page).toHaveScreenshot('moderator2-eraser1.png', screenshotOptions);
await this.modPage.page.mouse.up();
await expect(this.modPage.page).toHaveScreenshot('moderator1-eraser2.png', screenshotOptions);
await expect(this.modPage2.page).toHaveScreenshot('moderator2-eraser2.png', screenshotOptions);
}
}
exports.Eraser = Eraser;

View File

@ -0,0 +1,40 @@
const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
const defaultZoomLevel = '100%';
const zoomedInZoomLevel = '125%';
const maxZoomLevel = '400%';
class Pan extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async test() {
await this.modPage.waitForSelector(e.resetZoomButton, ELEMENT_WAIT_LONGER_TIME);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
for(let i = 100; i < 200; i += 25) {
await this.modPage.waitAndClick(e.zoomInButton);
}
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
await expect(this.modPage.page).toHaveScreenshot('moderator1-pan.png', screenshotOptions);
await expect(this.modPage2.page).toHaveScreenshot('moderator2-pan.png', screenshotOptions);
}
}
exports.Pan = Pan;

View File

@ -7,8 +7,15 @@ const { DrawLine } = require('./drawLine');
const { DrawPencil } = require('./drawPencil');
const { DrawText } = require('./drawText');
const { DrawStickyNote } = require('./drawStickyNote');
const { Zoom } = require('./zoom');
const { Pan } = require('./pan');
const { Eraser } = require('./eraser');
const { DrawArrow } = require('./drawArrow');
const { MultiUsers } = require('../user/multiusers');
const { encodeCustomParams } = require('../customparameters/util');
const { PARAMETER_HIDE_PRESENTATION_TOAST } = require('../core/constants');
const hidePresentationToast = encodeCustomParams(PARAMETER_HIDE_PRESENTATION_TOAST);
test.describe.parallel('Whiteboard @ci', () => {
test('Draw rectangle', async ({ browser, page }) => {
@ -24,7 +31,7 @@ test.describe.parallel('Whiteboard @ci', () => {
});
});
test.describe.parallel('Drawing - visual regression', () => {
test.describe.parallel('Whiteboard tools - visual regression', () => {
test.beforeEach(({ browserName }) => {
test.skip(browserName !== 'chromium',
'Drawing visual regression tests are enabled for Chromium only');
@ -32,50 +39,78 @@ test.describe.parallel('Drawing - visual regression', () => {
test('Draw rectangle', async ({ browser, context, page }) => {
const drawRectangle = new DrawRectangle(browser, context);
await drawRectangle.initModPage(page, true, { customMeetingId: 'draw_rectangle_meeting', customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawRectangle.initModPage2(true, context, { customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawRectangle.initModPage(page, true, { customMeetingId: 'draw_rectangle_meeting', customParameter: hidePresentationToast });
await drawRectangle.initModPage2(true, context, { customParameter: hidePresentationToast });
await drawRectangle.test();
});
test('Draw ellipse', async ({ browser, context, page }) => {
const drawEllipse = new DrawEllipse(browser, context);
await drawEllipse.initModPage(page, true, { customMeetingId: 'draw_ellipse_meeting', customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawEllipse.initModPage2(true, context, { customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawEllipse.initModPage(page, true, { customMeetingId: 'draw_ellipse_meeting', customParameter: hidePresentationToast });
await drawEllipse.initModPage2(true, context, { customParameter: hidePresentationToast });
await drawEllipse.test();
});
test('Draw triangle', async ({ browser, context, page }) => {
const drawTriangle = new DrawTriangle(browser, context);
await drawTriangle.initModPage(page, true, { customMeetingId: 'draw_triangle_meeting', customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawTriangle.initModPage2(true, context, { customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawTriangle.initModPage(page, true, { customMeetingId: 'draw_triangle_meeting', customParameter: hidePresentationToast });
await drawTriangle.initModPage2(true, context, { customParameter: hidePresentationToast });
await drawTriangle.test();
});
test('Draw line', async ({ browser, context, page }) => {
const drawLine = new DrawLine(browser, context);
await drawLine.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawLine.initModPage2(true, context, { customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawLine.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await drawLine.initModPage2(true, context, { customParameter: hidePresentationToast });
await drawLine.test();
});
test('Draw with pencil', async ({ browser, context, page }) => {
const drawPencil = new DrawPencil(browser, context);
await drawPencil.initModPage(page, true, { customMeetingId: 'draw_pencil_meeting', customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawPencil.initModPage2(true, context, { customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawPencil.initModPage(page, true, { customMeetingId: 'draw_pencil_meeting', customParameter: hidePresentationToast });
await drawPencil.initModPage2(true, context, { customParameter: hidePresentationToast });
await drawPencil.test();
});
test('Type text', async ({ browser, context, page }) => {
const drawText = new DrawText(browser, context);
await drawText.initModPage(page, true, { customMeetingId: 'draw_text_meeting', customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawText.initModPage2(true, context, { customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawText.initModPage(page, true, { customMeetingId: 'draw_text_meeting', customParameter: hidePresentationToast });
await drawText.initModPage2(true, context, { customParameter: hidePresentationToast });
await drawText.test();
});
test('Create sticky note', async ({ browser, context, page }) => {
const drawStickyNote = new DrawStickyNote(browser, context);
await drawStickyNote.initModPage(page, true, { customMeetingId: 'draw_sticky_meeting', customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawStickyNote.initModPage2(true, context, { customParameter: encodeCustomParams(`userdata-bbb_custom_style=.presentationUploaderToast{display: none;}.currentPresentationToast{display:none;}`) });
await drawStickyNote.initModPage(page, true, { customMeetingId: 'draw_sticky_meeting', customParameter: hidePresentationToast });
await drawStickyNote.initModPage2(true, context, { customParameter: hidePresentationToast });
await drawStickyNote.test();
});
test('Zoom', async ({ browser, context, page }) => {
const zoom = new Zoom(browser, context);
await zoom.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await zoom.initModPage2(true, context, { customParameter: hidePresentationToast });
await zoom.test();
});
test('Pan', async ({ browser, context, page }) => {
const pan = new Pan(browser, context);
await pan.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await pan.initModPage2(true, context, { customParameter: hidePresentationToast });
await pan.test();
});
test('Eraser', async ({ browser, context, page }) => {
const eraser = new Eraser(browser, context);
await eraser.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await eraser.initModPage2(true, context, { customParameter: hidePresentationToast });
await eraser.test();
});
test('Draw arrow', async ({ browser, context, page }) => {
const drawArrow = new DrawArrow(browser, context);
await drawArrow.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await drawArrow.initModPage2(true, context, { customParameter: hidePresentationToast });
await drawArrow.test();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,49 @@
const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { constructClipObj } = require('../core/util');
const defaultZoomLevel = '100%';
const zoomedInZoomLevel = '125%';
const maxZoomLevel = '400%';
class Zoom extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async test() {
await this.modPage.waitForSelector(e.resetZoomButton, ELEMENT_WAIT_LONGER_TIME);
const wbBox = await this.modPage.getElementBoundingBox(e.whiteboard);
const clipObj = constructClipObj(wbBox);
const screenshotOptions = {
maxDiffPixels: 1000,
clip: clipObj,
};
// 100%
const zoomOutButtonLocator = await this.modPage.getLocator(e.zoomOutButton);
await expect(zoomOutButtonLocator).toBeDisabled();
const resetZoomButtonLocator = await this.modPage.getLocator(e.resetZoomButton);
await expect(resetZoomButtonLocator).toContainText(defaultZoomLevel);
// 125%
await this.modPage.waitAndClick(e.zoomInButton);
await expect(zoomOutButtonLocator).toBeEnabled();
await expect(resetZoomButtonLocator).toContainText(zoomedInZoomLevel);
await expect(this.modPage.page).toHaveScreenshot('moderator1-zoom125.png', screenshotOptions);
await expect(this.modPage2.page).toHaveScreenshot('moderator2-zoom125.png', screenshotOptions);
// max zoom
for(let i = 125; i < 400; i += 25) {
await this.modPage.waitAndClick(e.zoomInButton);
}
await expect(resetZoomButtonLocator).toContainText(maxZoomLevel);
await expect(this.modPage.page).toHaveScreenshot('moderator1-zoom400.png', screenshotOptions);
await expect(this.modPage2.page).toHaveScreenshot('moderator2-zoom400.png', screenshotOptions);
}
}
exports.Zoom = Zoom;

View File

@ -62,7 +62,10 @@ dependencies {
implementation "org.grails:grails-core:5.2.4"
implementation "org.springframework.boot:spring-boot-starter-actuator:${springVersion}"
implementation "org.springframework.boot:spring-boot-starter-tomcat:${springVersion}"
implementation "org.grails:grails-web-boot:5.2.5"
implementation "org.springframework:spring-webmvc:5.3.26"
implementation "org.grails:grails-logging"
implementation "org.grails:grails-plugin-rest:5.2.5"
implementation "org.grails:grails-plugin-databinding:5.2.4"
@ -123,6 +126,8 @@ tasks.named('bootWarMainClassName') {
configurations.implementation {
exclude group: 'io.micronaut', module: 'micronaut-aop'
exclude group: 'com.h2database', module: 'h2'
exclude group: 'org.graalvm.sdk', module: 'graal-sdk'
}
configurations {

View File

@ -1,4 +1,4 @@
grailsVersion=5.2.4
grailsVersion=5.3.2
gormVersion=7.1.0
gradleWrapperVersion=7.3.1
grailsGradlePluginVersion=5.0.0

View File

@ -18,7 +18,7 @@ pushd .
mkdir -p staging/usr/lib/bbb-html5/node
cd staging/usr/lib/bbb-html5/node
wget https://nodejs.org/dist/v${NODE_VERSION}/${NODE_DIRNAME}.tar.gz
wget --waitretry=30 --timeout=20 --retry-connrefused --retry-on-host-error --retry-on-http-error=404,522 https://nodejs.org/dist/v${NODE_VERSION}/${NODE_DIRNAME}.tar.gz
if [ -f ${NODE_DIRNAME}.tar.gz ]; then
tar xfz ${NODE_DIRNAME}.tar.gz
mv ${NODE_DIRNAME}/* .

View File

@ -36,3 +36,23 @@ $ yarn build
This command generates static content into the `build` directory
and can be served using any static contents hosting service.
### Troubleshooting
Sometimes cached content can interfere with your changes during live updates
in development or when building the docs.
To avoid this you can run:
```
$ yarn clear # ensure cached content is not interfering with your changes
$ rm -r versioned_docs versioned_sidebars versions.json # if you build multiple versions
```
## Cutting a new release
The docs for all versions are build and deployed from the `develop`-branch,
but the actual documentation per version lives in each version-branch (e.g. `v2.6.x-release`).
When cutting a new BigBlueButton release at least these two files need to be adjusted on `develop`:
- `build.sh`: the variable `BRANCHES` is a list of all branches for which documentation will be included
- `docusaurus.config.js`: adjust metadata and versions in `config.presets.docs.versions`

View File

@ -282,16 +282,16 @@ to
so that FreeSWITCH announces the external IP address when a connection is established.
Check `/etc/bigbluebutton/nginx/sip.nginx` to ensure its binding to the external IP address of the firewall [Configure FreeSWITCH for using SSL](/administration/install#configure-freeswitch-for-using-ssl).
Check `/etc/bigbluebutton/nginx/sip.nginx` to ensure its binding to the external IP address of the firewall.
Check that `enableListenOnly` is set to true in `/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml`, as in
Check that `enableListenOnly` is set to true in `/etc/bigbluebutton/bbb-html5.yml`, as in
```bash
$ grep enableListenOnly /usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml
$ grep enableListenOnly /etc/bigbluebutton/bbb-html5.yml
enableListenOnly: true
```
Next, edit `/usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml` change the value to `ip` to match the external IP address of the server.
Next, edit `/etc/bigbluebutton/bbb-webrtc-sfu/production.yml` change the value to `ip` to match the external IP address of the server.
```yaml
freeswitch:
@ -300,7 +300,8 @@ freeswitch:
port: 5066
```
If your runnig 2.2.29 or later, the value of `sip_ip` depends on whether you have `sipjsHackViaWs` set to true or false in `/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml` (see [Configure FreeSWITCH for using SSL](/administration/install#configure-freeswitch-for-using-ssl)).
If your runnig 2.2.29 or later, the value of `sip_ip` depends on whether you have `sipjsHackViaWs`
set to true or false in `/etc/bigbluebutton/bbb-html5.yml`.
You also need to [setup Kurento to use a STUN server](#extra-steps-when-server-is-behind-nat).
@ -310,7 +311,8 @@ After making the above changes, restart BigBlueButton.
$ bbb-conf --restart
```
To test, launch FireFox and try connecting to your BigBlueButton server and join the audio. If you see the words '[ WebRTC Audio ]' in the lower right-hand corner, it worked.
To test, launch FireFox and try connecting to your BigBlueButton server and join the audio.
If you see the words '[ WebRTC Audio ]' in the lower right-hand corner, it worked.
If it didn't work, there are two likely error messages when you try to connect with audio.

View File

@ -43,10 +43,11 @@ chown meteor:meteor $HTML5_CONFIG
then when called by `bbb-conf`, the above `apply-conf.sh` script will
- use the helper function `enableUFWRules` to [restrict access to specific ports](#restrict-access-to-specific-ports), and
- use the helper function `enableUFWRules` to restrict access to specific ports, and
- set `enableScreensharing` to `false` in the HTML5 configuration file at `/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml`.
Notice that `apply-conf.sh` includes a helper script [apply-lib.sh](https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-config/bin/apply-lib.sh). This helper script contains some functions to make it easy to apply common configuration changes, along with some helper variables, such as `HTML5_CONFIG`.
Notice that `apply-conf.sh` includes a helper script [apply-lib.sh](https://github.com/bigbluebutton/bigbluebutton/blob/v2.6.x-release/bigbluebutton-config/bin/apply-lib.sh).
This helper script contains some functions to make it easy to apply common configuration changes, along with some helper variables, such as `HTML5_CONFIG`.
The contents of `apply-config.sh` are not owned by any package, so it will never be overwritten.
@ -1013,7 +1014,7 @@ Configuring IP firewalling is _essential for securing your installation_. By def
If your server is behind a firewall already -- such as running within your company or on an EC2 instance behind a Amazon Security Group -- and the firewall is enforcing the above restrictions, you don't need a second firewall and can skip this section.
BigBlueButton comes with a [UFW](https://launchpad.net/ufw) based ruleset. It it can be applied on restart (c.f. [Automatically apply configuration changes on restart](#automatically-apply-configuration-changes-on-restart)) and restricts access only to the following needed ports:
BigBlueButton comes with a [UFW](https://launchpad.net/ufw) based ruleset. It it can be applied on restart and restricts access only to the following needed ports:
- TCP/IP port 22 for SSH
- TCP/IP port 80 for HTTP
@ -1028,7 +1029,9 @@ tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp6 0 0 :::22 :::* LISTEN 1739/sshd
```
To restrict external access minimal needed ports for BigBlueButton (with [HTML5 client set as default](#make-the-html5-client-default)). BigBlueButton supplies a helper function that you can call in `/etc/bigbluebutton/bbb-conf/apply-conf.sh` to setup a minimal firewall (see [Setup Firewall](#setup-firewall).
To restrict external access minimal needed ports for BigBlueButton.
BigBlueButton supplies a helper function that you can call in `/etc/bigbluebutton/bbb-conf/apply-conf.sh`
to setup a minimal firewall (see [Setup Firewall](#setup-firewall)).
You can also do it manually with the following commands

View File

@ -8,7 +8,7 @@ keywords:
- install
---
BigBlueButton 2.6 is under active development. We have tools to make it easy for you, a system administrator, to install BigBlueButton on a dedicated linux server. This document shows you how to install.
We have tools to make it easy for you, a system administrator, to install BigBlueButton on a dedicated linux server. This document shows you how to install.
## Before you install
@ -268,7 +268,7 @@ ii bbb-webrtc-sfu 1:2.6-6 amd64 BigBlueButton WebRTC SFU
With Greenlight installed (that was the `-g` option), you can open `https://<hostname>/b` in a browser (where `<hostname>` is the hostname you specified in the `bbb-install-2.6.sh` command), create a local account, create a room and join it.
<img src="/img/greenlight_welcome.png" alt="BigBlueButton's Greenlight Interface"/>
![BigBlueButton's Greenlight Interface](/img/greenlight_welcome.png)
You can integrate BigBlueButton with one of the 3rd party integrations by providing the integration of the server's address and shared secret. You can use `bbb-conf` to display this information using `bbb-conf --secret`.

View File

@ -14,9 +14,15 @@ BigBlueButton is [certified](https://site.imsglobal.org/certifications/bigbluebu
![imscertifiedsm](/img/imscertifiedsm.png)
BigBlueButton can accept incoming LTI launch requests from a tool consumer, which is the IMS term for any platform that can make an LTI request to an external tool (such as BigBlueButton). Such platforms include Desire2Learn, BlackBoard, Pearson Learning Studio, etc. See [IMS Interoperability Conformance Certification Status](https://www.imsglobal.org/cc/statuschart.cfm) for a full list of LTI compliant platforms.
BigBlueButton can accept incoming LTI launch requests from a tool consumer,
which is the IMS term for any platform that can make an LTI request to an external tool (such as BigBlueButton).
Such platforms include Desire2Learn, BlackBoard, Pearson Learning Studio, etc.
See [IMS Interoperability Conformance Certification Status](https://www.imsglobal.org/cc/statuschart.cfm)
for a full list of LTI compliant platforms.
What this means is that with no custom code, any LTI compliant platform can add BigBlueButton virtual classrooms to its system. For example, the following video shows how BigBlueButton uses LTI to integrate with BlackBoard, click [BigBlueButton LTI video](https://www.youtube.com/watch?v=OSTGfvICYX4&feature=youtu.be&hd=1).
What this means is that with no custom code, any LTI compliant platform can add BigBlueButton virtual classrooms to its system.
For example, the following video shows how BigBlueButton uses LTI to integrate with BlackBoard,
click [BigBlueButton LTI video](https://www.youtube.com/watch?v=OSTGfvICYX4&feature=youtu.be&hd=1).
### Installation of LTI module
@ -61,9 +67,13 @@ If you make modifications to your own lti.properties, be sure to restart `bbb-lt
## Configuring BigBlueButton as an External Tool
All LTI consumers have the ability to launch an external application that is LTI-compliant. BigBlueButton is [LTI 1.0 compliant](https://www.imsglobal.org/cc/detail.cfm?ID=172).
All LTI consumers have the ability to launch an external application that is LTI-compliant.
BigBlueButton is [LTI 1.0 compliant](https://www.imsglobal.org/cc/detail.cfm?ID=172).
This means that your BigBlueButton server can receive a single sign-on request that includes roles and additional custom parameters. To configure an external tool in the LTI consumer, you need to provide three pieces of information: URL, customer identifier, and shared secret. After installing the `bbb-lti` package, you can use the command `bbb-conf --lti` to retrieve these values.
This means that your BigBlueButton server can receive a single sign-on request that includes roles and additional custom parameters.
To configure an external tool in the LTI consumer, you need to provide three pieces of information:
URL, customer identifier, and shared secret.
After installing the `bbb-lti` package, you can use the command `bbb-conf --lti` to retrieve these values.
Here are the LTI configuration variables from a test BigBlueButton server.
@ -77,15 +87,23 @@ $ bbb-conf --lti
Icon URL: https://demo.bigbluebutton.org/lti/img/icon.ico
```
In the external tool configuration, we recommend privacy settings are set to **public** to allow the LMS to send lis_person_sourcedid and lis_person_contact_email_primary. The `bbb-lti` module will use these parameters for user identification once logged into the BigBlueButton session. If none of them is sent by default a generic name is going to be used (Viewer for viewer and Moderator for moderator).
In the external tool configuration, we recommend privacy settings are set to **public**
to allow the LMS to send lis_person_sourcedid and lis_person_contact_email_primary.
The `bbb-lti` module will use these parameters for user identification once logged into the BigBlueButton session.
If none of them is sent by default a generic name is going to be used (Viewer for viewer and Moderator for moderator).
An important note is that if your LTI consumer uses https, as the LTI tool is displayed in an iframe you will see only a blank page. In that case you can configure the link to open the tool in a different window (most LTI consumers allow it) or use https with the URL provided (https://demo.bigbluebutton.org/lti/tool).
An important note is that if your LTI consumer uses https, as the LTI tool is displayed in an iframe, you will see only a blank page.
In that case you can configure the link to open the tool in a different window (most LTI consumers allow it)
or use https with the URL provided (e.g. `https://demo.bigbluebutton.org/lti/tool`).
## Launching BigBlueButton as an External Tool
The LTI launch request passes along the user's role (which `bbb-lti` will map to the two roles in BigBlueButton: moderator or viewer.
The LTI launch request passes along the user's role (which `bbb-lti` will map to the two roles in BigBlueButton): moderator or viewer.
If no role information is given, or if the role is privileged (i.e. . Faculty, Mentor, Administrator, Instructor, etc.), then when `bbb-lti` receives a valid launch request, it will start a BigBlueButton session and join the user as **moderator**. In all other cases, the user will join as a **viewer**.
If no role information is given, or if the role is privileged (i.e. . Faculty, Mentor, Administrator, Instructor, etc.),
then when `bbb-lti` receives a valid launch request,
it will start a BigBlueButton session and join the user as **moderator**.
In all other cases, the user will join as a **viewer**.
### Custom Parameters

View File

@ -90,19 +90,13 @@ Updated in 2.6:
There are three types in the API.
String
: This data type indicates a (UTF-8) encoded string. When passing String values to BigBlueButton API calls, make sure that you use correctly URL-encoded UTF-8 values so international text will show up correctly. The string must not contain control characters (values 0x00 through 0x1F).
**String:**<br /> This data type indicates a (UTF-8) encoded string. When passing String values to BigBlueButton API calls, make sure that you use correctly URL-encoded UTF-8 values so international text will show up correctly. The string must not contain control characters (values 0x00 through 0x1F).
Some BigBlueButton API parameters put additional restrictions on which characters are allowed, or on the lengths of the string. These restrictions are described in the parameter documentation.
Number
**Number:**<br /> This data type indicates a non-negative integer value. The parameter value must only contain the digits `0` through `9`. There should be no leading sign (`+` or `-`), and no comma or period characters.
: This data type indicates a non-negative integer value. The parameter value must only contain the digits `0` through `9`. There should be no leading sign (`+` or `-`), and no comma or period characters.
Boolean
: A true/false value. The value must be specified as the literal string `true` or `false` (all lowercase), other values may be misinterpreted.
**Boolean:**<br />A true/false value. The value must be specified as the literal string `true` or `false` (all lowercase), other values may be misinterpreted.
## API Security Model
@ -219,7 +213,7 @@ The following section describes the monitoring calls
### Recording
| Resource | Description |
| :--------------------- | :------------------------------------------------------------ |
| :--- | :--- |
| getRecordings | Get a list of recordings. |
| publishRecordings | Enables publishing or unpublishing of a recording. |
| deleteRecordings | Deletes an existing recording |
@ -234,16 +228,16 @@ The following response parameters are standard to every call and may be returned
**Parameters:**
| Param Name | Required / Optional | Type | Description |
| :--------- | :------------------ | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| checksum | Varies | String | See the [API Security ModelAnchor](#api-security-model) section for more details on the usage for this parameter. This is basically a SHA-1 hash of `callName + queryString + sharedSecret`. The security salt will be configured into the application at deploy time. All calls to the API must include the checksum parameter. |
| :--- | :--- | :---- | :--- |
| checksum | Varies | String | See the [API Security ModelAnchor](#api-security-model) section for more details on the usage for this parameter.<br /> This is basically a SHA-1 hash of `callName + queryString + sharedSecret`. The security salt will be configured into the application at deploy time. All calls to the API must include the checksum parameter. |
**Response:**
| Param Name | When Returned | Type | Description |
| :--------- | :------------ | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| returncode | Always | String | Indicates whether the intended function was successful or not. Always one of two values:`FAILED` There was an error of some sort look for the message and messageKey for more information. Note that if the `returncode` is FAILED, the call-specific response parameters marked as “always returned” will not be returned. They are only returned as part of successful responses.`SUCCESS` The call succeeded the other parameters that are normally associated with this call will be returned. |
| message | Sometimes | String | A message that gives additional information about the status of the call. A message parameter will always be returned if the returncode was `FAILED`. A message may also be returned in some cases where returncode was `SUCCESS` if additional information would be helpful. |
| messageKey | Sometimes | String | Provides similar functionality to the message and follows the same rules. However, a message key will be much shorter and will generally remain the same for the life of the API whereas a message may change over time. If your third party application would like to internationalize or otherwise change the standard messages returned, you can look up your own custom messages based on this messageKey. |
| Param Name | When Returned | Type | Description |
| :--- | :--- | :----- | :--- |
| returncode | Always | String | Indicates whether the intended function was successful or not. Always one of two values:<br /><br />`FAILED` There was an error of some sort look for the message and messageKey for more information. Note that if the `returncode` is FAILED, the call-specific response parameters marked as “always returned” will not be returned. They are only returned as part of successful responses.<br /><br />`SUCCESS` The call succeeded the other parameters that are normally associated with this call will be returned. |
| message | Sometimes | String | A message that gives additional information about the status of the call. A message parameter will always be returned if the returncode was `FAILED`. A message may also be returned in some cases where returncode was `SUCCESS` if additional information would be helpful.|
| messageKey | Sometimes | String | Provides similar functionality to the message and follows the same rules. However, a message key will be much shorter and will generally remain the same for the life of the API whereas a message may change over time. If your third party application would like to internationalize or otherwise change the standard messages returned, you can look up your own custom messages based on this messageKey.|
### create
@ -259,7 +253,66 @@ http&#58;//yourserver.com/bigbluebutton/api/create?[parameters]&checksum=[checks
**Parameters:**
{% include api_table.html endpoint="create" %}
| 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) |
**Example Requests:**
@ -278,7 +331,7 @@ http&#58;//yourserver.com/bigbluebutton/api/create?[parameters]&checksum=[checks
<attendeePW>ap</attendeePW>
<moderatorPW>mp</moderatorPW>
<createTime>1531155809613</createTime>
<voiceBridge>70757</voiceBridge>
<`voiceBridge`>70757</`voiceBridge`>
<dialNumber>613-555-1234</dialNumber>
<createDate>Mon Jul 09 17:03:29 UTC 2018</createDate>
<hasUserJoined>false</hasUserJoined>
@ -304,7 +357,7 @@ curl --request POST \
--data moderatorPW=mp \
--data name=random-1730297 \
--data record=false \
--data voiceBridge=71296 \
--data `voiceBridge`=71296 \
--data checksum=1234;
```
@ -422,7 +475,22 @@ http&#58;//yourserver.com/bigbluebutton/api/join?[parameters]&checksum=[checksum
**Parameters:**
{% include api_table.html endpoint="join" %}
| 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 |
**Example Requests:**
@ -457,7 +525,9 @@ https&#58;//yourserver.com/bigbluebutton/api/insertDocument?[parameters]&checksu
**Parameters:**
{% include api_table.html endpoint="insertDocument" %}
|Param Name|Type|Description|
|:----|:----|:----|
|`meetingID` *(required)*|String|The meeting ID that identifies the meeting you want to insert documents.|
**Example Requests:**
@ -505,7 +575,9 @@ http&#58;//yourserver.com/bigbluebutton/api/isMeetingRunning?[parameters]&checks
**Parameters:**
{% include api_table.html endpoint="isMeetingRunning" %}
|Param Name|Type|Description|
|:----|:----|:----|
|`meetingID` *(required)*|String|The meeting ID that identifies the meeting you are attempting to check on.|
**Example Requests:**
@ -532,7 +604,11 @@ Use this to forcibly end a meeting and kick all participants out of the meeting.
**Parameters:**
{% include api_table.html endpoint="end" %}
|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.|
**Example Requests:**
@ -574,7 +650,10 @@ Resource URL:
**Parameters:**
{% include api_table.html endpoint="getMeetingInfo" %}
|Param Name|Type|Description|
|:----|:----|:----|
|`meetingID` *(required)*|String|The meeting ID that identifies the meeting you are attempting to check on.|
**Example Requests:**
@ -590,7 +669,7 @@ Resource URL:
<internalMeetingID>183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1531240585189</internalMeetingID>
<createTime>1531240585189</createTime>
<createDate>Tue Jul 10 16:36:25 UTC 2018</createDate>
<voiceBridge>70066</voiceBridge>
<`voiceBridge`>70066</`voiceBridge`>
<dialNumber>613-555-1234</dialNumber>
<attendeePW>ap</attendeePW>
<moderatorPW>mp</moderatorPW>
@ -690,7 +769,7 @@ http&#58;//yourserver.com/bigbluebutton/api/getMeetings?checksum=1234
<internalMeetingID>183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1531241258036</internalMeetingID>
<createTime>1531241258036</createTime>
<createDate>Tue Jul 10 16:47:38 UTC 2018</createDate>
<voiceBridge>70066</voiceBridge>
<`voiceBridge`>70066</`voiceBridge`>
<dialNumber>613-555-1234</dialNumber>
<attendeePW>ap</attendeePW>
<moderatorPW>mp</moderatorPW>
@ -725,7 +804,15 @@ http&#58;//yourserver.com/bigbluebutton/api/getRecordings?[parameters]&checksum=
**Parameters:**
{% include api_table.html endpoint="getRecordings" %}
|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.|
**Example Requests:**
@ -838,7 +925,10 @@ Publish and unpublish recordings for a given recordID (or set of record IDs).
**Parameters:**
{% include api_table.html endpoint="publishRecordings" %}
|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.|
**Example Requests:**
@ -890,7 +980,9 @@ Update metadata for a given recordID (or set of record IDs). Available since ver
**Parameters:**
{% include api_table.html endpoint="updateRecordings" %}
|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.|
**Example Requests:**
@ -915,7 +1007,9 @@ Get a list of the caption/subtitle files currently available for a recording. It
**Parameters:**
{% include api_table.html endpoint="getRecordingTextTracks" %}
|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.)|
**Example Response:**
@ -1001,13 +1095,17 @@ This API is asynchronous. It can take several minutes for the uploaded file to b
**Parameters:**
{% include api_table.html endpoint="putRecordingTextTrack" %}
|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.|
POST Body
: 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.
file
: (Type Binary Data, Optional) Contains the uploaded subtitle or caption file. If this parameter is missing, or if the POST request has no body, then any existing text track matching the kind and lang specified will be deleted. If known, the uploading application should set the `Content-Type` to a value appropriate to the file format. If Content-Type is unset, or does not match a known subtitle format, the uploaded file will be probed to automatically detect the type.
**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.
**file:** <br />(Type Binary Data, Optional) Contains the uploaded subtitle or caption file. If this parameter is missing, or if the POST request has no body, then any existing text track matching the kind and lang specified will be deleted. If known, the uploading application should set the `Content-Type` to a value appropriate to the file format. If Content-Type is unset, or does not match a known subtitle format, the uploaded file will be probed to automatically detect the type.
Multiple types of subtitles are accepted for upload, but they will be converted to the WebVTT format for display.

View File

@ -658,4 +658,4 @@ The above will re-sync your clock.
## Set up HTTPS
Follow [Configure SSL on your BigBlueButton server](/administration/install#configure-ssl-on-your-bigbluebutton-server)
See the [installation instructions](/administration/install) on how to configure ssl on your BigBlueButton server.

View File

@ -21,9 +21,9 @@ If you would like to help translate BigBlueButton into your language, or you see
2. Choose the project
For helping to translate the HTML5 client, visit [BigBlueButton v2.4 HTML5 client](https://www.transifex.com/bigbluebutton/bigbluebutton-v24-html5-client/).
For helping to translate, visit the [BigBlueButton on Transifex](https://www.transifex.com/bigbluebutton/) (needs a login).
You'll see a list of languages ready for translation.
You'll see a list of languages and components of BigBlueButton ready for translation.
3. Click the name of the language you wish to translate
@ -35,9 +35,9 @@ You'll see a list of languages ready for translation.
#### Administrators can pull specific languages their Transifex account is associated with
In BigBlueButton 2.2, 2.3, 2.4, 2.5 the script used for pulling the locales is located in the `bigbluebutton-html5` directory
Up to BigBlueButton 2.5 the script used for pulling the locales is located in the `bigbluebutton-html5` directory
https://github.com/bigbluebutton/bigbluebutton/blob/v2.4.x-release/bigbluebutton-html5/transifex.sh
https://github.com/bigbluebutton/bigbluebutton/blob/v2.5.x-release/bigbluebutton-html5/transifex.sh
You can trigger the download of the latest strings by running a command of the format `./transifex.sh pt_BR de` (passing the code for the languages you'd like to download)

View File

@ -285,7 +285,7 @@ timestamp=1234567890
for this meeting. The application needs to detect the create event (`meeting_created_message`) to
have a proper mapping of internal to external meeting IDs. So make sure the web hooks application
is always running while BigBlueButton is running!
* If you register a hook with, for example, the URL "http://myserver.com/my/hook" and no `meetingID`
* If you register a hook with, for example, the URL `http://myserver.com/my/hook` and no `meetingID`
set (making it a global hook) and later try to register another hook with the same URL but with
a `meetingID` set, the first hook will not be removed nor modified, while the second hook will
not be created.

View File

@ -60,7 +60,7 @@ To do so, follow the steps below:
2) Download the migration rake tasks with the following command:
```bash
wget -P lib/tasks/migrations https://raw.githubusercontent.com/bigbluebutton/greenlight/master/lib/tasks/migrations/migrations.rake
wget -P lib/tasks/migrations https://raw.githubusercontent.com/bigbluebutton/greenlight/v2/lib/tasks/migrations/migrations.rake
```
The file **migrations.rake** should now be present in your **/lib/tasks/migrations** directory.
@ -125,8 +125,7 @@ sudo docker exec -it greenlight-v2 bundle exec rake migrations:roles
The Users will be migrated with their corresponding role.
Important notes:
- **Once the Users migration is completed, the migrated users will be prompted to reset their password via an automated email. The accounts passwords can't be migrated from Greenlight v2.**
- Pending, denied and deleted users will not be migrated to Greenlight v3.
- **The accounts passwords can't be migrated from Greenlight v2. A rake task that sends an email to all the users and prompts them to reset their password is provided for Greenlight v3. When the migration is completed, please jump to [After the Migration](#after-the-migration). Please note that if you are using external accounts, like Google or Microsoft, this is not applicable.**- Pending, denied and deleted users will not be migrated to Greenlight v3.
- Both local and external users will be migrated.
**To migrate all of your v2 users to v3, run the following command:**
@ -137,7 +136,7 @@ sudo docker exec -it greenlight-v2 bundle exec rake migrations:users
**To migrate only a portion of the users starting from *FIRST_USER_ID* to *LAST_USER_ID*, run this command instead:**
```bash
sudo docker exec -it greenlight-v2 bundle exec rake migrations:users[**FIRST_USER_ID, LAST_USER_ID**]
sudo docker exec -it greenlight-v2 bundle exec rake migrations:users\[<FIRST_USER_ID>,<LAST_USER_ID>]
```
*Administrators can use the last command to migrate resources in parallel, the same migration task can be run in separate processes each migrating a portion of the resources class simultaneously.*
@ -163,7 +162,7 @@ sudo docker exec -it greenlight-v2 bundle exec rake migrations:rooms
**To migrate only a portion of users starting from **FIRST_ROOM_ID** to **LAST_ROOM_ID**, run this command instead**:**
```bash
sudo docker exec -it greenlight-v2 bundle exec rake migrations:rooms[**FIRST_ROOM_ID, LAST_ROOM_ID**]
sudo docker exec -it greenlight-v2 bundle exec rake migrations:rooms\[<FIRST_ROOM_ID>,<LAST_ROOM_ID>]
```
*Note: The partitioning is based on resources id value and not there position in the database, so calling **rake migrations:rooms[1, 100]** will not migrate the first 100 active users rooms but rather active users rooms having an id of 1 to 100 if existed.*
@ -191,3 +190,24 @@ sudo docker exec -it greenlight-v2 bundle exec rake migrations:settings
```
**If you have an error, try re-running the migration task to resolve any failed resources migration.**
## After the Migration
Having completed the migration successfully, it is now imperative to inform users of the need to reset their Greenlight account passwords.
This can be achieved through the utilization of the rake task available in Greenlight v3.
**It is important to note, however, that this is not applicable for users who utilize external accounts such as Google or Microsoft.**
To send a reset password email to all your users, run the following command:
```bash
sudo docker exec -it greenlight-v3 bundle exec rake migration:reset_password_email\[<BASE URL>]
```
The &lt;BASE URL&gt; in the command above should be replaced with your Greenlight domain name.
Also, please note that the BigBlueButton recordings list will now be empty.
To re-sync the list of recordings, run the following command:
```bash
sudo docker exec -it greenlight-v3 bundle exec rake server_recordings_sync
```

View File

@ -10,9 +10,7 @@ order: 1
## Overview
This document gives you an overview of BigBlueButton 2.6, the latest version of BigBlueButton now in development.
*Note:* This document is DRAFT and will be expanded upon as 2.6 development goes through alpha, beta, and release.
This document gives you an overview of BigBlueButton 2.6.
BigBlueButton 2.6 offers users improved usability, increased engagement, and more performance.
@ -147,7 +145,7 @@ When you click Apply, BigBlueButton will send prompts to each user to move them
This release introduces a new recording format that creates a single video file from audio, video, screen share, presentation, and whiteboard marks recorded during the session. The file format is webm (vp9 video), although configuration options is available to create an mp4 (h264 video) file instead.
Learn more about [how to enable generating MP4 (h264 video) output](https://docs.bigbluebutton.org/admin/customize.html#enable-generating-mp4-h264-video-output)
Learn more about [how to enable generating MP4 (h264 video) output](https://docs.bigbluebutton.org/administration/customize#enable-generating-mp4-h264-video-output)
#### Change of parameters naming
@ -207,6 +205,9 @@ 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.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)
- [rc.7](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.6.0-rc.7)
- [rc.6](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.6.0-rc.6)
- [rc.5](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.6.0-rc.5)

View File

@ -46,7 +46,7 @@ We can measure our success by how easily we enable each person to accomplish the
* **Scalability** - We build BigBlueButton to be a highly collaborative environment. Our uses cases are one-to-one (such as student tutoring or coaching), small group collaboration, and one-to-many (recommend 100 users or less in a single session). Even in the one-to-many, you can have 20 users all sharing the webcams and all able to talk. In other words, we didn't build a webinar-type application that restricts usage. Still, we think about scalability in each release and add (and refactor) the product to increase it.
* **API** - BigBlueButton's provides a simple API for integration, and simple is good. The API has enabled a [growing list](https://www.bigbluebutton.org/integrations/) of 3rd party integrations with other open source products. As we work towards 1.0, we want to keep the APIs simple to further encourage integration.
* **API** - BigBlueButton's provides a simple API for integration, and simple is good. The API has enabled a [growing list](https://www.bigbluebutton.org/integrations/) of 3rd party integrations with other open source products.
Obviously, we can't do everything in a single release. If you read through the [release notes](/release-notes, you'll see that we sometimes implement features in phases -- such as record and playback being release first as capturing slides (v 0.80), then as capturing all content (v 0.81), and then with Start/Stop Record button (v 0.9.1) for moderator.
@ -54,7 +54,6 @@ The following sections outline (in no particular order) the road map for BigBlue
If you have feedback on this document, please post to [BigBlueButton-dev](https://groups.google.com/group/bigbluebutton-dev/topics?gvc=2) mailing list.
The latest release is [BigBlueButton 2.2](/administration/install), which provides a full HTML5 client for desktop, laptop, chromebook, and mobile (Android 6.0+ and iOS 12.2+).
## Core Features

View File

@ -56,7 +56,7 @@ and then to rebuild a recording, use `sudo bbb-record --rebuild <internal_meetin
$ sudo bbb-record --rebuild 298b06603719217df51c5d030b6e9417cc036476-1559314745219
```
## mediasoup
## bbb-webrtc-sfu and mediasoup
### Webcams/screen sharing aren't working
@ -162,6 +162,27 @@ No. Scalability improves a lot with mediasoup, but there are still a couple of b
- The signaling server (bbb-webrtc-sfu): it does not scale vertically indefinitely. There's always work ongoing on this area that can be tracked in [this issue](https://github.com/mconf/mconf-tracker/issues/238);
- The mediasoup worker balancing algorithm implemented by bbb-webrtc-sfu is still focused on multiparty meetings with a restrained number of users. If your goal is thousand-user 1-N (streaming-like) meetings, you may max out CPU usage on certain mediasoup workers even though there are other idle oworkers free.
### bbb-webrtc-sfu fails to start with a SETSCHEDULER error
bbb-webrtc-sfu runs with CPUSchedulingPolicy=fifo. In systems without appropriate capabilities (SYS_NICE), the application will fail to start.
The error can be verified in journalctl logs as 214/SETSCHEDULER.
Similar to [bbb-html5](#bbb-html5-fails-to-start-with-a-setscheduler-error), you can override this by running
```
mkdir /etc/systemd/system/bbb-webrtc-sfu.service.d
```
and creating `/etc/systemd/system/bbb-webrtc-sfu.service.d/override.conf` with the following contents
```
[Service]
CPUSchedulingPolicy=other
Nice=-10
```
Then do `systemctl daemon-reload` and restart BigBlueButton.
## Kurento
### WebRTC video not working with Kurento
@ -384,7 +405,7 @@ $ sudo bbb-conf --check
### FreeSWITCH fails to start with a SETSCHEDULER error
When running in a container (like a chroot, OpenVZ or LXC), it might not be possible for FreeSWITCH to set its CPU priority to [real-time round robin](https://man7.org/linux/man-pages/man2/sched_setscheduler.2.html). If not, it will result in lower performance compared to a non-virtualized installation.
When running in a container (like a chroot, OpenVZ, LXC or LXD), it might not be possible for FreeSWITCH to set its CPU priority to [real-time round robin](https://man7.org/linux/man-pages/man2/sched_setscheduler.2.html). If not, it will result in lower performance compared to a non-virtualized installation.
If you running BigBlueButton in a container and an error starting FreeSWITCH, try running `systemctl status freeswitch.service` and see if you see the error related to SETSCHEDULER
@ -405,12 +426,15 @@ Oct 02 16:17:29 scw-9e2305 systemd[1]: freeswitch.service: Start request repeate
Oct 02 16:17:29 scw-9e2305 systemd[1]: Failed to start freeswitch.
```
If you see `SETSCHEDULER` in the error message, edit `/lib/systemd/system/freeswitch.service` and comment out the line containing `CPUSchedulingPolicy=rr` (round robin)
If you see `SETSCHEDULER` in the error message, edit `/lib/systemd/system/freeswitch.service` and comment the following:
```ini
IOSchedulingPriority=2
```properties
#LimitRTPRIO=infinity
#LimitRTTIME=7000000
#IOSchedulingClass=realtime
#IOSchedulingPriority=2
#CPUSchedulingPolicy=rr
CPUSchedulingPriority=89
#CPUSchedulingPriority=89
```
Save the file, run `systemctl daemon-reload`, and then restart BigBlueButton. FreeSWITCH should now startup without error.
@ -904,55 +928,16 @@ However, if you install BigBlueButton within an LXD container, you will get the
# Error: Unable to connect to the FreeSWITCH Event Socket Layer on port 8021
```
You'll also get an error from starting FreeSWITCH with `bbb-conf --restart`. When you try `systemctl status freeswitch.service`, you'll see an error with SETSCHEDULER.
If you check the output of `sudo bbb-conf --status`, you'll be able to identify that three different applications failed to start: FreeSWITCH, bbb-webrtc-sfu and bbb-html5.
Optionally, check their errors via `systemctl status <service-name>.service` and verify that their boot sequence failed due to a SETSCHEDULER error.
```bash
$ sudo systemctl status freeswitch.service
● freeswitch.service - freeswitch
Loaded: loaded (/lib/systemd/system/freeswitch.service; enabled; vendor preset: enabled)
Active: inactive (dead) (Result: exit-code) since Wed 2017-04-26 16:34:24 UTC; 23h ago
Process: 7038 ExecStart=/opt/freeswitch/bin/freeswitch -u freeswitch -g daemon -ncwait $DAEMON_OPTS (code=exited, status=214/SETSCHEDULER)
This error occurs because the default systemd unit scripts for FreeSWITCH, bbb-html5 and bbb-webrtc-sfu try to run with permissions not available to the LXD container.
To get them working within an LXD container, follow the steps outlined in the following sections:
- [FreeSWITCH fails to start with a SETSCHEDULER error](#freeswitch-fails-to-start-with-a-setscheduler-error)
- [bbb-webrtc-sfu fails to start with a SETSCHEDULER error](#bbb-webrtc-sfu-fails-to-start-with-a-setscheduler-error)
- [bbb-html5 fails to start with a SETSCHEDULER error](#bbb-html5-fails-to-start-with-a-setscheduler-error)
Apr 26 16:34:24 big systemd[1]: Failed to start freeswitch.
Apr 26 16:34:24 big systemd[1]: freeswitch.service: Unit entered failed state.
Apr 26 16:34:24 big systemd[1]: freeswitch.service: Failed with result 'exit-code'.
Apr 26 16:34:24 big systemd[1]: freeswitch.service: Service hold-off time over, scheduling restart.
Apr 26 16:34:24 big systemd[1]: Stopped freeswitch.
Apr 26 16:34:24 big systemd[1]: freeswitch.service: Start request repeated too quickly.
Apr 26 16:34:24 big systemd[1]: Failed to start freeswitch.
```
This error occurs because the default systemd unit script for FreeSWITCH tries to run with permissions not available to the LXD container. To run FreeSWITCH within an LXD container, edit `/lib/systemd/system/freeswitch.service` and replace with the following
```properties
[Unit]
Description=freeswitch
After=syslog.target network.target local-fs.target
[Service]
Type=forking
PIDFile=/opt/freeswitch/var/run/freeswitch/freeswitch.pid
Environment="DAEMON_OPTS=-nonat"
EnvironmentFile=-/etc/default/freeswitch
ExecStart=/opt/freeswitch/bin/freeswitch -u freeswitch -g daemon -ncwait $DAEMON_OPTS
TimeoutSec=45s
Restart=always
WorkingDirectory=/opt/freeswitch
User=freeswitch
Group=daemon
[Install]
WantedBy=multi-user.target
```
Then enter the following commands to load the new unit file and restart BigBlueButton.
```bash
$ sudo systemctl daemon-reload
$ sudo bbb-conf --restart
```
You can run BigBlueButton within a LXD container.
You can now run BigBlueButton within a LXD container.
### Unable to connect to redis

View File

@ -8,8 +8,8 @@ const darkCodeTheme = require('prism-react-renderer/themes/dracula');
const config = {
title: 'BigBlueButton',
tagline: 'Official Documentation',
url: 'https://bigbluebutton.github.io/',
baseUrl: '/bigbluebutton/',
url: 'https://docs.bigbluebutton.org/',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
@ -60,7 +60,7 @@ const config = {
src: 'img/logo.svg',
},
items: [
{to: '/teaching', label: 'Teaching', position: 'left'},
{to: 'https://bigbluebutton.org/teachers/tutorials/', label: 'Teaching', position: 'left'},
{to: '/development/guide', label: 'Development', position: 'left'},
{to: '/administration/install', label: 'Administration', position: 'left'},
{to: '/greenlight/v2/overview', label: 'Greenlight', position: 'left'},

View File

@ -16,17 +16,8 @@ const sidebars = {
teaching: [
{
label: 'Teaching',
type: 'category',
items: [
{
type: 'autogenerated',
dirName: 'teaching',
},
],
link: {
type: 'doc',
id: 'teaching/index',
},
type: 'link',
href: 'https://bigbluebutton.org/teachers/tutorials/',
}
],
development: [

View File

@ -22,7 +22,7 @@ const FeatureList: FeatureItem[] = [
</>
),
actionText: "Teaching Experience",
docId: "/teaching"
docId: "https://bigbluebutton.org/teachers/tutorials/"
},
{
title: 'I am a developer',
@ -65,7 +65,7 @@ const FeatureList: FeatureItem[] = [
Svg: require('@site/static/img/icon_new.svg').default,
description: (
<>
Discover the new features of BigBlueButton in version 2.6 (still under development).
Discover the new features of BigBlueButton in version 2.6.
</>
),
actionText: "Discover",

View File

@ -86,7 +86,7 @@ html {
font-family: 'Open Sans';
src: url('/static/fonts/OpenSans-Bold.ttf');
font-weight: bold;
font-style: italic;
font-style: normal;
}
@font-face {
@ -100,7 +100,7 @@ html {
font-family: 'Open Sans';
src: url('/static/fonts/OpenSans-ExtraBold.ttf');
font-weight: bold;
font-style: italic;
font-style: normal;
}
@font-face {
@ -112,7 +112,7 @@ html {
.col {
padding-bottom: 48px;
}
}
.button.button--install {
background-color: #273d85;

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