Skip to content

Commit

Permalink
feat(ToggleCamera): Implement for web.
Browse files Browse the repository at this point in the history
  • Loading branch information
muscat1 authored Mar 17, 2021
1 parent bb19567 commit aef0287
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 5 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ flow-typed/*
libs/*
resources/*
react/features/stream-effects/virtual-background/vendor/*
load-test/*

# ESLint will by default ignore its own configuration file. However, there does
# not seem to be a reason why we will want to risk being inconsistent with our
Expand Down
3 changes: 2 additions & 1 deletion react/features/base/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export const TOOLBAR_BUTTONS = [
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
'security', 'toggle-camera'
];
3 changes: 3 additions & 0 deletions react/features/base/icons/svg/camera-refresh.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions react/features/base/icons/svg/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { default as IconCamera } from './camera.svg';
export { default as IconCameraDisabled } from './camera-disabled.svg';
export { default as IconCameraEmpty } from './camera-empty.svg';
export { default as IconCameraEmptyDisabled } from './camera-empty-disabled.svg';
export { default as IconCameraRefresh } from './camera-refresh.svg';
export { default as IconCancelSelection } from './cancel.svg';
export { default as IconChat } from './chat.svg';
export { default as IconChatSend } from './send.svg';
Expand Down
7 changes: 5 additions & 2 deletions react/features/base/lib-jitsi-meet/functions.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
* are "video" or "audio".
* @param {string} deviceId - The id of the target media source.
* @param {number} [timeout] - A timeout for the JitsiMeetJS.createLocalTracks function call.
* @param {Object} additionalOptions - Extra options to be passed to lib-jitsi-meet's {@code createLocalTracks}.
*
* @returns {Promise<JitsiLocalTrack>}
*/
export function createLocalTrack(type: string, deviceId: string, timeout: ?number) {
export function createLocalTrack(type: string, deviceId: string, timeout: ?number, additionalOptions: ?Object) {
return (
JitsiMeetJS.createLocalTracks({
cameraDeviceId: deviceId,
Expand All @@ -26,7 +28,8 @@ export function createLocalTrack(type: string, deviceId: string, timeout: ?numbe
firefox_fake_device:
window.config && window.config.firefox_fake_device,
micDeviceId: deviceId,
timeout
timeout,
...additionalOptions
})
.then(([ jitsiLocalTrack ]) => jitsiLocalTrack));
}
Expand Down
40 changes: 39 additions & 1 deletion react/features/base/tracks/actions.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* global APP */

import {
createTrackMutedEvent,
sendAnalytics
} from '../../analytics';
import { showErrorNotification, showNotification } from '../../notifications';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { JitsiTrackErrors, JitsiTrackEvents, createLocalTrack } from '../lib-jitsi-meet';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
Expand All @@ -13,6 +15,7 @@ import {
VIDEO_TYPE
} from '../media';
import { getLocalParticipant } from '../participants';
import { updateSettings } from '../settings';

import {
SET_NO_SRC_DATA_NOTIFICATION_UID,
Expand Down Expand Up @@ -717,3 +720,38 @@ export function updateLastTrackVideoMediaEvent(track, name) {
name
};
}

/**
* Toggles the facingMode constraint on the video stream.
*
* @returns {Function}
*/
export function toggleCamera() {
return async (dispatch, getState) => {
const state = getState();
const tracks = state['features/base/tracks'];
const localVideoTrack = getLocalVideoTrack(tracks).jitsiTrack;
const currentFacingMode = localVideoTrack.getCameraFacingMode();

/**
* FIXME: Ideally, we should be dispatching {@code replaceLocalTrack} here,
* but it seems to not trigger the re-rendering of the local video on Chrome;
* could be due to a plan B vs unified plan issue. Therefore, we use the legacy
* method defined in conference.js that manually takes care of updating the local
* video as well.
*/
await APP.conference.useVideoStream(null);

const targetFacingMode = currentFacingMode === CAMERA_FACING_MODE.USER
? CAMERA_FACING_MODE.ENVIRONMENT
: CAMERA_FACING_MODE.USER;

// Update the flipX value so the environment facing camera is not flipped, before the new track is created.
dispatch(updateSettings({ localFlipX: targetFacingMode === CAMERA_FACING_MODE.USER }));

const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode });

// FIXME: See above.
await APP.conference.useVideoStream(newVideoTrack);
};
}
77 changes: 77 additions & 0 deletions react/features/toolbox/components/web/ToggleCameraButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// @flow

import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { IconCameraRefresh } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { isLocalCameraTrackMuted, toggleCamera } from '../../../base/tracks';

/**
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
*/
type Props = AbstractButtonProps & {

/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean,

/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean,

/**
* The Redux dispatch function.
*/
dispatch: Function
};

/**
* An implementation of a button for toggling the camera facing mode.
*/
class ToggleCameraButton extends AbstractButton<Props, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
icon = IconCameraRefresh;
label = 'toolbar.toggleCamera';

/**
* Handles clicking/pressing the button.
*
* @returns {void}
*/
_handleClick() {
this.props.dispatch(toggleCamera());
}

/**
* Whether this button is disabled or not.
*
* @returns {boolean}
*/
_isDisabled() {
return this.props._audioOnly || this.props._videoMuted;
}
}

/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function mapStateToProps(state): Object {
const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks'];
const { videoInput } = state['features/base/devices'].availableDevices;

return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalCameraTrackMuted(tracks),
visible: isMobileBrowser() && videoInput.length > 1
};
}

export default translate(connect(mapStateToProps)(ToggleCameraButton));
7 changes: 6 additions & 1 deletion react/features/toolbox/components/web/Toolbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import MuteEveryoneButton from '../MuteEveryoneButton';
import AudioSettingsButton from './AudioSettingsButton';
import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import ToggleCameraButton from './ToggleCameraButton';
import ToolbarButton from './ToolbarButton';
import VideoSettingsButton from './VideoSettingsButton';

Expand Down Expand Up @@ -929,7 +930,7 @@ class Toolbox extends Component<Props, State> {
}

/**
* Returns true if the profile button is visible and false otherwise.
* Returns true if the embed meeting button is visible and false otherwise.
*
* @returns {boolean}
*/
Expand Down Expand Up @@ -967,6 +968,10 @@ class Toolbox extends Component<Props, State> {
const group1 = [
...additionalButtons,

this._shouldShowButton('toggle-camera')
&& <ToggleCameraButton
key = 'toggle-camera'
showLabel = { true } />,
this._shouldShowButton('videoquality')
&& <OverflowMenuVideoQualityItem
key = 'videoquality'
Expand Down

0 comments on commit aef0287

Please sign in to comment.