feat(android) add screen-sharing support

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
Co-authored-by: zycwind <391321232@qq.com>
This commit is contained in:
Titus-Andrei Moldovan 2020-11-04 09:32:06 +01:00 committed by Saúl Ibarra Corretgé
parent 9742e90bb5
commit 9a35026d6a
21 changed files with 295 additions and 25 deletions

View File

@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-feature
android:glEsVersion="0x00020000"
@ -34,8 +34,7 @@
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize">
</activity>
android:windowSoftInputMode="adjustResize"></activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service
@ -46,7 +45,9 @@
</intent-filter>
</service>
<service android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService" />
<service
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
android:foregroundServiceType="mediaProjection" />
</application>
</manifest>

View File

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

View File

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

6
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
function _appStateChanged({ dispatch, getState }, next, action) {
if (navigator.product === 'ReactNative') {
const { appState } = action;
const mute = appState !== 'active'; // Note that 'background' and 'inactive' are treated equal.
const mute = appState !== 'active' && !isLocalVideoTrackDesktop(getState());
sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
}
return next(action);
}

View File

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

View File

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

View File

@ -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<Props, *> {
*/
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) {

View File

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

View File

@ -1,3 +1,4 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './functions';

View File

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

View File

@ -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<Props, State> {
<AudioOnlyButton { ...buttonProps } />
<RaiseHandButton { ...buttonProps } />
<LobbyModeButton { ...buttonProps } />
<ScreenSharingButton { ...buttonProps } />
<MoreOptionsButton { ...moreOptionsButtonProps } />
<Collapsible collapsed = { !showMore }>
<ToggleCameraButton { ...buttonProps } />

View File

@ -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<Props, *> {
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));

View File

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

View File

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

View File

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