Merge pull request #17746 from Carloshsc/port-present-webcam-27

feat(camera as content): present webcam
This commit is contained in:
Anton Georgiev 2023-05-26 08:01:40 -04:00 committed by GitHub
commit 1a1f442d5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 600 additions and 194 deletions

View File

@ -91,6 +91,14 @@ object ScreenshareModel {
def getHasAudio(status: ScreenshareModel): Boolean = {
status.hasAudio
}
def setContentType(status: ScreenshareModel, contentType: String): Unit = {
status.contentType = contentType
}
def getContentType(status: ScreenshareModel): String = {
status.contentType
}
}
class ScreenshareModel {
@ -103,4 +111,5 @@ class ScreenshareModel {
private var screenshareConf: String = ""
private var timestamp: String = ""
private var hasAudio = false
private var contentType = "camera"
}

View File

@ -26,9 +26,10 @@ trait GetScreenshareStatusReqMsgHdlr {
val vidHeight = ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel)
val timestamp = ScreenshareModel.getTimestamp(liveMeeting.screenshareModel)
val hasAudio = ScreenshareModel.getHasAudio(liveMeeting.screenshareModel)
val contentType = ScreenshareModel.getContentType(liveMeeting.screenshareModel)
val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf,
stream, vidWidth, vidHeight, timestamp, hasAudio)
stream, vidWidth, vidHeight, timestamp, hasAudio, contentType)
val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}

View File

@ -10,7 +10,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
def handle(msg: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastEvent(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String, hasAudio: Boolean): BbbCommonEnvCoreMsg = {
timestamp: String, hasAudio: Boolean, contentType: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
@ -23,7 +23,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
)
val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf,
stream, vidWidth, vidHeight, timestamp, hasAudio)
stream, vidWidth, vidHeight, timestamp, hasAudio, contentType)
val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
@ -45,12 +45,13 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf)
ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp)
ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio)
ScreenshareModel.setContentType(liveMeeting.screenshareModel, msg.body.contentType)
log.info("START broadcast ALLOWED when isBroadcastingRTMP=false")
// Notify viewers in the meeting that there's an rtmp stream to view
val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio)
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio, msg.body.contentType)
bus.outGW.send(msgEvent)
} else {
log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true")

View File

@ -27,7 +27,8 @@ trait SyncGetScreenshareInfoRespMsgHdlr {
ScreenshareModel.getScreenshareVideoWidth(liveMeeting.screenshareModel),
ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel),
ScreenshareModel.getTimestamp(liveMeeting.screenshareModel),
ScreenshareModel.getHasAudio(liveMeeting.screenshareModel)
ScreenshareModel.getHasAudio(liveMeeting.screenshareModel),
ScreenshareModel.getContentType(liveMeeting.screenshareModel)
)
val event = SyncGetScreenshareInfoRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)

View File

