Merge branch 'v2.6.x-release' of https://github.com/bigbluebutton/bigbluebutton into tldraw-viewbox-sync
This commit is contained in:
commit
7bf8668f23
@ -1 +1 @@
|
||||
git clone --branch v2.9.0-beta.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.9.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -30,6 +30,7 @@ const DEFAULT_FULLAUDIO_MEDIA_SERVER = MEDIA.audio.fullAudioMediaServer;
|
||||
const LISTEN_ONLY_OFFERING = MEDIA.listenOnlyOffering;
|
||||
const MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, '');
|
||||
const RECONNECT_TIMEOUT_MS = MEDIA.listenOnlyCallTimeout || 15000;
|
||||
const { audio: NETWORK_PRIORITY } = MEDIA.networkPriorities || {};
|
||||
const SENDRECV_ROLE = 'sendrecv';
|
||||
const RECV_ROLE = 'recv';
|
||||
const BRIDGE_NAME = 'fullaudio';
|
||||
@ -316,6 +317,7 @@ export default class SFUAudioBridge extends BaseAudioBridge {
|
||||
offering: isListenOnly ? LISTEN_ONLY_OFFERING : true,
|
||||
signalCandidates: SIGNAL_CANDIDATES,
|
||||
traceLogs: TRACE_LOGS,
|
||||
networkPriority: NETWORK_PRIORITY,
|
||||
};
|
||||
|
||||
this.broker = new AudioBroker(
|
||||
|
@ -12,6 +12,7 @@ const SFU_URL = SFU_CONFIG.wsUrl;
|
||||
const OFFERING = SFU_CONFIG.screenshare.subscriberOffering;
|
||||
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
|
||||
const TRACE_LOGS = Meteor.settings.public.kurento.traceLogs;
|
||||
const { screenshare: NETWORK_PRIORITY } = Meteor.settings.public.media.networkPriorities || {};
|
||||
|
||||
const BRIDGE_NAME = 'kurento'
|
||||
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
|
||||
@ -307,6 +308,7 @@ export default class KurentoScreenshareBridge {
|
||||
signalCandidates: SIGNAL_CANDIDATES,
|
||||
forceRelay: shouldForceRelay(),
|
||||
traceLogs: TRACE_LOGS,
|
||||
networkPriority: NETWORK_PRIORITY,
|
||||
};
|
||||
|
||||
this.broker = new ScreenshareBroker(
|
||||
|
@ -4,6 +4,7 @@ import WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteb
|
||||
import WhiteboardContainer from '/imports/ui/components/whiteboard/container';
|
||||
import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container';
|
||||
import { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils';
|
||||
import { SPACE } from '/imports/utils/keyCodes';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Session } from 'meteor/session';
|
||||
@ -76,6 +77,7 @@ class Presentation extends PureComponent {
|
||||
fitToWidth: false,
|
||||
isFullscreen: false,
|
||||
tldrawAPI: null,
|
||||
isPanning: false,
|
||||
};
|
||||
|
||||
this.currentPresentationToastId = null;
|
||||
@ -91,6 +93,8 @@ class Presentation extends PureComponent {
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.setTldrawAPI = this.setTldrawAPI.bind(this);
|
||||
this.renderPresentationMenu = this.renderPresentationMenu.bind(this);
|
||||
this.setIsPanning = this.setIsPanning.bind(this);
|
||||
this.handlePanShortcut = this.handlePanShortcut.bind(this);
|
||||
|
||||
this.onResize = () => setTimeout(this.handleResize.bind(this), 0);
|
||||
this.renderCurrentPresentationToast = this.renderCurrentPresentationToast.bind(this);
|
||||
@ -122,8 +126,22 @@ class Presentation extends PureComponent {
|
||||
return stateChange;
|
||||
}
|
||||
|
||||
handlePanShortcut(e) {
|
||||
const { userIsPresenter } = this.props;
|
||||
if (e.keyCode === SPACE && userIsPresenter) {
|
||||
switch(e.type) {
|
||||
case 'keyup':
|
||||
return this.state.isPanning && this.setIsPanning();
|
||||
case 'keydown':
|
||||
return !this.state.isPanning && this.setIsPanning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getInitialPresentationSizes();
|
||||
this.refPresentationContainer.addEventListener('keydown', this.handlePanShortcut);
|
||||
this.refPresentationContainer.addEventListener('keyup', this.handlePanShortcut);
|
||||
this.refPresentationContainer
|
||||
.addEventListener(FULLSCREEN_CHANGE_EVENT, this.onFullscreenChange);
|
||||
window.addEventListener('resize', this.onResize, false);
|
||||
@ -166,7 +184,7 @@ class Presentation extends PureComponent {
|
||||
clearFakeAnnotations,
|
||||
} = this.props;
|
||||
|
||||
const { presentationWidth, presentationHeight } = this.state;
|
||||
const { presentationWidth, presentationHeight, isPanning } = this.state;
|
||||
const {
|
||||
numCameras: prevNumCameras,
|
||||
presentationBounds: prevPresentationBounds,
|
||||
@ -267,6 +285,10 @@ class Presentation extends PureComponent {
|
||||
value: currentSlide.num,
|
||||
});
|
||||
}
|
||||
|
||||
if (isPanning || !userIsPresenter && prevProps.userIsPresenter) {
|
||||
this.setIsPanning();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -276,6 +298,8 @@ class Presentation extends PureComponent {
|
||||
window.removeEventListener('resize', this.onResize, false);
|
||||
this.refPresentationContainer
|
||||
.removeEventListener(FULLSCREEN_CHANGE_EVENT, this.onFullscreenChange);
|
||||
this.refPresentationContainer.removeEventListener('keydown', this.handlePanShortcut);
|
||||
this.refPresentationContainer.removeEventListener('keyup', this.handlePanShortcut);
|
||||
|
||||
if (fullscreenContext) {
|
||||
layoutContextDispatch({
|
||||
@ -291,7 +315,14 @@ class Presentation extends PureComponent {
|
||||
setTldrawAPI(api) {
|
||||
this.setState({
|
||||
tldrawAPI: api,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setIsPanning() {
|
||||
this.setState({
|
||||
isPanning: !this.state.isPanning,
|
||||
});
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
@ -738,6 +769,8 @@ class Presentation extends PureComponent {
|
||||
layoutContextDispatch,
|
||||
presentationIsOpen,
|
||||
}}
|
||||
setIsPanning={this.setIsPanning}
|
||||
isPanning={this.state.isPanning}
|
||||
isZoomed={this.state.isZoomed}
|
||||
curPageId={this.state.tldrawAPI?.getPage()?.id}
|
||||
currentSlideNum={currentSlide.num}
|
||||
@ -993,6 +1026,7 @@ class Presentation extends PureComponent {
|
||||
presentationHeight={svgHeight}
|
||||
isViewersCursorLocked={isViewersCursorLocked}
|
||||
zoomChanger={this.zoomChanger}
|
||||
isPanning={this.state.isPanning}
|
||||
fitToWidth={fitToWidth}
|
||||
zoomValue={zoom}
|
||||
/>
|
||||
|
@ -84,6 +84,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.whiteboard.toolbar.multiUserOff',
|
||||
description: 'Whiteboard toolbar turn multi-user off menu',
|
||||
},
|
||||
pan: {
|
||||
id: 'app.whiteboard.toolbar.tools.hand',
|
||||
description: 'presentation toolbar pan label',
|
||||
}
|
||||
});
|
||||
|
||||
class PresentationToolbar extends PureComponent {
|
||||
@ -257,6 +261,8 @@ class PresentationToolbar extends PureComponent {
|
||||
toolbarWidth,
|
||||
multiUserSize,
|
||||
multiUser,
|
||||
setIsPanning,
|
||||
isPanning,
|
||||
} = this.props;
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
@ -393,6 +399,20 @@ class PresentationToolbar extends PureComponent {
|
||||
/>
|
||||
</TooltipContainer>
|
||||
) : null}
|
||||
<Styled.FitToWidthButton
|
||||
role="button"
|
||||
data-test="panButton"
|
||||
aria-label={intl.formatMessage(intlMessages.pan)}
|
||||
color="light"
|
||||
disabled={zoom === HUNDRED_PERCENT}
|
||||
icon="hand"
|
||||
size="md"
|
||||
circle
|
||||
onClick={setIsPanning}
|
||||
label={intl.formatMessage(intlMessages.pan)}
|
||||
hideLabel
|
||||
panning={isPanning}
|
||||
/>
|
||||
<Styled.FitToWidthButton
|
||||
role="button"
|
||||
data-test="fitToWidthButton"
|
||||
|
@ -89,13 +89,9 @@ const PresentationSlideControls = styled.div`
|
||||
`;
|
||||
|
||||
const PrevSlideButton = styled(Button)`
|
||||
|
||||
i {
|
||||
padding-left: 20%;
|
||||
}
|
||||
|
||||
& > i {
|
||||
font-size: 1rem;
|
||||
padding-left: 20%;
|
||||
|
||||
[dir="rtl"] & {
|
||||
-webkit-transform: scale(-1, 1);
|
||||
@ -105,17 +101,12 @@ const PrevSlideButton = styled(Button)`
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
const NextSlideButton = styled(Button)`
|
||||
|
||||
i {
|
||||
padding-left: 60%;
|
||||
}
|
||||
|
||||
& > i {
|
||||
font-size: 1rem;
|
||||
padding-left: 60%;
|
||||
|
||||
[dir="rtl"] & {
|
||||
-webkit-transform: scale(-1, 1);
|
||||
@ -204,6 +195,12 @@ const FitToWidthButton = styled(Button)`
|
||||
background-color: ${colorOffWhite};
|
||||
border: 0;
|
||||
}
|
||||
|
||||
${({ panning }) => panning && `
|
||||
> span {
|
||||
background-color: #DCE4EC;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const MultiUserTool = styled.span`
|
||||
@ -213,7 +210,6 @@ const MultiUserTool = styled.span`
|
||||
height: 1rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
right: 1rem;
|
||||
bottom: 0.5rem;
|
||||
color: ${colorWhite};
|
||||
display: flex;
|
||||
@ -221,20 +217,35 @@ const MultiUserTool = styled.span`
|
||||
align-items: center;
|
||||
box-shadow: 1px 1px ${borderSizeLarge} ${colorGrayDark};
|
||||
font-size: ${smPaddingX};
|
||||
|
||||
[dir="ltr"] & {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const MUTPlaceholder = styled.div`
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
position: relative;
|
||||
right: 1rem;
|
||||
bottom: 0.5rem;
|
||||
|
||||
[dir="ltr"] & {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const WBAccessButton = styled(Button)`
|
||||
border: none !important;
|
||||
|
||||
& > i {
|
||||
i {
|
||||
font-size: 1.2rem;
|
||||
|
||||
[dir="rtl"] & {
|
||||
|
@ -27,6 +27,7 @@ import WebRtcPeer from '/imports/ui/services/webrtc-base/peer';
|
||||
// FIXME Remove hardcoded defaults 2.3.
|
||||
const WS_CONN_TIMEOUT = Meteor.settings.public.kurento.wsConnectionTimeout || 4000;
|
||||
|
||||
const { webcam: NETWORK_PRIORITY } = Meteor.settings.public.media.networkPriorities || {};
|
||||
const {
|
||||
baseTimeout: CAMERA_SHARE_FAILED_WAIT_TIME = 15000,
|
||||
maxTimeout: MAX_CAMERA_SHARE_FAILED_WAIT_TIME = 60000,
|
||||
@ -616,6 +617,7 @@ class VideoProvider extends Component {
|
||||
iceTransportPolicy: shouldForceRelay() ? 'relay' : undefined,
|
||||
},
|
||||
trace: TRACE_LOGS,
|
||||
networkPriorities: NETWORK_PRIORITY ? { video: NETWORK_PRIORITY } : undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -46,6 +46,7 @@ export default function Whiteboard(props) {
|
||||
zoomChanger,
|
||||
isMultiUserActive,
|
||||
isRTL,
|
||||
isPanning,
|
||||
fitToWidth,
|
||||
zoomValue,
|
||||
} = props;
|
||||
@ -61,6 +62,7 @@ export default function Whiteboard(props) {
|
||||
assets: {},
|
||||
});
|
||||
const [tldrawAPI, setTLDrawAPI] = React.useState(null);
|
||||
const [forcePanning, setForcePanning] = React.useState(false);
|
||||
const [zoom, setZoom] = React.useState(HUNDRED_PERCENT);
|
||||
const [isMounting, setIsMounting] = React.useState(true);
|
||||
const prevShapes = usePrevious(shapes);
|
||||
@ -219,8 +221,18 @@ export default function Whiteboard(props) {
|
||||
// removes image tool from the tldraw toolbar
|
||||
document.getElementById("TD-PrimaryTools-Image").style.display = 'none';
|
||||
}
|
||||
|
||||
if (tldrawAPI) {
|
||||
tldrawAPI.isForcePanning = isPanning;
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tldrawAPI) {
|
||||
tldrawAPI.isForcePanning = isPanning;
|
||||
}
|
||||
}, [isPanning]);
|
||||
|
||||
const onMount = (app) => {
|
||||
app.setSetting('language', document.getElementsByTagName('html')[0]?.lang || 'en');
|
||||
app.setSetting('dockPosition', isRTL ? 'left' : 'right');
|
||||
@ -306,7 +318,7 @@ export default function Whiteboard(props) {
|
||||
|
||||
const editableWB = (
|
||||
<Tldraw
|
||||
key={`wb-${document?.documentElement?.dir}-${document.getElementById('Navbar')?.style?.width}`}
|
||||
key={`wb-${document?.documentElement?.dir}-${document.getElementById('Navbar')?.style?.width}-${forcePanning}`}
|
||||
document={doc}
|
||||
// disable the ability to drag and drop files onto the whiteboard
|
||||
// until we handle saving of assets in akka.
|
||||
@ -521,6 +533,8 @@ export default function Whiteboard(props) {
|
||||
whiteboardId={whiteboardId}
|
||||
isViewersCursorLocked={isViewersCursorLocked}
|
||||
isMultiUserActive={isMultiUserActive}
|
||||
isPanning={isPanning}
|
||||
|
||||
>
|
||||
{hasWBAccess || isPresenter ? editableWB : readOnlyWB}
|
||||
</Cursors>
|
||||
|
@ -126,6 +126,7 @@ export default function Cursors(props) {
|
||||
hasMultiUserAccess,
|
||||
isMultiUserActive,
|
||||
application,
|
||||
isPanning,
|
||||
} = props;
|
||||
|
||||
const start = () => setActive(true);
|
||||
@ -260,10 +261,12 @@ export default function Cursors(props) {
|
||||
});
|
||||
|
||||
const multiUserAccess = hasMultiUserAccess(whiteboardId, currentUser?.userId);
|
||||
|
||||
let cursorType = multiUserAccess || currentUser?.presenter ? "none" : "default";
|
||||
if (isPanning) cursorType = 'grab';
|
||||
|
||||
return (
|
||||
<span ref={(r) => (cursorWrapper = r)}>
|
||||
<div style={{ height: "100%", cursor: multiUserAccess || currentUser?.presenter ? "none" : "default" }}>
|
||||
<div style={{ height: "100%", cursor: cursorType }}>
|
||||
{(active && multiUserAccess || (active && currentUser?.presenter)) && (
|
||||
<PositionLabel
|
||||
pos={pos}
|
||||
|
@ -218,8 +218,13 @@ const getMultiUserSize = (whiteboardId) => {
|
||||
const multiUserSize = Users.find(
|
||||
{
|
||||
meetingId: Auth.meetingID,
|
||||
userId: { $in: multiUser },
|
||||
presenter: false,
|
||||
$or: [
|
||||
{
|
||||
userId: { $in: multiUser },
|
||||
presenter: false,
|
||||
},
|
||||
{ presenter: true },
|
||||
],
|
||||
},
|
||||
{ fields: { userId: 1 } }
|
||||
).fetch();
|
||||
|
@ -28,6 +28,7 @@ class AudioBroker extends BaseBroker {
|
||||
// stream,
|
||||
// signalCandidates
|
||||
// traceLogs
|
||||
// networkPriority
|
||||
Object.assign(this, options);
|
||||
}
|
||||
|
||||
@ -82,6 +83,7 @@ class AudioBroker extends BaseBroker {
|
||||
this.onIceCandidate(candidate, this.role);
|
||||
},
|
||||
trace: this.traceLogs,
|
||||
networkPriorities: this.networkPriority ? { audio: this.networkPriority } : undefined,
|
||||
};
|
||||
|
||||
const peerRole = this.role === 'sendrecv' ? this.role : 'recvonly';
|
||||
|
@ -37,6 +37,7 @@ class ScreenshareBroker extends BaseBroker {
|
||||
// mediaServer,
|
||||
// signalCandidates,
|
||||
// traceLogs
|
||||
// networkPriority
|
||||
Object.assign(this, options);
|
||||
}
|
||||
|
||||
@ -187,6 +188,7 @@ class ScreenshareBroker extends BaseBroker {
|
||||
videoStream: this.stream,
|
||||
configuration: this.populatePeerConfiguration(),
|
||||
trace: this.traceLogs,
|
||||
networkPriorities: this.networkPriority ? { video: this.networkPriority } : undefined,
|
||||
};
|
||||
this.webRtcPeer = new WebRtcPeer('sendonly', options);
|
||||
this.webRtcPeer.iceQueue = [];
|
||||
|
@ -19,6 +19,12 @@ export default class WebRtcPeer extends EventEmitter2 {
|
||||
this.configuration = this.options.configuration;
|
||||
this.onicecandidate = this.options.onicecandidate;
|
||||
this.oncandidategatheringdone = this.options.oncandidategatheringdone;
|
||||
// this.networkPriorities: <{
|
||||
// audio: <'very-low' | 'low' | 'medium' | 'high' | undefined>
|
||||
// video: <'very-low' | 'low' | 'medium' | 'high' | undefined>
|
||||
// } | undefined >
|
||||
this.networkPriorities = this.options.networkPriorities;
|
||||
|
||||
this.candidateGatheringDone = false;
|
||||
|
||||
this._outboundCandidateQueue = [];
|
||||
@ -41,6 +47,40 @@ export default class WebRtcPeer extends EventEmitter2 {
|
||||
}
|
||||
}
|
||||
|
||||
_processEncodingOptions() {
|
||||
this.peerConnection?.getSenders().forEach((sender) => {
|
||||
const { track } = sender;
|
||||
if (track) {
|
||||
// TODO: this is not ideal and a bit anti-spec. The correct thing to do
|
||||
// would be to set this in the transceiver creation via sendEncodings in
|
||||
// addTransceiver, but FF doesn't support that. So we should split this
|
||||
// between Chromium/WebKit (addTransceiver) and FF (this way) later - prlanzarin
|
||||
const parameters = sender.getParameters();
|
||||
// The encoder parameters might not be up yet; if that's the case,
|
||||
// add a filler object so we can alter the parameters anyways
|
||||
if (parameters.encodings == null || parameters.encodings.length === 0) {
|
||||
parameters.encodings = [{}];
|
||||
}
|
||||
|
||||
parameters.encodings.forEach((encoding) => {
|
||||
// networkPriority
|
||||
if (this.networkPriorities && this.networkPriorities[track.kind]) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
encoding.networkPriority = this.networkPriorities[track.kind];
|
||||
}
|
||||
|
||||
// Add further custom encoding parameters here
|
||||
});
|
||||
|
||||
try {
|
||||
sender.setParameters(parameters);
|
||||
} catch (error) {
|
||||
this.logger.error('BBB::WebRtcPeer::_processEncodingOptions - setParameters failed', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_flushInboundCandidateQueue() {
|
||||
while (this._inboundCandidateQueue.length) {
|
||||
const entry = this._inboundCandidateQueue.shift();
|
||||
@ -282,6 +322,7 @@ export default class WebRtcPeer extends EventEmitter2 {
|
||||
return this.peerConnection.setLocalDescription(offer);
|
||||
})
|
||||
.then(() => {
|
||||
this._processEncodingOptions();
|
||||
const localDescription = this.getLocalSessionDescriptor();
|
||||
this.logger.debug('BBB::WebRtcPeer::generateOffer - local description set', localDescription);
|
||||
return localDescription.sdp;
|
||||
|
@ -621,6 +621,13 @@ public:
|
||||
useRtcLoopbackInChromium: true
|
||||
# showVolumeMeter: shows an energy bar for microphones in the AudioSettings view
|
||||
showVolumeMeter: true
|
||||
# networkPriorities: DSCP markings for each media type. Chromium only, applies
|
||||
# to sender flows. See https://datatracker.ietf.org/doc/html/rfc8837#section-5
|
||||
# for further info.
|
||||
#networkPriorities:
|
||||
# audio: high
|
||||
# webcam: medium
|
||||
# screenshare: medium
|
||||
stats:
|
||||
enabled: true
|
||||
interval: 10000
|
||||
|
Loading…
Reference in New Issue
Block a user