diff --git a/android/sdk/src/main/AndroidManifest.xml b/android/sdk/src/main/AndroidManifest.xml index b69676395a..b87d5f4b31 100644 --- a/android/sdk/src/main/AndroidManifest.xml +++ b/android/sdk/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ - + - + android:windowSoftInputMode="adjustResize"> - + \ No newline at end of file diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java index a8edcd9573..9b954c0905 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java @@ -43,6 +43,7 @@ class PictureInPictureModule extends ReactContextBaseJavaModule { private static final String TAG = NAME; private static boolean isSupported; + private boolean isDisabled; public PictureInPictureModule(ReactApplicationContext reactContext) { super(reactContext); @@ -83,6 +84,10 @@ class PictureInPictureModule extends ReactContextBaseJavaModule { */ @TargetApi(Build.VERSION_CODES.O) public void enterPictureInPicture() { + if (isDisabled) { + return; + } + if (!isSupported) { throw new IllegalStateException("Picture-in-Picture not supported"); } @@ -126,6 +131,11 @@ class PictureInPictureModule extends ReactContextBaseJavaModule { } } + @ReactMethod + public void setPictureInPictureDisabled(Boolean disabled) { + this.isDisabled = disabled; + } + public boolean isPictureInPictureSupported() { return isSupported; } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3e777c05f7..9b3c49ca32 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -293,8 +293,8 @@ PODS: - React - react-native-splash-screen (3.2.0): - React - - react-native-webrtc (1.84.0): - - React + - react-native-webrtc (1.84.1): + - React-Core - react-native-webview (10.9.0): - React - React-RCTActionSheet (0.61.5-jitsi.2): @@ -562,7 +562,7 @@ SPEC CHECKSUMS: react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 - react-native-webrtc: 9268ae9a2bc9730796b0968d012327e92c392adf + react-native-webrtc: edd689b0d5a462d7a6f6f52bca3f9414fc0ee11c react-native-webview: 6ee7868ca8eba635dbf7963986d1ab7959da0391 React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9 React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6 diff --git a/package-lock.json b/package-lock.json index 2e82012ec5..112b68f055 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14265,9 +14265,9 @@ "integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow==" }, "react-native-webrtc": { - "version": "1.84.0", - "resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.0.tgz", - "integrity": "sha512-xPOFbrcehuBzLnFy3keCM2HyMsyCVDQjQNAn8SIHKH/PA8Q7kZ4spuytc2E1hBTr7zH/vQ2Px+DWqu7on12jag==", + "version": "1.84.1", + "resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.1.tgz", + "integrity": "sha512-ewZBgKE+YhLaivo9Wh6aiaEp8ZRvFMqblrkDl1nptQiNNH6CungoAzSOxGDnHWAxepRfiUrW5qnADrsYKmaNeQ==", "requires": { "base64-js": "^1.1.2", "event-target-shim": "^1.0.5", diff --git a/package.json b/package.json index ea47deb96c..2b393cdec1 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "react-native-svg-transformer": "0.14.3", "react-native-url-polyfill": "1.2.0", "react-native-watch-connectivity": "0.4.3", - "react-native-webrtc": "1.84.0", + "react-native-webrtc": "1.84.1", "react-native-webview": "10.9.0", "react-native-youtube-iframe": "1.2.3", "react-redux": "7.1.0", @@ -92,7 +92,7 @@ "react-transition-group": "2.4.0", "redux": "4.0.4", "redux-thunk": "2.2.0", - "rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af", + "rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af", "rtcstats": "github:jitsi/rtcstats#v6.2.0", "stackblur-canvas": "2.3.0", "styled-components": "3.4.9", diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.any.js similarity index 100% rename from react/features/base/conference/middleware.js rename to react/features/base/conference/middleware.any.js diff --git a/react/features/base/conference/middleware.native.js b/react/features/base/conference/middleware.native.js new file mode 100644 index 0000000000..b19fd8fdab --- /dev/null +++ b/react/features/base/conference/middleware.native.js @@ -0,0 +1,74 @@ +// @flow + +import { setPictureInPictureDisabled } from '../../mobile/picture-in-picture/functions'; +import { setAudioOnly } from '../audio-only'; +import JitsiMeetJS from '../lib-jitsi-meet'; +import { MiddlewareRegistry } from '../redux'; +import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes'; +import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from '../tracks/actions'; +import { getLocalVideoTrack, isLocalVideoTrackDesktop } from '../tracks/functions'; + +import './middleware.any'; + +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case TOGGLE_SCREENSHARING: { + _toggleScreenSharing(store); + break; + } + } + + return next(action); +}); + +/** + * Toggles screen sharing. + * + * @private + * @param {Store} store - The redux. + * @returns {void} + */ +function _toggleScreenSharing(store) { + const { dispatch, getState } = store; + const state = getState(); + + const isSharing = isLocalVideoTrackDesktop(state); + + if (isSharing) { + dispatch(destroyLocalDesktopTrackIfExists()); + } else { + _startScreenSharing(dispatch, state); + } +} + +/** + * Creates desktop track and replaces the local one. + * + * @private + * @param {Dispatch} dispatch - The redux {@code dispatch} function. + * @param {Object} state - The redux state. + * @returns {void} + */ +function _startScreenSharing(dispatch, state) { + setPictureInPictureDisabled(true); + + JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] }) + .then(tracks => { + const track = tracks[0]; + const currentLocalTrack = getLocalVideoTrack(state['features/base/tracks']); + const currentJitsiTrack = currentLocalTrack && currentLocalTrack.jitsiTrack; + + dispatch(replaceLocalTrack(currentJitsiTrack, track)); + + const { enabled: audioOnly } = state['features/base/audio-only']; + + if (audioOnly) { + dispatch(setAudioOnly(false)); + } + }) + .catch(error => { + console.log('ERROR creating ScreeSharing stream ', error); + + setPictureInPictureDisabled(false); + }); +} diff --git a/react/features/base/conference/middleware.web.js b/react/features/base/conference/middleware.web.js new file mode 100644 index 0000000000..dd083b6069 --- /dev/null +++ b/react/features/base/conference/middleware.web.js @@ -0,0 +1,23 @@ +// @flow + +import UIEvents from '../../../../service/UI/UIEvents'; +import { MiddlewareRegistry } from '../redux'; +import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes'; + +import './middleware.any'; + +declare var APP: Object; + +MiddlewareRegistry.register((/* store */) => next => action => { + switch (action.type) { + case TOGGLE_SCREENSHARING: { + if (typeof APP === 'object') { + APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING); + } + + break; + } + } + + return next(action); +}); diff --git a/react/features/base/lastn/middleware.js b/react/features/base/lastn/middleware.js index 1d5959c188..4b0916b9c0 100644 --- a/react/features/base/lastn/middleware.js +++ b/react/features/base/lastn/middleware.js @@ -16,6 +16,7 @@ import { getParticipantCount } from '../participants/functions'; import { MiddlewareRegistry } from '../redux'; +import { isLocalVideoTrackDesktop } from '../tracks/functions'; import { limitLastN } from './functions'; import logger from './logger'; @@ -78,7 +79,7 @@ function _updateLastN({ getState }) { } if (typeof appState !== 'undefined' && appState !== 'active') { - lastN = 0; + lastN = isLocalVideoTrackDesktop(state) ? 1 : 0; } else if (audioOnly) { const { screenShares, tileViewEnabled } = state['features/video-layout']; const largeVideoParticipantId = state['features/large-video'].participantId; diff --git a/react/features/base/media/middleware.js b/react/features/base/media/middleware.js index 0ea99eae84..9e160d1e11 100644 --- a/react/features/base/media/middleware.js +++ b/react/features/base/media/middleware.js @@ -13,7 +13,7 @@ import { isRoomValid, SET_ROOM } from '../conference'; import JitsiMeetJS from '../lib-jitsi-meet'; import { MiddlewareRegistry } from '../redux'; import { getPropertyValue } from '../settings'; -import { setTrackMuted, TRACK_ADDED } from '../tracks'; +import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks'; import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions'; import { @@ -73,13 +73,15 @@ MiddlewareRegistry.register(store => next => action => { * @private * @returns {Object} The value returned by {@code next(action)}. */ -function _appStateChanged({ dispatch }, next, action) { - const { appState } = action; - const mute = appState !== 'active'; // Note that 'background' and 'inactive' are treated equal. +function _appStateChanged({ dispatch, getState }, next, action) { + if (navigator.product === 'ReactNative') { + const { appState } = action; + const mute = appState !== 'active' && !isLocalVideoTrackDesktop(getState()); - sendAnalytics(createTrackMutedEvent('video', 'background mode', mute)); + sendAnalytics(createTrackMutedEvent('video', 'background mode', mute)); - dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND)); + dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND)); + } return next(action); } diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js index 9e8a8a7880..328eb16c6d 100644 --- a/react/features/base/tracks/actions.js +++ b/react/features/base/tracks/actions.js @@ -9,7 +9,8 @@ import { MEDIA_TYPE, setAudioMuted, setVideoMuted, - VIDEO_MUTISM_AUTHORITY + VIDEO_MUTISM_AUTHORITY, + VIDEO_TYPE } from '../media'; import { getLocalParticipant } from '../participants'; @@ -24,7 +25,13 @@ import { TRACK_UPDATED, TRACK_WILL_CREATE } from './actionTypes'; -import { createLocalTracksF, getLocalTrack, getLocalTracks, getTrackByJitsiTrack } from './functions'; +import { + createLocalTracksF, + getLocalTrack, + getLocalTracks, + getLocalVideoTrack, + getTrackByJitsiTrack +} from './functions'; import logger from './logger'; /** @@ -40,6 +47,8 @@ export function createDesiredLocalTracks(...desiredTypes) { return (dispatch, getState) => { const state = getState(); + dispatch(destroyLocalDesktopTrackIfExists()); + if (desiredTypes.length === 0) { const { audio, video } = state['features/base/media']; @@ -663,6 +672,22 @@ function _trackCreateCanceled(mediaType) { }; } +/** + * If thee local track if of type Desktop, it calls _disposeAndRemoveTracks) on it. + * + * @returns {Function} + */ +export function destroyLocalDesktopTrackIfExists() { + return (dispatch, getState) => { + const videoTrack = getLocalVideoTrack(getState()['features/base/tracks']); + const isDesktopTrack = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP; + + if (isDesktopTrack) { + dispatch(_disposeAndRemoveTracks([ videoTrack.jitsiTrack ])); + } + }; +} + /** * Sets UID of the displayed no data from source notification. Used to track * if the notification was previously displayed in this context. diff --git a/react/features/base/tracks/functions.js b/react/features/base/tracks/functions.js index fbcc4af660..5d4ab2b7b9 100644 --- a/react/features/base/tracks/functions.js +++ b/react/features/base/tracks/functions.js @@ -1,7 +1,7 @@ /* global APP */ import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet'; -import { MEDIA_TYPE, setAudioMuted } from '../media'; +import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media'; import { getUserSelectedCameraDeviceId, getUserSelectedMicDeviceId @@ -383,6 +383,19 @@ export function isLocalTrackMuted(tracks, mediaType) { return !track || track.muted; } +/** + * Checks if the local video track is of type DESKtOP. + * + * @param {Object} state - The redux state. + * @returns {boolean} + */ +export function isLocalVideoTrackDesktop(state) { + const videoTrack = getLocalVideoTrack(state['features/base/tracks']); + + return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP; +} + + /** * Returns true if the remote track of the given media type and the given * participant is muted, false otherwise. diff --git a/react/features/mobile/picture-in-picture/components/PictureInPictureButton.js b/react/features/mobile/picture-in-picture/components/PictureInPictureButton.js index 280fd121f0..b3b8d5ab97 100644 --- a/react/features/mobile/picture-in-picture/components/PictureInPictureButton.js +++ b/react/features/mobile/picture-in-picture/components/PictureInPictureButton.js @@ -7,6 +7,7 @@ import { translate } from '../../../base/i18n'; import { IconMenuDown } from '../../../base/icons'; import { connect } from '../../../base/redux'; import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; +import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions'; import { enterPictureInPicture } from '../actions'; type Props = AbstractButtonProps & { @@ -63,7 +64,7 @@ class PictureInPictureButton extends AbstractButton { */ function _mapStateToProps(state): Object { const flag = Boolean(getFeatureFlag(state, PIP_ENABLED)); - let enabled = flag; + let enabled = flag && !isLocalVideoTrackDesktop(state); // Override flag for Android, since it might be unsupported. if (Platform.OS === 'android' && !NativeModules.PictureInPicture.SUPPORTED) { diff --git a/react/features/mobile/picture-in-picture/functions.js b/react/features/mobile/picture-in-picture/functions.js new file mode 100644 index 0000000000..619501da07 --- /dev/null +++ b/react/features/mobile/picture-in-picture/functions.js @@ -0,0 +1,15 @@ +// @flow + +import { NativeModules } from 'react-native'; + +/** + * Enabled/Disables the PictureInPicture mode in PiP native module. + * + * @param {boolean} disabled - Whether the PiP mode should be disabled. + * @returns {void} + */ +export function setPictureInPictureDisabled(disabled: boolean) { + const { PictureInPicture } = NativeModules; + + PictureInPicture.setPictureInPictureDisabled(disabled); +} diff --git a/react/features/mobile/picture-in-picture/index.js b/react/features/mobile/picture-in-picture/index.js index 803dacd06c..b845e8859c 100644 --- a/react/features/mobile/picture-in-picture/index.js +++ b/react/features/mobile/picture-in-picture/index.js @@ -1,3 +1,4 @@ export * from './actions'; export * from './actionTypes'; export * from './components'; +export * from './functions'; diff --git a/react/features/toolbox/components/VideoMuteButton.js b/react/features/toolbox/components/VideoMuteButton.js index 0376879828..edddccbfce 100644 --- a/react/features/toolbox/components/VideoMuteButton.js +++ b/react/features/toolbox/components/VideoMuteButton.js @@ -9,7 +9,6 @@ import { sendAnalytics } from '../../analytics'; import { setAudioOnly } from '../../base/audio-only'; -import { hasAvailableDevices } from '../../base/devices'; import { translate } from '../../base/i18n'; import { VIDEO_MUTISM_AUTHORITY, @@ -19,6 +18,7 @@ import { connect } from '../../base/redux'; import { AbstractVideoMuteButton } from '../../base/toolbox/components'; import type { AbstractButtonProps } from '../../base/toolbox/components'; import { getLocalVideoType, isLocalCameraTrackMuted } from '../../base/tracks'; +import { isVideoMuteButtonDisabled } from '../functions'; declare var APP: Object; @@ -190,7 +190,7 @@ function _mapStateToProps(state): Object { return { _audioOnly: Boolean(audioOnly), - _videoDisabled: !hasAvailableDevices(state, 'videoInput'), + _videoDisabled: isVideoMuteButtonDisabled(state), _videoMediaType: getLocalVideoType(tracks), _videoMuted: isLocalCameraTrackMuted(tracks) }; diff --git a/react/features/toolbox/components/native/OverflowMenu.js b/react/features/toolbox/components/native/OverflowMenu.js index 092cd1b686..dce6e0f434 100644 --- a/react/features/toolbox/components/native/OverflowMenu.js +++ b/react/features/toolbox/components/native/OverflowMenu.js @@ -23,6 +23,7 @@ import HelpButton from '../HelpButton'; import AudioOnlyButton from './AudioOnlyButton'; import MoreOptionsButton from './MoreOptionsButton'; import RaiseHandButton from './RaiseHandButton'; +import ScreenSharingButton from './ScreenSharingButton.js'; import ToggleCameraButton from './ToggleCameraButton'; import styles from './styles'; @@ -131,6 +132,7 @@ class OverflowMenu extends PureComponent { + diff --git a/react/features/toolbox/components/native/ScreenSharingButton.js b/react/features/toolbox/components/native/ScreenSharingButton.js new file mode 100644 index 0000000000..b9966d3e3b --- /dev/null +++ b/react/features/toolbox/components/native/ScreenSharingButton.js @@ -0,0 +1,77 @@ +// @flow + +import { Platform } from 'react-native'; + +import { translate } from '../../../base/i18n'; +import { IconShareDesktop } from '../../../base/icons'; +import { connect } from '../../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; +import { toggleScreensharing, isLocalVideoTrackDesktop } from '../../../base/tracks'; + +/** + * The type of the React {@code Component} props of {@link ScreenSharingButton}. + */ +type Props = AbstractButtonProps & { + + /** + * Whether video is currently muted or not. + */ + _screensharing: boolean, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function +}; + +/** + * An implementation of a button for toggling screen sharing. + */ +class ScreenSharingButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen'; + icon = IconShareDesktop; + label = 'toolbar.startScreenSharing'; + toggledLabel = 'toolbar.stopScreenSharing'; + + /** + * Handles clicking / pressing the button. + * + * @override + * @protected + * @returns {void} + */ + _handleClick() { + this.props.dispatch(toggleScreensharing()); + } + + /** + * Indicates whether this button is in toggled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isToggled() { + return this.props._screensharing; + } +} + +/** + * Maps (parts of) the redux state to the associated props for the + * {@code ToggleCameraButton} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _disabled: boolean, + * _screensharing: boolean + * }} + */ +function _mapStateToProps(state): Object { + return { + _screensharing: isLocalVideoTrackDesktop(state), + visible: Platform.OS === 'android' + }; +} + +export default translate(connect(_mapStateToProps)(ScreenSharingButton)); diff --git a/react/features/toolbox/functions.native.js b/react/features/toolbox/functions.native.js index 0e6392a062..5679ff5b10 100644 --- a/react/features/toolbox/functions.native.js +++ b/react/features/toolbox/functions.native.js @@ -1,7 +1,9 @@ // @flow +import { hasAvailableDevices } from '../base/devices'; import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag } from '../base/flags'; import { toState } from '../base/redux'; +import { isLocalVideoTrackDesktop } from '../base/tracks'; /** * Returns true if the toolbox is visible. @@ -18,3 +20,13 @@ export function isToolboxVisible(stateful: Object | Function) { return enabled && (alwaysVisible || visible || participantCount === 1 || flag); } + +/** + * Indicates if the video mute button is disabled or not. + * + * @param {string} state - The state from the Redux store. + * @returns {boolean} + */ +export function isVideoMuteButtonDisabled(state: Object) { + return !hasAvailableDevices(state, 'videoInput') || isLocalVideoTrackDesktop(state); +} diff --git a/react/features/toolbox/functions.web.js b/react/features/toolbox/functions.web.js index 95aa0bd35f..297a776a51 100644 --- a/react/features/toolbox/functions.web.js +++ b/react/features/toolbox/functions.web.js @@ -77,3 +77,13 @@ export function isAudioSettingsButtonDisabled(state: Object) { export function isVideoSettingsButtonDisabled(state: Object) { return !hasAvailableDevices(state, 'videoInput'); } + +/** + * Indicates if the video mute button is disabled or not. + * + * @param {string} state - The state from the Redux store. + * @returns {boolean} + */ +export function isVideoMuteButtonDisabled(state: Object) { + return !hasAvailableDevices(state, 'videoInput'); +} diff --git a/react/features/welcome/components/WelcomePage.native.js b/react/features/welcome/components/WelcomePage.native.js index 8dcac4e23b..29f11fc874 100644 --- a/react/features/welcome/components/WelcomePage.native.js +++ b/react/features/welcome/components/WelcomePage.native.js @@ -19,6 +19,7 @@ import { connect } from '../../base/redux'; import { ColorPalette } from '../../base/styles'; import { createDesiredLocalTracks, + destroyLocalDesktopTrackIfExists, destroyLocalTracks } from '../../base/tracks'; import { HelpView } from '../../help'; @@ -81,6 +82,8 @@ class WelcomePage extends AbstractWelcomePage { if (this.props._settings.startAudioOnly) { dispatch(destroyLocalTracks()); } else { + dispatch(destroyLocalDesktopTrackIfExists()); + // Make sure we don't request the permission for the camera from // the start. We will, however, create a video track iff the user // already granted the permission.