@ -17,7 +17,7 @@ case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg(
extends VoiceStandardMsg
case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf: String, screenshareConf: String,
stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String, hasAudio: Boolean)
timestamp: String, hasAudio: Boolean, contentType: String)
/**
* Sent to clients to notify them of an RTMP stream starting.
@ -30,7 +30,7 @@ case class ScreenshareRtmpBroadcastStartedEvtMsg(
extends BbbCoreMsg
case class ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf: String, screenshareConf: String,
stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String, hasAudio: Boolean)
timestamp: String, hasAudio: Boolean, contentType: String)
/**
* Sync screenshare state with bbb-html5
@ -48,7 +48,8 @@ case class SyncGetScreenshareInfoRespMsgBody(
vidWidth: Int,
vidHeight: Int,
timestamp: String,
hasAudio: Boolean
hasAudio: Boolean,
contentType: String
)
/**

View File

@ -2,7 +2,7 @@ import Auth from '/imports/ui/services/auth';
import logger from '/imports/startup/client/logger';
import BridgeService from './service';
import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker';
import { setSharingScreen, screenShareEndAlert } from '/imports/ui/components/screenshare/service';
import { setIsSharing, screenShareEndAlert } from '/imports/ui/components/screenshare/service';
import { SCREENSHARING_ERRORS } from './errors';
import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils';
import MediaStreamUtils from '/imports/utils/media-stream-utils';
@ -292,7 +292,7 @@ export default class KurentoScreenshareBridge {
screenShareEndAlert();
}
share(stream, onFailure) {
share(stream, onFailure, contentType) {
return new Promise(async (resolve, reject) => {
this.onerror = onFailure;
this.connectionAttempts += 1;
@ -322,6 +322,7 @@ export default class KurentoScreenshareBridge {
userName: Auth.fullname,
stream,
hasAudio: this.hasAudio,
contentType: contentType,
bitrate: BridgeService.BASE_BITRATE,
offering: true,
mediaServer: BridgeService.getMediaServerAdapter(),
@ -365,7 +366,7 @@ export default class KurentoScreenshareBridge {
// component tracker to be extra sure we won't have any client-side state
// inconsistency - prlanzarin
if (this.broker && this.broker.role === SEND_ROLE && !this.reconnecting) {
setSharingScreen(false);
setIsSharing(false);
}
this.broker = null;
}

View File

@ -10,8 +10,9 @@ import Styled from './styles';
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
import { PANELS, ACTIONS, LAYOUT_TYPE } from '../../layout/enums';
import { uniqueId } from '/imports/utils/string-utils';
import { isPresentationEnabled } from '/imports/ui/services/features';
import {isLayoutsEnabled} from '/imports/ui/services/features';
import { isPresentationEnabled, isLayoutsEnabled } from '/imports/ui/services/features';
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
const propTypes = {
amIPresenter: PropTypes.bool.isRequired,
@ -95,6 +96,14 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.actionsDropdown.layoutModal',
description: 'Label for layouts selection button',
},
shareCameraAsContent: {
id: 'app.actionsBar.actionsDropdown.shareCameraAsContent',
description: 'Label for share camera as content',
},
unshareCameraAsContent: {
id: 'app.actionsBar.actionsDropdown.unshareCameraAsContent',
description: 'Label for unshare camera as content',
},
});
const handlePresentationClick = () => Session.set('showUploadPresentationView', true);
@ -110,14 +119,18 @@ class ActionsDropdown extends PureComponent {
this.state = {
isExternalVideoModalOpen: false,
isRandomUserSelectModalOpen: false,
isLayoutModalOpen: false,
}
isLayoutModalOpen: false,
isCameraAsContentModalOpen: false,
};
this.handleExternalVideoClick = this.handleExternalVideoClick.bind(this);
this.makePresentationItems = this.makePresentationItems.bind(this);
this.setExternalVideoModalIsOpen = this.setExternalVideoModalIsOpen.bind(this);
this.setRandomUserSelectModalIsOpen = this.setRandomUserSelectModalIsOpen.bind(this);
this.setLayoutModalIsOpen = this.setLayoutModalIsOpen.bind(this);
this.setCameraAsContentModalIsOpen = this.setCameraAsContentModalIsOpen.bind(this);
this.setPropsToPassModal = this.setPropsToPassModal.bind(this);
this.setForceOpen = this.setForceOpen.bind(this);
}
componentDidUpdate(prevProps) {
@ -147,6 +160,9 @@ class ActionsDropdown extends PureComponent {
setPushLayout,
showPushLayout,
amIModerator,
isMobile,
hasCameraAsContent,
isCameraAsContentEnabled,
} = this.props;
const {
@ -244,7 +260,23 @@ class ActionsDropdown extends PureComponent {
dataTest: 'layoutModal',
});
}
if (isCameraAsContentEnabled && amIPresenter && !isMobile) {
actions.push({
icon: hasCameraAsContent ? 'video_off' : 'video',
label: hasCameraAsContent
? intl.formatMessage(intlMessages.unshareCameraAsContent)
: intl.formatMessage(intlMessages.shareCameraAsContent),
key: 'camera as content',
onClick: hasCameraAsContent
? screenshareHasEnded
: () => {
screenshareHasEnded();
this.setCameraAsContentModalIsOpen(true);
},
});
}
return actions;
}
@ -294,6 +326,15 @@ class ActionsDropdown extends PureComponent {
setLayoutModalIsOpen(value) {
this.setState({isLayoutModalOpen: value});
}
setCameraAsContentModalIsOpen(value) {
this.setState({isCameraAsContentModalOpen: value});
}
setPropsToPassModal(value) {
this.setState({propsToPassModal: value});
}
setForceOpen(value){
this.setState({forceOpen: value});
}
renderModal(isOpen, setIsOpen, priority, Component) {
return isOpen ? <Component
@ -316,10 +357,15 @@ class ActionsDropdown extends PureComponent {
isMobile,
isRTL,
isSelectRandomUserEnabled,
propsToPassModal,
} = this.props;
const { isExternalVideoModalOpen,
isRandomUserSelectModalOpen, isLayoutModalOpen } = this.state;
const {
isExternalVideoModalOpen,
isRandomUserSelectModalOpen,
isLayoutModalOpen,
isCameraAsContentModalOpen,
} = this.state;
const availableActions = this.getAvailableActions();
const availablePresentations = this.makePresentationItems();
@ -368,8 +414,25 @@ class ActionsDropdown extends PureComponent {
ExternalVideoModal)}
{(amIPresenter && isSelectRandomUserEnabled) ? this.renderModal(isRandomUserSelectModalOpen, this.setRandomUserSelectModalIsOpen,
"low", RandomUserSelectContainer) : null }
{this.renderModal(isLayoutModalOpen, this.setLayoutModalIsOpen,
{this.renderModal(isLayoutModalOpen, this.setLayoutModalIsOpen,
"low", LayoutModalContainer)}
{this.renderModal(isCameraAsContentModalOpen, this.setCameraAsContentModalIsOpen,
'low', () => (
<VideoPreviewContainer
cameraAsContent
amIPresenter
{...{
callbackToClose: () => {
this.setPropsToPassModal({});
this.setForceOpen(false);
},
priority: 'low',
setIsOpen: this.setCameraAsContentModalIsOpen,
isOpen: isCameraAsContentModalOpen,
}}
{...propsToPassModal}
/>
))}
</>
);
}

View File

@ -28,6 +28,8 @@ const ActionsDropdownContainer = (props) => {
);
};
const ENABLE_CAMERA_AS_CONTENT = Meteor.settings.public.app.enableCameraAsContent;
export default withTracker(() => {
const presentations = Presentations.find({ 'conversion.done': true }).fetch();
return ({
@ -35,5 +37,6 @@ export default withTracker(() => {
isDropdownOpen: Session.get('dropdownOpen'),
setPresentation: PresentationUploaderService.setPresentation,
podIds: PresentationPodService.getPresentationPodIds(),
isCameraAsContentEnabled: ENABLE_CAMERA_AS_CONTENT,
});
})(ActionsDropdownContainer);

View File

@ -39,6 +39,8 @@ class ActionsBar extends PureComponent {
isSharingVideo,
isSharedNotesPinned,
hasScreenshare,
hasGenericContent,
hasCameraAsContent,
stopExternalVideoShare,
isCaptionsAvailable,
isMeteorConnected,
@ -84,6 +86,7 @@ class ActionsBar extends PureComponent {
setPushLayout,
presentationIsOpen,
showPushLayout,
hasCameraAsContent,
}}
/>
{isCaptionsAvailable
@ -133,6 +136,8 @@ class ActionsBar extends PureComponent {
hasExternalVideo={isSharingVideo}
hasScreenshare={hasScreenshare}
hasPinnedSharedNotes={isSharedNotesPinned}
hasGenericContent={hasGenericContent}
hasCameraAsContent={hasCameraAsContent}
/>
: null
}

View File

@ -15,6 +15,7 @@ import CaptionsService from '/imports/ui/components/captions/service';
import { layoutSelectOutput, layoutDispatch } from '../layout/context';
import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
import { isExternalVideoEnabled, isPollingEnabled, isPresentationEnabled } from '/imports/ui/services/features';
import { isScreenBroadcasting, isCameraAsContentBroadcasting } from '/imports/ui/components/screenshare/service';
import MediaService from '../media/service';
@ -58,7 +59,8 @@ export default withTracker(() => ({
parseCurrentSlideContent: PresentationService.parseCurrentSlideContent,
isSharingVideo: Service.isSharingVideo(),
isSharedNotesPinned: Service.isSharedNotesPinned(),
hasScreenshare: isVideoBroadcasting(),
hasScreenshare: isScreenBroadcasting(),
hasCameraAsContent: isCameraAsContentBroadcasting(),
isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
isMeteorConnected: Meteor.status().connected,
isPollingEnabled: isPollingEnabled() && isPresentationEnabled(),

View File

@ -39,6 +39,8 @@ const PresentationOptionsContainer = ({
hasExternalVideo,
hasScreenshare,
hasPinnedSharedNotes,
hasGenericContent,
hasCameraAsContent,
}) => {
let buttonType = 'presentation';
if (hasExternalVideo) {
@ -46,10 +48,13 @@ const PresentationOptionsContainer = ({
buttonType = 'external-video';
} else if (hasScreenshare) {
buttonType = 'desktop';
} else if (hasCameraAsContent) {
buttonType = 'video';
}
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare
|| hasPresentation || hasPinnedSharedNotes;
|| hasPresentation || hasPinnedSharedNotes
|| hasGenericContent || hasCameraAsContent;
return (
<Button
icon={`${buttonType}${!presentationIsOpen ? '_off' : ''}`}

View File

@ -22,7 +22,7 @@ const propTypes = {
intl: PropTypes.objectOf(Object).isRequired,
enabled: PropTypes.bool.isRequired,
amIPresenter: PropTypes.bool.isRequired,
isVideoBroadcasting: PropTypes.bool.isRequired,
isScreenBroadcasting: PropTypes.bool.isRequired,
isMeteorConnected: PropTypes.bool.isRequired,
screenshareDataSavingSetting: PropTypes.bool.isRequired,
};
@ -114,7 +114,7 @@ const getErrorLocale = (errorCode) => {
const ScreenshareButton = ({
intl,
enabled,
isVideoBroadcasting,
isScreenBroadcasting,
amIPresenter,
isMeteorConnected,
}) => {
@ -155,34 +155,35 @@ const ScreenshareButton = ({
const screenshareLabel = intlMessages.desktopShareLabel;
const vLabel = isVideoBroadcasting
const vLabel = isScreenBroadcasting
? intlMessages.stopDesktopShareLabel : screenshareLabel;
const vDescr = isVideoBroadcasting
const vDescr = isScreenBroadcasting
? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
const amIBroadcasting = isScreenBroadcasting && amIPresenter;
const shouldAllowScreensharing = enabled
&& ( !isMobile || isTabletApp)
&& amIPresenter;
const dataTest = isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare';
const dataTest = isScreenBroadcasting ? 'stopScreenShare' : 'startScreenShare';
return <>
{
shouldAllowScreensharing
? (
<Button
disabled={(!isMeteorConnected && !isVideoBroadcasting)}
icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
disabled={(!isMeteorConnected && !isScreenBroadcasting)}
icon={amIBroadcasting ? 'desktop' : 'desktop_off'}
data-test={dataTest}
label={intl.formatMessage(vLabel)}
description={intl.formatMessage(vDescr)}
color={isVideoBroadcasting ? 'primary' : 'default'}
ghost={!isVideoBroadcasting}
color={amIBroadcasting ? 'primary' : 'default'}
ghost={!amIBroadcasting}
hideLabel
circle
size="lg"
onClick={isVideoBroadcasting
onClick={amIBroadcasting
? screenshareHasEnded
: () => {
if (isSafari && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) {
@ -191,7 +192,7 @@ const ScreenshareButton = ({
shareScreen(amIPresenter, handleFailure);
}
}}
id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
id={amIBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
/>
) : null
}

View File

@ -3,7 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
import ScreenshareButton from './component';
import { isScreenSharingEnabled } from '/imports/ui/services/features';
import {
isVideoBroadcasting,
isScreenBroadcasting,
dataSavingSetting,
} from '/imports/ui/components/screenshare/service';
@ -11,14 +11,14 @@ const ScreenshareButtonContainer = (props) => <ScreenshareButton {...props} />;
/*
* All props, including the ones that are inherited from actions-bar
* isVideoBroadcasting,
* isScreenBroadcasting,
* amIPresenter,
* screenSharingCheck,
* isMeteorConnected,
* screenshareDataSavingSetting,
*/
export default withTracker(() => ({
isVideoBroadcasting: isVideoBroadcasting(),
isScreenBroadcasting: isScreenBroadcasting(),
screenshareDataSavingSetting: dataSavingSetting(),
enabled: isScreenSharingEnabled(),
}))(ScreenshareButtonContainer);

