feat(ToggleCamera): Implement for web.

This commit is contained in:
Mihai-Andrei Uscat 2021-03-17 10:44:18 +02:00 committed by GitHub
parent bb19567efa
commit aef0287605
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 134 additions and 5 deletions

View File

@ -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

View File

@ -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'
];

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.33335 7.50001V15C3.33335 15.4602 3.70645 15.8333 4.16669 15.8333H15.8334C16.2936 15.8333 16.6667 15.4602 16.6667 15V7.50001C16.6667 7.03977 16.2936 6.66668 15.8334 6.66668H13.4763L11.8097 5.00001H8.19038L6.52371 6.66668H4.16669C3.70645 6.66668 3.33335 7.03977 3.33335 7.50001ZM5.83335 5.00001L7.01187 3.8215C7.32443 3.50894 7.74835 3.33334 8.19038 3.33334H11.8097C12.2517 3.33334 12.6756 3.50894 12.9882 3.8215L14.1667 5.00001H15.8334C17.2141 5.00001 18.3334 6.1193 18.3334 7.50001V15C18.3334 16.3807 17.2141 17.5 15.8334 17.5H4.16669C2.78598 17.5 1.66669 16.3807 1.66669 15V7.50001C1.66669 6.1193 2.78598 5.00001 4.16669 5.00001H5.83335ZM4.33335 11.6667L6.83335 9.16668L9.33335 11.6667H7.64313C7.98572 12.6298 8.90536 13.3194 9.98605 13.3194C10.5912 13.3194 11.1585 13.1028 11.6011 12.7238L12.7474 13.935C12.0136 14.5888 11.0462 14.986 9.98605 14.986C7.978 14.986 6.30283 13.5608 5.91699 11.6667H4.33335ZM15.8334 10L13.3334 12.5L10.8334 10H12.3618C12.0232 9.02958 11.0999 8.33334 10.014 8.33334C9.4406 8.33334 8.90113 8.52599 8.46348 8.87591C8.44177 8.89327 7.42268 7.57418 7.42268 7.57418C8.13299 7.00625 9.03383 6.66668 10.014 6.66668C12.027 6.66668 13.7054 8.09895 14.0859 10H15.8334Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -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';

View File

@ -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,
@ -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));
}

View File

@ -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,
@ -13,6 +15,7 @@ import {
VIDEO_TYPE
} from '../media';
import { getLocalParticipant } from '../participants';
import { updateSettings } from '../settings';
import {
SET_NO_SRC_DATA_NOTIFICATION_UID,
@ -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);
};
}

View File

@ -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));

View File

@ -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';
@ -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}
*/
@ -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'