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:
parent
e79ebb720b
commit
8f8bfc8903
@ -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);
|
||||
@ -111,13 +121,19 @@ class ActionsDropdown extends PureComponent {
|
||||
isExternalVideoModalOpen: false,
|
||||
isRandomUserSelectModalOpen: 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 {
|
||||
@ -245,6 +264,19 @@ class ActionsDropdown extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@ -370,6 +419,23 @@ class ActionsDropdown extends PureComponent {
|
||||
"low", RandomUserSelectContainer) : null }
|
||||
{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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user