View File

@ -1,4 +1,5 @@
import Presentations from '/imports/api/presentations';
import { isScreenBroadcasting, isCameraAsContentBroadcasting } from '/imports/ui/components/screenshare/service';
import Settings from '/imports/ui/services/settings';
import getFromUserSettings from '/imports/ui/services/users-settings';
import { isExternalVideoEnabled, isScreenSharingEnabled } from '/imports/ui/services/features';
@ -31,7 +32,8 @@ function shouldShowWhiteboard() {
function shouldShowScreenshare() {
const { viewScreenshare } = Settings.dataSaving;
return isScreenSharingEnabled() && (viewScreenshare || UserService.isUserPresenter()) && isVideoBroadcasting();
return isScreenSharingEnabled() && (viewScreenshare || UserService.isUserPresenter())
&& (isScreenBroadcasting() || isCameraAsContentBroadcasting());
}
function shouldShowExternalVideo() {
@ -53,7 +55,6 @@ const setPresentationIsOpen = (layoutContextDispatch, value) => {
});
};
const isThereWebcamOn = (meetingID) => {
return VideoStreams.find({
meetingId: meetingID
@ -63,9 +64,8 @@ const isThereWebcamOn = (meetingID) => {
const buildLayoutWhenPresentationAreaIsDisabled = (layoutContextDispatch) => {
const isSharingVideo = getVideoUrl();
const isSharedNotesPinned = NotesService.isSharedNotesPinned();
const hasScreenshare = isVideoBroadcasting();
const hasScreenshare = isScreenSharingEnabled();
const isThereWebcam = isThereWebcamOn(Auth.meetingID);
const isGeneralMediaOff = !hasScreenshare && !isSharedNotesPinned && !isSharingVideo
const webcamIsOnlyContent = isThereWebcam && isGeneralMediaOff;
const isThereNoMedia = !isThereWebcam && isGeneralMediaOff;
@ -84,7 +84,8 @@ export default {
shouldShowScreenshare,
shouldShowExternalVideo,
shouldShowOverlay,
isVideoBroadcasting,
isScreenBroadcasting,
isCameraAsContentBroadcasting,
setPresentationIsOpen,
shouldShowSharedNotes,
};

View File

@ -1,5 +1,5 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { debounce } from 'radash';
import FullscreenButtonContainer from '/imports/ui/components/common/fullscreen-button/container';
@ -32,40 +32,6 @@ import Settings from '/imports/ui/services/settings';
import deviceInfo from '/imports/utils/deviceInfo';
import { uniqueId } from '/imports/utils/string-utils';
const intlMessages = defineMessages({
screenShareLabel: {
id: 'app.screenshare.screenShareLabel',
description: 'screen share area element label',
},
presenterLoadingLabel: {
id: 'app.screenshare.presenterLoadingLabel',
},
viewerLoadingLabel: {
id: 'app.screenshare.viewerLoadingLabel',
},
presenterSharingLabel: {
id: 'app.screenshare.presenterSharingLabel',
},
autoplayBlockedDesc: {
id: 'app.media.screenshare.autoplayBlockedDesc',
},
autoplayAllowLabel: {
id: 'app.media.screenshare.autoplayAllowLabel',
},
screenshareStarted: {
id: 'app.media.screenshare.start',
description: 'toast to show when a screenshare has started',
},
screenshareEnded: {
id: 'app.media.screenshare.end',
description: 'toast to show when a screenshare has ended',
},
screenshareEndedDueToDataSaving: {
id: 'app.media.screenshare.endDueToDataSaving',
description: 'toast to show when a screenshare has ended by changing data savings option',
},
});
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
const MOBILE_HOVER_TIMEOUT = 5000;
const MEDIA_FLOW_PROBE_INTERVAL = 500;
@ -80,7 +46,7 @@ class ScreenshareComponent extends React.Component {
);
}
constructor() {
constructor(props) {
super();
this.state = {
loaded: false,
@ -100,11 +66,17 @@ class ScreenshareComponent extends React.Component {
this.onStreamStateChange = this.onStreamStateChange.bind(this);
this.onSwitched = this.onSwitched.bind(this);
this.handleOnVolumeChanged = this.handleOnVolumeChanged.bind(this);
this.dispatchScreenShareSize = this.dispatchScreenShareSize.bind(this);
this.handleOnMuted = this.handleOnMuted.bind(this);
this.debouncedDispatchScreenShareSize = debounce(
{ delay: SCREEN_SIZE_DISPATCH_INTERVAL },
this.dispatchScreenShareSize
this.dispatchScreenShareSize,
);
const { locales, icon } = props;
this.locales = locales;
this.icon = icon;
this.volume = getVolume();
this.mobileHoverSetTimeout = null;
this.mediaFlowMonitor = null;
@ -116,6 +88,7 @@ class ScreenshareComponent extends React.Component {
layoutContextDispatch,
intl,
isPresenter,
startPreviewSizeBig,
} = this.props;
screenshareHasStarted(isPresenter);
@ -126,7 +99,9 @@ class ScreenshareComponent extends React.Component {
// Attaches the local stream if it exists to serve as the local presenter preview
attachLocalPreviewStream(getMediaElement());
notify(intl.formatMessage(intlMessages.screenshareStarted), 'info', 'desktop');
this.setState({ switched: startPreviewSizeBig });
notify(intl.formatMessage(this.locales.started), 'info', this.icon);
layoutContextDispatch({
type: ACTIONS.SET_HAS_SCREEN_SHARE,
@ -160,9 +135,9 @@ class ScreenshareComponent extends React.Component {
unsubscribeFromStreamStateChange('screenshare', this.onStreamStateChange);
if (Settings.dataSaving.viewScreenshare) {
notify(intl.formatMessage(intlMessages.screenshareEnded), 'info', 'desktop');
notify(intl.formatMessage(this.locales.ended), 'info', this.icon);
} else {
notify(intl.formatMessage(intlMessages.screenshareEndedDueToDataSaving), 'info', 'desktop');
notify(intl.formatMessage(this.locales.endedDueToDataSaving), 'info', this.icon);
}
layoutContextDispatch({
@ -274,7 +249,7 @@ class ScreenshareComponent extends React.Component {
height,
browserWidth: window.innerWidth,
browserHeight: window.innerHeight,
}
};
layoutContextDispatch({
type: ACTIONS.SET_SCREEN_SHARE_SIZE,
@ -282,12 +257,6 @@ class ScreenshareComponent extends React.Component {
});
}
onVideoResize() {
// Debounced version of the dispatcher to pace things out - we don't want
// to hog the CPU just for resize recalculations...
this.debouncedDispatchScreenShareSize();
}
onLoadedMetadata() {
const element = getMediaElement();
@ -322,6 +291,12 @@ class ScreenshareComponent extends React.Component {
}
}
onVideoResize() {
// Debounced version of the dispatcher to pace things out - we don't want
// to hog the CPU just for resize recalculations...
this.debouncedDispatchScreenShareSize();
}
onStreamStateChange(event) {
const { streamState } = event.detail;
const { mediaFlowing } = this.state;
@ -343,8 +318,8 @@ class ScreenshareComponent extends React.Component {
return (
<FullscreenButtonContainer
key={uniqueId('fullscreenButton-')}
elementName={intl.formatMessage(intlMessages.screenShareLabel)}
key={_.uniqueId('fullscreenButton-')}
elementName={intl.formatMessage(this.locales.label)}
fullscreenRef={this.screenshareContainer}
elementId={fullscreenElementId}
isFullscreen={fullscreenContext}
@ -358,17 +333,20 @@ class ScreenshareComponent extends React.Component {
return (
<AutoplayOverlay
key={uniqueId('screenshareAutoplayOverlay')}
autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)}
autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)}
key={_.uniqueId('screenshareAutoplayOverlay')}
autoplayBlockedDesc={intl.formatMessage(this.locales.autoplayBlockedDesc)}
autoplayAllowLabel={intl.formatMessage(this.locales.autoplayAllowLabel)}
handleAllowAutoplay={this.handleAllowAutoplay}
/>
);
}
renderSwitchButton() {
const { showSwitchPreviewSizeButton } = this.props;
const { switched } = this.state;
if (!showSwitchPreviewSizeButton) return null;
return (
<SwitchButtonContainer
handleSwitch={this.onSwitched}
@ -468,12 +446,12 @@ class ScreenshareComponent extends React.Component {
<div data-test="isSharingScreen">
{!switched
&& ScreenshareComponent.renderScreenshareContainerInside(
intl.formatMessage(intlMessages.presenterSharingLabel),
intl.formatMessage(this.locales.presenterSharingLabel),
)}
</div>
)
: ScreenshareComponent.renderScreenshareContainerInside(
intl.formatMessage(intlMessages.presenterLoadingLabel),
intl.formatMessage(this.locales.presenterLoadingLabel),
)
}
</Styled.ScreenshareContainer>
@ -501,7 +479,7 @@ class ScreenshareComponent extends React.Component {
{
!loaded
? ScreenshareComponent.renderScreenshareContainerInside(
intl.formatMessage(intlMessages.viewerLoadingLabel),
intl.formatMessage(this.locales.viewerLoadingLabel),
)
: null
}
@ -580,5 +558,6 @@ ScreenshareComponent.propTypes = {
formatMessage: PropTypes.func.isRequired,
}).isRequired,
isPresenter: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
enableVolumeControl: PropTypes.bool.isRequired,
};

View File

@ -2,8 +2,12 @@ import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import {
isVideoBroadcasting,
isGloballyBroadcasting,
getSharingContentType,
getBroadcastContentType,
isScreenGloballyBroadcasting,
isCameraAsContentGloballyBroadcasting,
isScreenBroadcasting,
isCameraAsContentBroadcasting,
} from './service';
import ScreenshareComponent from './component';
import { layoutSelect, layoutSelectOutput, layoutDispatch } from '../layout/context';
@ -11,6 +15,77 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import { shouldEnableVolumeControl } from './service';
import MediaService from '/imports/ui/components/media/service';
import { defineMessages } from 'react-intl';
const screenshareIntlMessages = defineMessages({
// SCREENSHARE
label: {
id: 'app.screenshare.screenShareLabel',
description: 'screen share area element label',
},
presenterLoadingLabel: {
id: 'app.screenshare.presenterLoadingLabel',
},
viewerLoadingLabel: {
id: 'app.screenshare.viewerLoadingLabel',
},
presenterSharingLabel: {
id: 'app.screenshare.presenterSharingLabel',
},
autoplayBlockedDesc: {
id: 'app.media.screenshare.autoplayBlockedDesc',
},
autoplayAllowLabel: {
id: 'app.media.screenshare.autoplayAllowLabel',
},
started: {
id: 'app.media.screenshare.start',
description: 'toast to show when a screenshare has started',
},
ended: {
id: 'app.media.screenshare.end',
description: 'toast to show when a screenshare has ended',
},
endedDueToDataSaving: {
id: 'app.media.screenshare.endDueToDataSaving',
description: 'toast to show when a screenshare has ended by changing data savings option',
}
});
const cameraAsContentIntlMessages = defineMessages({
// CAMERA AS CONTENT
label: {
id: 'app.cameraAsContent.cameraAsContentLabel',
description: 'screen share area element label',
},
presenterLoadingLabel: {
id: 'app.cameraAsContent.presenterLoadingLabel',
},
viewerLoadingLabel: {
id: 'app.cameraAsContent.viewerLoadingLabel',
},
presenterSharingLabel: {
id: 'app.cameraAsContent.presenterSharingLabel',
},
autoplayBlockedDesc: {
id: 'app.media.cameraAsContent.autoplayBlockedDesc',
},
autoplayAllowLabel: {
id: 'app.media.cameraAsContent.autoplayAllowLabel',
},
started: {
id: 'app.media.cameraAsContent.start',
description: 'toast to show when camera as content has started',
},
ended: {
id: 'app.media.cameraAsContent.end',
description: 'toast to show when camera as content has ended',
},
endedDueToDataSaving: {
id: 'app.media.cameraAsContent.endDueToDataSaving',
description: 'toast to show when camera as content has ended by changing data savings option',
},
});
const ScreenshareContainer = (props) => {
const screenShare = layoutSelectOutput((i) => i.screenShare);
@ -26,7 +101,29 @@ const ScreenshareContainer = (props) => {
const currentUser = users[Auth.meetingID][Auth.userID];
const isPresenter = currentUser.presenter;
if (isVideoBroadcasting()) {
const info = {
screenshare: {
icon: "desktop",
locales: screenshareIntlMessages,
startPreviewSizeBig: false,
showSwitchPreviewSizeButton: true,
},
camera: {
icon: "video",
locales: cameraAsContentIntlMessages,
startPreviewSizeBig: true,
showSwitchPreviewSizeButton: false,
},
};
const getContentType = () => {
return isPresenter ? getSharingContentType() : getBroadcastContentType();
}
const contentTypeInfo = info[getContentType()];
const defaultInfo = info.camera;
const selectedInfo = contentTypeInfo ? contentTypeInfo : defaultInfo;
if (isScreenBroadcasting() || isCameraAsContentBroadcasting()) {
return (
<ScreenshareComponent
{
@ -37,6 +134,7 @@ const ScreenshareContainer = (props) => {
fullscreenContext,
fullscreenElementId,
isPresenter,
...selectedInfo,
}
}
/>
@ -49,7 +147,7 @@ const LAYOUT_CONFIG = Meteor.settings.public.layout;
export default withTracker(() => {
return {
isGloballyBroadcasting: isGloballyBroadcasting(),
isGloballyBroadcasting: isScreenGloballyBroadcasting() || isCameraAsContentGloballyBroadcasting(),
toggleSwapLayout: MediaService.toggleSwapLayout,
hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin),
enableVolumeControl: shouldEnableVolumeControl(),

View File

@ -21,21 +21,58 @@ const DEFAULT_SCREENSHARE_STATS_TYPES = [
'inbound-rtp',
];
const CONTENT_TYPE_CAMERA = "camera";
const CONTENT_TYPE_SCREENSHARE = "screenshare";
let _isSharingScreen = false;
const _sharingScreenDep = {
const _isSharingDep = {
value: false,
tracker: new Tracker.Dependency(),
};
const isSharingScreen = () => {
_sharingScreenDep.tracker.depend();
return _sharingScreenDep.value;
const _sharingContentTypeDep = {
value: false,
tracker: new Tracker.Dependency(),
};
const setSharingScreen = (isSharingScreen) => {
if (_sharingScreenDep.value !== isSharingScreen) {
_sharingScreenDep.value = isSharingScreen;
_sharingScreenDep.tracker.changed();
const _cameraAsContentDeviceIdTypeDep = {
value: '',
tracker: new Tracker.Dependency(),
};
const isSharing = () => {
_isSharingDep.tracker.depend();
return _isSharingDep.value;
};
const setIsSharing = (isSharing) => {
if (_isSharingDep.value !== isSharing) {
_isSharingDep.value = isSharing;
_isSharingDep.tracker.changed();
}
};
const setSharingContentType = (contentType) => {
if (_sharingContentTypeDep.value !== contentType) {
_sharingContentTypeDep.value = contentType;
_sharingContentTypeDep.tracker.changed();
}
}
const getSharingContentType = () => {
_sharingContentTypeDep.tracker.depend();
return _sharingContentTypeDep.value;
};
const getCameraAsContentDeviceId = () => {
_cameraAsContentDeviceIdTypeDep.tracker.depend();
return _cameraAsContentDeviceIdTypeDep.value;
};
const setCameraAsContentDeviceId = (deviceId) => {
if (_cameraAsContentDeviceIdTypeDep.value !== deviceId) {
_cameraAsContentDeviceIdTypeDep.value = deviceId;
_cameraAsContentDeviceIdTypeDep.tracker.changed();
}
};
@ -65,32 +102,58 @@ const _handleStreamTermination = () => {
screenshareHasEnded();
};
// A simplified, trackable version of isVideoBroadcasting that DOES NOT
// A simplified, trackable version of isScreenBroadcasting that DOES NOT
// account for the presenter's local sharing state.
// It reflects the GLOBAL screen sharing state (akka-apps)
const isGloballyBroadcasting = () => {
const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID },
const isScreenGloballyBroadcasting = () => {
const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID, "screenshare.contentType": CONTENT_TYPE_SCREENSHARE },
{ fields: { 'screenshare.stream': 1 } });
return (!screenshareEntry ? false : !!screenshareEntry.screenshare.stream);
}
// A simplified, trackable version of isCameraContentBroadcasting that DOES NOT
// account for the presenter's local sharing state.
// It reflects the GLOBAL camera as content sharing state (akka-apps)
const isCameraAsContentGloballyBroadcasting = () => {
const cameraAsContentEntry = Screenshare.findOne({ meetingId: Auth.meetingID, "screenshare.contentType": CONTENT_TYPE_CAMERA },
{ fields: { 'screenshare.stream': 1 } });
return (!cameraAsContentEntry ? false : !!cameraAsContentEntry.screenshare.stream);
}
// when the meeting information has been updated check to see if it was
// screensharing. If it has changed either trigger a call to receive video
// and display it, or end the call and hide the video
const isVideoBroadcasting = () => {
const sharing = isSharingScreen();
const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID },
const isScreenBroadcasting = () => {
const sharing = isSharing() && getSharingContentType() == CONTENT_TYPE_SCREENSHARE;
const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID, "screenshare.contentType": CONTENT_TYPE_SCREENSHARE },
{ fields: { 'screenshare.stream': 1 } });
const screenIsShared = !screenshareEntry ? false : !!screenshareEntry.screenshare.stream;
if (screenIsShared && isSharingScreen) {
setSharingScreen(false);
if (screenIsShared && isSharing) {
setIsSharing(false);
}
return sharing || screenIsShared;
};
// when the meeting information has been updated check to see if it was
// sharing camera as content. If it has changed either trigger a call to receive video
// and display it, or end the call and hide the video
const isCameraAsContentBroadcasting = () => {
const sharing = isSharing() && getSharingContentType() === CONTENT_TYPE_CAMERA;
const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID, "screenshare.contentType": CONTENT_TYPE_CAMERA },
{ fields: { 'screenshare.stream': 1 } });
const cameraAsContentIsShared = !screenshareEntry ? false : !!screenshareEntry.screenshare.stream;
if (cameraAsContentIsShared && isSharing) {
setIsSharing(false);
}
return sharing || cameraAsContentIsShared;
};
const screenshareHasAudio = () => {
const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID },
@ -103,9 +166,24 @@ const screenshareHasAudio = () => {
return !!screenshareEntry.screenshare.hasAudio;
}
const getBroadcastContentType = () => {
const screenshareEntry = Screenshare.findOne({meetindId: Auth.meedingID},
{ fields: { 'screenshare.contentType': 1} });
if (!screenshareEntry) {
// defaults to contentType: "camera"
return CONTENT_TYPE_CAMERA;
}
return screenshareEntry.screenshare.contentType;
}
const screenshareHasEnded = () => {
if (isSharingScreen()) {
setSharingScreen(false);
if (isSharing()) {
setIsSharing(false);
}
if (getSharingContentType() === CONTENT_TYPE_CAMERA) {
setCameraAsContentDeviceId('');
}
KurentoBridge.stop();
@ -151,7 +229,10 @@ const screenshareHasStarted = (isPresenter) => {
}
};
const shareScreen = async (isPresenter, onFail) => {
const shareScreen = async (isPresenter, onFail, options = {}) => {
if (isCameraAsContentBroadcasting()) {
screenshareHasEnded();
}
// stop external video share if running
const meeting = Meetings.findOne({ meetingId: Auth.meetingID });
@ -160,7 +241,14 @@ const shareScreen = async (isPresenter, onFail) => {
}
try {
const stream = await BridgeService.getScreenStream();
let stream;
let contentType = CONTENT_TYPE_SCREENSHARE;
if (options.stream == null) {
stream = await BridgeService.getScreenStream();
} else {
contentType = CONTENT_TYPE_CAMERA;
stream = options.stream;
}
_trackStreamTermination(stream, _handleStreamTermination);
if (!isPresenter) {
@ -168,7 +256,7 @@ const shareScreen = async (isPresenter, onFail) => {
return;
}
await KurentoBridge.share(stream, onFail);
await KurentoBridge.share(stream, onFail, contentType);
// Stream might have been disabled in the meantime. I love badly designed
// async components like this screen sharing bridge :) - prlanzarin 09 May 22
@ -180,7 +268,8 @@ const shareScreen = async (isPresenter, onFail) => {
// Close Shared Notes if open.
NotesService.pinSharedNotes(false);
setSharingScreen(true);
setSharingContentType(contentType);
setIsSharing(true);
} catch (error) {
onFail(error);
}
@ -257,21 +346,28 @@ const isMediaFlowing = (previousStats, currentStats) => {
export {
SCREENSHARE_MEDIA_ELEMENT_NAME,
isMediaFlowing,
isVideoBroadcasting,
isScreenBroadcasting,
isCameraAsContentBroadcasting,
screenshareHasEnded,
screenshareHasStarted,
screenshareHasAudio,
getBroadcastContentType,
shareScreen,
screenShareEndAlert,
dataSavingSetting,
isSharingScreen,
setSharingScreen,
isSharing,
setIsSharing,
setSharingContentType,
getSharingContentType,
getMediaElement,
getMediaElementDimensions,
attachLocalPreviewStream,
isGloballyBroadcasting,
isScreenGloballyBroadcasting,
isCameraAsContentGloballyBroadcasting,
getStats,
setVolume,
getVolume,
shouldEnableVolumeControl,
setCameraAsContentDeviceId,
getCameraAsContentDeviceId,
};

View File

@ -205,6 +205,10 @@ const intlMessages = defineMessages({
id: 'app.videoPreview.wholeImageBrightnessDesc',
description: 'Whole image brightness aria description',
},
cameraAsContentSettingsTitle: {
id: 'app.videoPreview.cameraAsContentSettingsTitle',
description: 'Title for the video preview modal when sharing camera as content',
},
sliderDesc: {
id: 'app.videoPreview.sliderDesc',
description: 'Brightness slider aria description',
@ -426,7 +430,7 @@ class VideoPreview extends Component {
handleVirtualBgSelected(type, name, customParams) {
const { sharedDevices } = this.props;
const { webcamDeviceId } = this.state;
const shared = sharedDevices.includes(webcamDeviceId);
const shared = this.isAlreadyShared(webcamDeviceId);
if (type !== EFFECT_TYPES.NONE_TYPE || CAMERA_BRIGHTNESS_AVAILABLE) {
return this.startVirtualBackground(this.currentVideoStream, type, name, customParams).then((switched) => {
@ -479,6 +483,8 @@ class VideoPreview extends Component {
const {
resolve,
startSharing,
cameraAsContent,
startSharingCameraAsContent,
} = this.props;
const {
webcamDeviceId,
@ -503,18 +509,28 @@ class VideoPreview extends Component {
this.updateVirtualBackgroundInfo();
this.cleanupStreamAndVideo();
PreviewService.changeProfile(selectedProfile);
PreviewService.changeWebcam(webcamDeviceId);
startSharing(webcamDeviceId);
if (cameraAsContent) {
startSharingCameraAsContent(webcamDeviceId);
} else {
startSharing(webcamDeviceId);
}
if (resolve) resolve();
}
handleStopSharing() {
const { resolve, stopSharing } = this.props;
const { resolve, stopSharing, stopSharingCameraAsContent } = this.props;
const { webcamDeviceId } = this.state;
PreviewService.deleteStream(webcamDeviceId);
stopSharing(webcamDeviceId);
this.cleanupStreamAndVideo();
if (this.isCameraAsContentDevice(webcamDeviceId)) {
stopSharingCameraAsContent();
} else {
PreviewService.deleteStream(webcamDeviceId);
stopSharing(webcamDeviceId);
this.cleanupStreamAndVideo();
}
if (resolve) resolve();
}
@ -631,7 +647,8 @@ class VideoPreview extends Component {
}
getInitialCameraStream(deviceId) {
const defaultProfile = PreviewService.getDefaultProfile();
const { cameraAsContent } = this.props;
const defaultProfile = !cameraAsContent ? PreviewService.getDefaultProfile() : PreviewService.getCameraAsContentProfile();
return this.getCameraStream(deviceId, defaultProfile).then(() => {
this.updateDeviceId(deviceId);
@ -706,11 +723,24 @@ class VideoPreview extends Component {
return `${intl.formatMessage(intlMessages.cameraLabel)} ${index}`
}
isAlreadyShared (webcamId) {
const { sharedDevices, cameraAsContentDeviceId } = this.props;
return sharedDevices.includes(webcamId) || webcamId === cameraAsContentDeviceId;
}
isCameraAsContentDevice (deviceId) {
const { cameraAsContentDeviceId } = this.props;
return deviceId === cameraAsContentDeviceId;
}
renderDeviceSelectors() {
const {
intl,
sharedDevices,
isVisualEffects,
cameraAsContent,
} = this.props;
const {
@ -720,6 +750,7 @@ class VideoPreview extends Component {
} = this.state;
const shared = sharedDevices.includes(webcamDeviceId);
const shouldShowVirtualBackgrounds = isVirtualBackgroundsEnabled() && !cameraAsContent;
if (isVisualEffects) {
return (
@ -731,70 +762,103 @@ class VideoPreview extends Component {
return (
<>
<Styled.Label htmlFor="setCam">
{intl.formatMessage(intlMessages.cameraLabel)}
</Styled.Label>
{ availableWebcams && availableWebcams.length > 0
? (
<Styled.Select
id="setCam"
value={webcamDeviceId || ''}
onChange={this.handleSelectWebcam}
>
{availableWebcams.map((webcam, index) => (
<option key={webcam.deviceId} value={webcam.deviceId}>
{webcam.label || this.getFallbackLabel(webcam, index)}
</option>
))}
</Styled.Select>
)
: (
<span>
{intl.formatMessage(intlMessages.webcamNotFoundLabel)}
</span>
)
}
{ shared
? (
<Styled.Label>
{intl.formatMessage(intlMessages.sharedCameraLabel)}
</Styled.Label>
)
: (
<>
<Styled.Label htmlFor="setQuality">
{intl.formatMessage(intlMessages.qualityLabel)}
</Styled.Label>
{PreviewService.PREVIEW_CAMERA_PROFILES.length > 0
? (
<Styled.Select
id="setQuality"
value={selectedProfile || ''}
onChange={this.handleSelectProfile}
>
{PreviewService.PREVIEW_CAMERA_PROFILES.map((profile) => {
const label = intlMessages[`${profile.id}`]
? intl.formatMessage(intlMessages[`${profile.id}`])
: profile.name;
return (
<option key={profile.id} value={profile.id}>
{`${label}`}
</option>
);
})}
{ cameraAsContent
? (
<>
<Styled.Label htmlFor="setCam">
{intl.formatMessage(intlMessages.cameraLabel)}
</Styled.Label>
{ availableWebcams && availableWebcams.length > 0
? (
<Styled.Select
id="setCam"
value={webcamDeviceId || ''}
onChange={this.handleSelectWebcam}
>
{availableWebcams.map((webcam, index) => (
<option key={webcam.deviceId} value={webcam.deviceId}>
{webcam.label || this.getFallbackLabel(webcam, index)}
</option>
))}
</Styled.Select>
)
: (
<span>
{intl.formatMessage(intlMessages.profileNotFoundLabel)}
</span>
)
}
</>
)
}
{isVirtualBackgroundsEnabled() && this.renderVirtualBgSelector()}
<span>
{intl.formatMessage(intlMessages.webcamNotFoundLabel)}
</span>
)
}
</>
)
:
<>
<Styled.Label htmlFor="setCam">
{intl.formatMessage(intlMessages.cameraLabel)}
</Styled.Label>
{ availableWebcams && availableWebcams.length > 0
? (
<Styled.Select
id="setCam"
value={webcamDeviceId || ''}
onChange={this.handleSelectWebcam}
>
{availableWebcams.map((webcam, index) => (
<option key={webcam.deviceId} value={webcam.deviceId}>
{webcam.label || this.getFallbackLabel(webcam, index)}
</option>
))}
</Styled.Select>
)
: (
<span>
{intl.formatMessage(intlMessages.webcamNotFoundLabel)}
</span>
)
}
{ shared
? (
<Styled.Label>
{intl.formatMessage(intlMessages.sharedCameraLabel)}
</Styled.Label>
)
: (
<>
<Styled.Label htmlFor="setQuality">
{intl.formatMessage(intlMessages.qualityLabel)}
</Styled.Label>
{PreviewService.PREVIEW_CAMERA_PROFILES.length > 0
? (
<Styled.Select
id="setQuality"
value={selectedProfile || ''}
onChange={this.handleSelectProfile}
>
{PreviewService.PREVIEW_CAMERA_PROFILES.map((profile) => {
const label = intlMessages[`${profile.id}`]
? intl.formatMessage(intlMessages[`${profile.id}`])
: profile.name;
return (
<option key={profile.id} value={profile.id}>
{`${label}`}
</option>
);
})}
</Styled.Select>
)
: (
<span>
{intl.formatMessage(intlMessages.profileNotFoundLabel)}
</span>
)
}
</>
)
}
{shouldShowVirtualBackgrounds && this.renderVirtualBgSelector()}
</>
}
</>
);
}
@ -806,16 +870,25 @@ class VideoPreview extends Component {
}
renderBrightnessInput() {
const {
cameraAsContent,
} = this.props;
const {
webcamDeviceId,
} = this.state;
if (!ENABLE_CAMERA_BRIGHTNESS) return null;
const { intl } = this.props;
const { brightness, wholeImageBrightness, isStartSharingDisabled } = this.state;
const shared = this.isAlreadyShared(webcamDeviceId);
const origin = brightness <= 100 ? 'left' : 'right';
const offset = origin === 'left'
? (brightness * 100) / 200
: ((200 - brightness) * 100) / 200;
if(cameraAsContent){ return null }
return (
<>
<Styled.Label htmlFor="brightness">
@ -951,10 +1024,15 @@ class VideoPreview extends Component {
}
}
getModalTitle() {
const { intl, cameraAsContent } = this.props;
if (cameraAsContent) return intl.formatMessage(intlMessages.cameraAsContentSettingsTitle);
return intl.formatMessage(intlMessages.webcamSettingsTitle);
}
renderModalContent() {
const {
intl,
sharedDevices,
hasVideoStream,
forceOpen,
camCapReached,
@ -971,7 +1049,7 @@ class VideoPreview extends Component {
&& !forceOpen
&& !(deviceError || previewError);
const shared = sharedDevices.includes(webcamDeviceId);
const shared = this.isAlreadyShared(webcamDeviceId);
const { isIe } = browserInfo;

View File

@ -3,6 +3,9 @@ import { withTracker } from 'meteor/react-meteor-data';
import Service from './service';
import VideoPreview from './component';
import VideoService from '../video-provider/service';
import ScreenShareService from '/imports/ui/components/screenshare/service';
import logger from '/imports/startup/client/logger';
import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors';
const VideoPreviewContainer = (props) => <VideoPreview {...props} />;
@ -12,6 +15,27 @@ export default withTracker(({ setIsOpen, callbackToClose }) => ({
setIsOpen(false);
VideoService.joinVideo(deviceId);
},
startSharingCameraAsContent: (deviceId) => {
callbackToClose();
setIsOpen(false);
const handleFailure = (error) => {
const {
errorCode = SCREENSHARING_ERRORS.UNKNOWN_ERROR.errorCode,
errorMessage = error.message,
} = error;
logger.error({
logCode: 'camera_as_content_failed',
extraInfo: { errorCode, errorMessage },
}, `Sharing camera as content failed: ${errorMessage} (code=${errorCode})`);
ScreenShareService.screenshareHasEnded();
};
ScreenShareService.shareScreen(
true, handleFailure, { stream: Service.getStream(deviceId)._mediaStream }
);
ScreenShareService.setCameraAsContentDeviceId(deviceId);
},
stopSharing: (deviceId) => {
callbackToClose();
setIsOpen(false);
@ -22,7 +46,13 @@ export default withTracker(({ setIsOpen, callbackToClose }) => ({
VideoService.exitVideo();
}
},
stopSharingCameraAsContent: () => {
callbackToClose();
setIsOpen(false);
ScreenShareService.screenshareHasEnded();
},
sharedDevices: VideoService.getSharedDevices(),
cameraAsContentDeviceId: ScreenShareService.getCameraAsContentDeviceId(),
isCamLocked: VideoService.isUserLocked(),
camCapReached: VideoService.hasCapReached(),
closeModal: () => {

View File

@ -14,6 +14,7 @@ const GUM_RETRY_DELAY = 200;
const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles || [];
// Filtered, without hidden profiles
const PREVIEW_CAMERA_PROFILES = CAMERA_PROFILES.filter(p => !p.hidden);
const CAMERA_AS_CONTENT_PROFILE_ID = 'fhd';
const getDefaultProfile = () => {
return CAMERA_PROFILES.find(profile => profile.id === BBBStorage.getItem('WebcamProfileId'))
@ -22,6 +23,11 @@ const getDefaultProfile = () => {
|| CAMERA_PROFILES[0];
}
const getCameraAsContentProfile = () => {
return CAMERA_PROFILES.find(profile => profile.id == CAMERA_AS_CONTENT_PROFILE_ID)
|| CAMERA_PROFILES.find(profile => profile.default)
}
const getCameraProfile = (id) => {
return CAMERA_PROFILES.find(profile => profile.id === id);
}
@ -236,6 +242,7 @@ export default {
deleteStream,
digestVideoDevices,
getDefaultProfile,
getCameraAsContentProfile,
getCameraProfile,
doGUM,
terminateCameraStream,

View File

@ -23,6 +23,7 @@ class ScreenshareBroker extends BaseBroker {
this.ws = null;
this.webRtcPeer = null;
this.hasAudio = false;
this.contentType = "camera";
this.offering = true;
this.signalCandidates = true;
this.ending = false;
@ -32,6 +33,7 @@ class ScreenshareBroker extends BaseBroker {
// caleeName,
// iceServers,
// hasAudio,
// contentType,
// bitrate,
// offering,
// mediaServer,
@ -161,6 +163,7 @@ class ScreenshareBroker extends BaseBroker {
callerName: this.userId,
sdpOffer: offer,
hasAudio: !!this.hasAudio,
contentType: this.contentType,
bitrate: this.bitrate,
mediaServer: this.mediaServer,
};

View File

@ -67,6 +67,7 @@ public:
alwaysShowWaitingRoomUI: true
enableLimitOfViewersInWebcam: false
enableMultipleCameras: true
enableCameraAsContent: false
# Allow users to open webcam video modal/preview when video is already
# active. This also allows to change virtual backgrounds without
# restarting webcam.
@ -390,6 +391,14 @@ public:
width: 1280
height: 720
frameRate: 30
- id: fhd
name: Camera as content
hidden: true
default: false
bitrate: 1500
constraints:
width: 1920
height: 1080
enableScreensharing: true
enableVideo: true
enableVideoMenu: true

View File

@ -177,6 +177,11 @@
"app.media.screenshare.notSupported": "Screensharing is not supported in this browser.",
"app.media.screenshare.autoplayBlockedDesc": "We need your permission to show you the presenter's screen.",
"app.media.screenshare.autoplayAllowLabel": "View shared screen",
"app.media.cameraAsContent.start": "Present camera has started",
"app.media.cameraAsContent.end": "Present camera has ended",
"app.media.cameraAsContent.endDueToDataSaving": "Present camera stopped due to data savings",
"app.media.cameraAsContent.autoplayBlockedDesc": "We need your permission to show you the presenter's camera.",
"app.media.cameraAsContent.autoplayAllowLabel": "View present camera",
"app.screenshare.presenterLoadingLabel": "Your screenshare is loading",
"app.screenshare.viewerLoadingLabel": "The presenter's screen is loading",
"app.screenshare.presenterSharingLabel": "You are now sharing your screen",
@ -185,6 +190,9 @@
"app.screenshare.screenshareRetryOtherEnvError": "Code {0}. Could not share the screen. Try again using a different browser or device.",
"app.screenshare.screenshareUnsupportedEnv": "Code {0}. Browser is not supported. Try again using a different browser or device.",
"app.screenshare.screensharePermissionError": "Code {0}. Permission to capture the screen needs to be granted.",
"app.cameraAsContent.presenterLoadingLabel": "Your camera is loading",
"app.cameraAsContent.viewerLoadingLabel": "The presenter's camera is loading",
"app.cameraAsContent.presenterSharingLabel": "You are now presenting your camera",
"app.meeting.ended": "This session has ended",
"app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}",
"app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon",
@ -453,7 +461,10 @@
"app.actionsBar.actionsDropdown.minimizePresentationLabel": "Minimize presentation",
"app.actionsBar.actionsDropdown.minimizePresentationDesc": "Button used to minimize presentation",
"app.actionsBar.actionsDropdown.layoutModal": "Layout Settings Modal",
"app.actionsBar.actionsDropdown.shareCameraAsContent": "Share camera as content",
"app.actionsBar.actionsDropdown.unshareCameraAsContent": "Stop camera as content",
"app.screenshare.screenShareLabel" : "Screen share",
"app.cameraAsContent.cameraAsContentLabel" : "Present camera",
"app.submenu.application.applicationSectionTitle": "Application",
"app.submenu.application.animationsLabel": "Animations",
"app.submenu.application.audioFilterLabel": "Audio Filters for Microphone",
@ -972,6 +983,7 @@
"app.videoPreview.webcamPreviewLabel": "Webcam preview",
"app.videoPreview.webcamSettingsTitle": "Webcam settings",
"app.videoPreview.webcamEffectsTitle": "Webcam visual effects",
"app.videoPreview.cameraAsContentSettingsTitle": "Present Camera",
"app.videoPreview.webcamVirtualBackgroundLabel": "Virtual background settings",
"app.videoPreview.webcamVirtualBackgroundDisabledLabel": "This device does not support virtual backgrounds",
"app.videoPreview.webcamNotFoundLabel": "Webcam not found",