feat(camera as content): port to BBB

Enables the presenter to share a camera in the presentation area.
The shared camera automatically uses a pre-defined, fixed and hidden camera.
Profile defined in the settings.yml file.
It is currently using the screenshare's backend.
This commit is contained in:
Carlos 2023-04-26 13:09:02 -03:00 committed by Arthurk12
parent e79ebb720b
commit 8f8bfc8903
8 changed files with 135 additions and 13 deletions

View File

@ -12,6 +12,8 @@ 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 VideoPreviewContainer from '/imports/ui/components/video-preview/container';
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
const propTypes = {
amIPresenter: PropTypes.bool.isRequired,
@ -95,6 +97,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 unshar camera as content',
},
});
const handlePresentationClick = () => Session.set('showUploadPresentationView', true);
@ -110,14 +120,20 @@ class ActionsDropdown extends PureComponent {
this.state = {
isExternalVideoModalOpen: false,
isRandomUserSelectModalOpen: false,
isLayoutModalOpen: false,
}
isLayoutModalOpen: false,
isCameraAsContentModalOpen: false,
propsToPassModal: this.props,
forceOpen: 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 +163,9 @@ class ActionsDropdown extends PureComponent {
setPushLayout,
showPushLayout,
amIModerator,
isMobile,
hasScreenshare,
isCameraAsContentEnabled,
} = this.props;
const {
@ -244,7 +263,20 @@ class ActionsDropdown extends PureComponent {
dataTest: 'layoutModal',
});
}
if (isCameraAsContentEnabled && amIPresenter && !isMobile) {
actions.push({
icon: hasScreenshare ? 'video_off' : 'video',
label: hasScreenshare
? intl.formatMessage(intlMessages.unshareCameraAsContent)
: intl.formatMessage(intlMessages.shareCameraAsContent),
key: 'camera as content',
onClick: hasScreenshare
? screenshareHasEnded
: () => this.setCameraAsContentModalIsOpen(true),
});
}
return actions;
}
@ -294,6 +326,17 @@ 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 +359,16 @@ class ActionsDropdown extends PureComponent {
isMobile,
isRTL,
isSelectRandomUserEnabled,
propsToPassModal,
} = this.props;
const { isExternalVideoModalOpen,
isRandomUserSelectModalOpen, isLayoutModalOpen } = this.state;
const {
isExternalVideoModalOpen,
isRandomUserSelectModalOpen,
isLayoutModalOpen,
isCameraAsContentModalOpen,
setPropsToPassModal,
} = this.state;
const availableActions = this.getAvailableActions();
const availablePresentations = this.makePresentationItems();
@ -368,8 +417,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

@ -151,7 +151,7 @@ const screenshareHasStarted = (isPresenter) => {
}
};
const shareScreen = async (isPresenter, onFail) => {
const shareScreen = async (isPresenter, onFail, options) => {
// stop external video share if running
const meeting = Meetings.findOne({ meetingId: Auth.meetingID });
@ -160,7 +160,12 @@ const shareScreen = async (isPresenter, onFail) => {
}
try {
const stream = await BridgeService.getScreenStream();
let stream;
if (!options) {
stream = await BridgeService.getScreenStream();
} else {
stream = options.stream;
}
_trackStreamTermination(stream, _handleStreamTermination);
if (!isPresenter) {

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',
@ -264,6 +268,7 @@ class VideoPreview extends Component {
const {
webcamDeviceId,
forceOpen,
cameraAsContent
} = this.props;
this._isMounted = true;
@ -479,6 +484,8 @@ class VideoPreview extends Component {
const {
resolve,
startSharing,
cameraAsContent,
startSharingCameraAsContent,
} = this.props;
const {
webcamDeviceId,
@ -503,9 +510,14 @@ 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();
}
@ -631,7 +643,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);
@ -711,15 +724,16 @@ class VideoPreview extends Component {
intl,
sharedDevices,
isVisualEffects,
cameraAsContent,
} = this.props;
const {
webcamDeviceId,
availableWebcams,
selectedProfile,
} = this.state;
const shared = sharedDevices.includes(webcamDeviceId);
const shouldShowVirtualBackgrounds = isVirtualBackgroundsEnabled() && !cameraAsContent;
if (isVisualEffects) {
return (
@ -769,7 +783,7 @@ class VideoPreview extends Component {
? (
<Styled.Select
id="setQuality"
value={selectedProfile || ''}
value={''}
onChange={this.handleSelectProfile}
>
{PreviewService.PREVIEW_CAMERA_PROFILES.map((profile) => {
@ -794,7 +808,7 @@ class VideoPreview extends Component {
</>
)
}
{isVirtualBackgroundsEnabled() && this.renderVirtualBgSelector()}
{shouldShowVirtualBackgrounds && this.renderVirtualBgSelector()}
</>
);
}
@ -951,6 +965,12 @@ 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,

View File

@ -3,6 +3,7 @@ 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';
const VideoPreviewContainer = (props) => <VideoPreview {...props} />;
@ -12,6 +13,13 @@ export default withTracker(({ setIsOpen, callbackToClose }) => ({
setIsOpen(false);
VideoService.joinVideo(deviceId);
},
startSharingCameraAsContent: (deviceId) => {
callbackToClose();
setIsOpen(false);
ScreenShareService.shareScreen(true,
(error) => console.log(error),
{ stream: Service.getStream(deviceId)._mediaStream });
},
stopSharing: (deviceId) => {
callbackToClose();
setIsOpen(false);

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

@ -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,15 @@ public:
width: 1280
height: 720
frameRate: 30
- id: fhd
name: Camera as content
hidden: true
default: false
bitrate: 1500
constraints:
width: 1920
height: 1080
frameRate: 30
enableScreensharing: true
enableVideo: true
enableVideoMenu: true

View File

@ -446,6 +446,8 @@
"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.submenu.application.applicationSectionTitle": "Application",
"app.submenu.application.animationsLabel": "Animations",
@ -965,6 +967,7 @@
"app.videoPreview.webcamPreviewLabel": "Webcam preview",
"app.videoPreview.webcamSettingsTitle": "Webcam settings",
"app.videoPreview.webcamEffectsTitle": "Webcam visual effects",
"app.videoPreview.cameraAsContentSettingsTitle": "Camera as content",
"app.videoPreview.webcamVirtualBackgroundLabel": "Virtual background settings",
"app.videoPreview.webcamVirtualBackgroundDisabledLabel": "This device does not support virtual backgrounds",
"app.videoPreview.webcamNotFoundLabel": "Webcam not found",