浏览代码

feat(android) add screen-sharing support

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
Co-authored-by: zycwind <391321232@qq.com>
master
Titus-Andrei Moldovan 5 年前
父节点
当前提交
9a35026d6a

+ 5
- 4
android/sdk/src/main/AndroidManifest.xml 查看文件

12
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
12
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
13
     <uses-permission android:name="android.permission.WAKE_LOCK" />
13
     <uses-permission android:name="android.permission.WAKE_LOCK" />
14
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
14
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
15
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
15
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
16
 
16
 
17
     <uses-feature
17
     <uses-feature
18
         android:glEsVersion="0x00020000"
18
         android:glEsVersion="0x00020000"
34
             android:launchMode="singleTask"
34
             android:launchMode="singleTask"
35
             android:resizeableActivity="true"
35
             android:resizeableActivity="true"
36
             android:supportsPictureInPicture="true"
36
             android:supportsPictureInPicture="true"
37
-            android:windowSoftInputMode="adjustResize">
38
-        </activity>
37
+            android:windowSoftInputMode="adjustResize"></activity>
39
         <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
38
         <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
40
 
39
 
41
         <service
40
         <service
46
             </intent-filter>
45
             </intent-filter>
47
         </service>
46
         </service>
48
 
47
 
49
-        <service android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService" />
48
+        <service
49
+            android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
50
+            android:foregroundServiceType="mediaProjection" />
50
     </application>
51
     </application>
51
 
52
 
52
 </manifest>
53
 </manifest>

+ 10
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java 查看文件

43
     private static final String TAG = NAME;
43
     private static final String TAG = NAME;
44
 
44
 
45
     private static boolean isSupported;
45
     private static boolean isSupported;
46
+    private boolean isDisabled;
46
 
47
 
47
     public PictureInPictureModule(ReactApplicationContext reactContext) {
48
     public PictureInPictureModule(ReactApplicationContext reactContext) {
48
         super(reactContext);
49
         super(reactContext);
83
      */
84
      */
84
     @TargetApi(Build.VERSION_CODES.O)
85
     @TargetApi(Build.VERSION_CODES.O)
85
     public void enterPictureInPicture() {
86
     public void enterPictureInPicture() {
87
+        if (isDisabled) {
88
+            return;
89
+        }
90
+
86
         if (!isSupported) {
91
         if (!isSupported) {
87
             throw new IllegalStateException("Picture-in-Picture not supported");
92
             throw new IllegalStateException("Picture-in-Picture not supported");
88
         }
93
         }
126
         }
131
         }
127
     }
132
     }
128
 
133
 
134
+    @ReactMethod
135
+    public void setPictureInPictureDisabled(Boolean disabled) {
136
+        this.isDisabled = disabled;
137
+    }
138
+
129
     public boolean isPictureInPictureSupported() {
139
     public boolean isPictureInPictureSupported() {
130
         return isSupported;
140
         return isSupported;
131
     }
141
     }

+ 3
- 3
ios/Podfile.lock 查看文件

293
     - React
293
     - React
294
   - react-native-splash-screen (3.2.0):
294
   - react-native-splash-screen (3.2.0):
295
     - React
295
     - React
296
-  - react-native-webrtc (1.84.0):
297
-    - React
296
+  - react-native-webrtc (1.84.1):
297
+    - React-Core
298
   - react-native-webview (10.9.0):
298
   - react-native-webview (10.9.0):
299
     - React
299
     - React
300
   - React-RCTActionSheet (0.61.5-jitsi.2):
300
   - React-RCTActionSheet (0.61.5-jitsi.2):
562
   react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
562
   react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
563
   react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
563
   react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
564
   react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
564
   react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
565
-  react-native-webrtc: 9268ae9a2bc9730796b0968d012327e92c392adf
565
+  react-native-webrtc: edd689b0d5a462d7a6f6f52bca3f9414fc0ee11c
566
   react-native-webview: 6ee7868ca8eba635dbf7963986d1ab7959da0391
566
   react-native-webview: 6ee7868ca8eba635dbf7963986d1ab7959da0391
567
   React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
567
   React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
568
   React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
568
   React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6

+ 3
- 3
package-lock.json 查看文件

14265
       "integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
14265
       "integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
14266
     },
14266
     },
14267
     "react-native-webrtc": {
14267
     "react-native-webrtc": {
14268
-      "version": "1.84.0",
14269
-      "resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.0.tgz",
14270
-      "integrity": "sha512-xPOFbrcehuBzLnFy3keCM2HyMsyCVDQjQNAn8SIHKH/PA8Q7kZ4spuytc2E1hBTr7zH/vQ2Px+DWqu7on12jag==",
14268
+      "version": "1.84.1",
14269
+      "resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.1.tgz",
14270
+      "integrity": "sha512-ewZBgKE+YhLaivo9Wh6aiaEp8ZRvFMqblrkDl1nptQiNNH6CungoAzSOxGDnHWAxepRfiUrW5qnADrsYKmaNeQ==",
14271
       "requires": {
14271
       "requires": {
14272
         "base64-js": "^1.1.2",
14272
         "base64-js": "^1.1.2",
14273
         "event-target-shim": "^1.0.5",
14273
         "event-target-shim": "^1.0.5",

+ 2
- 2
package.json 查看文件

84
     "react-native-svg-transformer": "0.14.3",
84
     "react-native-svg-transformer": "0.14.3",
85
     "react-native-url-polyfill": "1.2.0",
85
     "react-native-url-polyfill": "1.2.0",
86
     "react-native-watch-connectivity": "0.4.3",
86
     "react-native-watch-connectivity": "0.4.3",
87
-    "react-native-webrtc": "1.84.0",
87
+    "react-native-webrtc": "1.84.1",
88
     "react-native-webview": "10.9.0",
88
     "react-native-webview": "10.9.0",
89
     "react-native-youtube-iframe": "1.2.3",
89
     "react-native-youtube-iframe": "1.2.3",
90
     "react-redux": "7.1.0",
90
     "react-redux": "7.1.0",
92
     "react-transition-group": "2.4.0",
92
     "react-transition-group": "2.4.0",
93
     "redux": "4.0.4",
93
     "redux": "4.0.4",
94
     "redux-thunk": "2.2.0",
94
     "redux-thunk": "2.2.0",
95
-    "rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
95
+    "rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
96
     "rtcstats": "github:jitsi/rtcstats#v6.2.0",
96
     "rtcstats": "github:jitsi/rtcstats#v6.2.0",
97
     "stackblur-canvas": "2.3.0",
97
     "stackblur-canvas": "2.3.0",
98
     "styled-components": "3.4.9",
98
     "styled-components": "3.4.9",

react/features/base/conference/middleware.js → react/features/base/conference/middleware.any.js 查看文件


+ 74
- 0
react/features/base/conference/middleware.native.js 查看文件

1
+// @flow
2
+
3
+import { setPictureInPictureDisabled } from '../../mobile/picture-in-picture/functions';
4
+import { setAudioOnly } from '../audio-only';
5
+import JitsiMeetJS from '../lib-jitsi-meet';
6
+import { MiddlewareRegistry } from '../redux';
7
+import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
8
+import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from '../tracks/actions';
9
+import { getLocalVideoTrack, isLocalVideoTrackDesktop } from '../tracks/functions';
10
+
11
+import './middleware.any';
12
+
13
+MiddlewareRegistry.register(store => next => action => {
14
+    switch (action.type) {
15
+    case TOGGLE_SCREENSHARING: {
16
+        _toggleScreenSharing(store);
17
+        break;
18
+    }
19
+    }
20
+
21
+    return next(action);
22
+});
23
+
24
+/**
25
+ * Toggles screen sharing.
26
+ *
27
+ * @private
28
+ * @param {Store} store - The redux.
29
+ * @returns {void}
30
+ */
31
+function _toggleScreenSharing(store) {
32
+    const { dispatch, getState } = store;
33
+    const state = getState();
34
+
35
+    const isSharing = isLocalVideoTrackDesktop(state);
36
+
37
+    if (isSharing) {
38
+        dispatch(destroyLocalDesktopTrackIfExists());
39
+    } else {
40
+        _startScreenSharing(dispatch, state);
41
+    }
42
+}
43
+
44
+/**
45
+ * Creates desktop track and replaces the local one.
46
+ *
47
+ * @private
48
+ * @param {Dispatch} dispatch - The redux {@code dispatch} function.
49
+ * @param {Object} state - The redux state.
50
+ * @returns {void}
51
+ */
52
+function _startScreenSharing(dispatch, state) {
53
+    setPictureInPictureDisabled(true);
54
+
55
+    JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] })
56
+    .then(tracks => {
57
+        const track = tracks[0];
58
+        const currentLocalTrack = getLocalVideoTrack(state['features/base/tracks']);
59
+        const currentJitsiTrack = currentLocalTrack && currentLocalTrack.jitsiTrack;
60
+
61
+        dispatch(replaceLocalTrack(currentJitsiTrack, track));
62
+
63
+        const { enabled: audioOnly } = state['features/base/audio-only'];
64
+
65
+        if (audioOnly) {
66
+            dispatch(setAudioOnly(false));
67
+        }
68
+    })
69
+    .catch(error => {
70
+        console.log('ERROR creating ScreeSharing stream ', error);
71
+
72
+        setPictureInPictureDisabled(false);
73
+    });
74
+}

+ 23
- 0
react/features/base/conference/middleware.web.js 查看文件

1
+// @flow
2
+
3
+import UIEvents from '../../../../service/UI/UIEvents';
4
+import { MiddlewareRegistry } from '../redux';
5
+import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
6
+
7
+import './middleware.any';
8
+
9
+declare var APP: Object;
10
+
11
+MiddlewareRegistry.register((/* store */) => next => action => {
12
+    switch (action.type) {
13
+    case TOGGLE_SCREENSHARING: {
14
+        if (typeof APP === 'object') {
15
+            APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
16
+        }
17
+
18
+        break;
19
+    }
20
+    }
21
+
22
+    return next(action);
23
+});

+ 2
- 1
react/features/base/lastn/middleware.js 查看文件

16
     getParticipantCount
16
     getParticipantCount
17
 } from '../participants/functions';
17
 } from '../participants/functions';
18
 import { MiddlewareRegistry } from '../redux';
18
 import { MiddlewareRegistry } from '../redux';
19
+import { isLocalVideoTrackDesktop } from '../tracks/functions';
19
 
20
 
20
 import { limitLastN } from './functions';
21
 import { limitLastN } from './functions';
21
 import logger from './logger';
22
 import logger from './logger';
78
     }
79
     }
79
 
80
 
80
     if (typeof appState !== 'undefined' && appState !== 'active') {
81
     if (typeof appState !== 'undefined' && appState !== 'active') {
81
-        lastN = 0;
82
+        lastN = isLocalVideoTrackDesktop(state) ? 1 : 0;
82
     } else if (audioOnly) {
83
     } else if (audioOnly) {
83
         const { screenShares, tileViewEnabled } = state['features/video-layout'];
84
         const { screenShares, tileViewEnabled } = state['features/video-layout'];
84
         const largeVideoParticipantId = state['features/large-video'].participantId;
85
         const largeVideoParticipantId = state['features/large-video'].participantId;

+ 8
- 6
react/features/base/media/middleware.js 查看文件

13
 import JitsiMeetJS from '../lib-jitsi-meet';
13
 import JitsiMeetJS from '../lib-jitsi-meet';
14
 import { MiddlewareRegistry } from '../redux';
14
 import { MiddlewareRegistry } from '../redux';
15
 import { getPropertyValue } from '../settings';
15
 import { getPropertyValue } from '../settings';
16
-import { setTrackMuted, TRACK_ADDED } from '../tracks';
16
+import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
17
 
17
 
18
 import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
18
 import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
19
 import {
19
 import {
73
  * @private
73
  * @private
74
  * @returns {Object} The value returned by {@code next(action)}.
74
  * @returns {Object} The value returned by {@code next(action)}.
75
  */
75
  */
76
-function _appStateChanged({ dispatch }, next, action) {
77
-    const { appState } = action;
78
-    const mute = appState !== 'active'; // Note that 'background' and 'inactive' are treated equal.
76
+function _appStateChanged({ dispatch, getState }, next, action) {
77
+    if (navigator.product === 'ReactNative') {
78
+        const { appState } = action;
79
+        const mute = appState !== 'active' && !isLocalVideoTrackDesktop(getState());
79
 
80
 
80
-    sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
81
+        sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
81
 
82
 
82
-    dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
83
+        dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
84
+    }
83
 
85
 
84
     return next(action);
86
     return next(action);
85
 }
87
 }

+ 27
- 2
react/features/base/tracks/actions.js 查看文件

9
     MEDIA_TYPE,
9
     MEDIA_TYPE,
10
     setAudioMuted,
10
     setAudioMuted,
11
     setVideoMuted,
11
     setVideoMuted,
12
-    VIDEO_MUTISM_AUTHORITY
12
+    VIDEO_MUTISM_AUTHORITY,
13
+    VIDEO_TYPE
13
 } from '../media';
14
 } from '../media';
14
 import { getLocalParticipant } from '../participants';
15
 import { getLocalParticipant } from '../participants';
15
 
16
 
24
     TRACK_UPDATED,
25
     TRACK_UPDATED,
25
     TRACK_WILL_CREATE
26
     TRACK_WILL_CREATE
26
 } from './actionTypes';
27
 } from './actionTypes';
27
-import { createLocalTracksF, getLocalTrack, getLocalTracks, getTrackByJitsiTrack } from './functions';
28
+import {
29
+    createLocalTracksF,
30
+    getLocalTrack,
31
+    getLocalTracks,
32
+    getLocalVideoTrack,
33
+    getTrackByJitsiTrack
34
+} from './functions';
28
 import logger from './logger';
35
 import logger from './logger';
29
 
36
 
30
 /**
37
 /**
40
     return (dispatch, getState) => {
47
     return (dispatch, getState) => {
41
         const state = getState();
48
         const state = getState();
42
 
49
 
50
+        dispatch(destroyLocalDesktopTrackIfExists());
51
+
43
         if (desiredTypes.length === 0) {
52
         if (desiredTypes.length === 0) {
44
             const { audio, video } = state['features/base/media'];
53
             const { audio, video } = state['features/base/media'];
45
 
54
 
663
     };
672
     };
664
 }
673
 }
665
 
674
 
675
+/**
676
+ * If thee local track if of type Desktop, it calls _disposeAndRemoveTracks) on it.
677
+ *
678
+ * @returns {Function}
679
+ */
680
+export function destroyLocalDesktopTrackIfExists() {
681
+    return (dispatch, getState) => {
682
+        const videoTrack = getLocalVideoTrack(getState()['features/base/tracks']);
683
+        const isDesktopTrack = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
684
+
685
+        if (isDesktopTrack) {
686
+            dispatch(_disposeAndRemoveTracks([ videoTrack.jitsiTrack ]));
687
+        }
688
+    };
689
+}
690
+
666
 /**
691
 /**
667
  * Sets UID of the displayed no data from source notification. Used to track
692
  * Sets UID of the displayed no data from source notification. Used to track
668
  * if the notification was previously displayed in this context.
693
  * if the notification was previously displayed in this context.

+ 14
- 1
react/features/base/tracks/functions.js 查看文件

1
 /* global APP */
1
 /* global APP */
2
 
2
 
3
 import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
3
 import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
4
-import { MEDIA_TYPE, setAudioMuted } from '../media';
4
+import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media';
5
 import {
5
 import {
6
     getUserSelectedCameraDeviceId,
6
     getUserSelectedCameraDeviceId,
7
     getUserSelectedMicDeviceId
7
     getUserSelectedMicDeviceId
383
     return !track || track.muted;
383
     return !track || track.muted;
384
 }
384
 }
385
 
385
 
386
+/**
387
+ * Checks if the local video track is of type DESKtOP.
388
+ *
389
+ * @param {Object} state - The redux state.
390
+ * @returns {boolean}
391
+ */
392
+export function isLocalVideoTrackDesktop(state) {
393
+    const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
394
+
395
+    return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
396
+}
397
+
398
+
386
 /**
399
 /**
387
  * Returns true if the remote track of the given media type and the given
400
  * Returns true if the remote track of the given media type and the given
388
  * participant is muted, false otherwise.
401
  * participant is muted, false otherwise.

+ 2
- 1
react/features/mobile/picture-in-picture/components/PictureInPictureButton.js 查看文件

7
 import { IconMenuDown } from '../../../base/icons';
7
 import { IconMenuDown } from '../../../base/icons';
8
 import { connect } from '../../../base/redux';
8
 import { connect } from '../../../base/redux';
9
 import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
9
 import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
10
+import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions';
10
 import { enterPictureInPicture } from '../actions';
11
 import { enterPictureInPicture } from '../actions';
11
 
12
 
12
 type Props = AbstractButtonProps & {
13
 type Props = AbstractButtonProps & {
63
  */
64
  */
64
 function _mapStateToProps(state): Object {
65
 function _mapStateToProps(state): Object {
65
     const flag = Boolean(getFeatureFlag(state, PIP_ENABLED));
66
     const flag = Boolean(getFeatureFlag(state, PIP_ENABLED));
66
-    let enabled = flag;
67
+    let enabled = flag && !isLocalVideoTrackDesktop(state);
67
 
68
 
68
     // Override flag for Android, since it might be unsupported.
69
     // Override flag for Android, since it might be unsupported.
69
     if (Platform.OS === 'android' && !NativeModules.PictureInPicture.SUPPORTED) {
70
     if (Platform.OS === 'android' && !NativeModules.PictureInPicture.SUPPORTED) {

+ 15
- 0
react/features/mobile/picture-in-picture/functions.js 查看文件

1
+// @flow
2
+
3
+import { NativeModules } from 'react-native';
4
+
5
+/**
6
+ * Enabled/Disables the PictureInPicture mode in PiP native module.
7
+ *
8
+ * @param {boolean} disabled - Whether the PiP mode should be disabled.
9
+ * @returns {void}
10
+ */
11
+export function setPictureInPictureDisabled(disabled: boolean) {
12
+    const { PictureInPicture } = NativeModules;
13
+
14
+    PictureInPicture.setPictureInPictureDisabled(disabled);
15
+}

+ 1
- 0
react/features/mobile/picture-in-picture/index.js 查看文件

1
 export * from './actions';
1
 export * from './actions';
2
 export * from './actionTypes';
2
 export * from './actionTypes';
3
 export * from './components';
3
 export * from './components';
4
+export * from './functions';

+ 2
- 2
react/features/toolbox/components/VideoMuteButton.js 查看文件

9
     sendAnalytics
9
     sendAnalytics
10
 } from '../../analytics';
10
 } from '../../analytics';
11
 import { setAudioOnly } from '../../base/audio-only';
11
 import { setAudioOnly } from '../../base/audio-only';
12
-import { hasAvailableDevices } from '../../base/devices';
13
 import { translate } from '../../base/i18n';
12
 import { translate } from '../../base/i18n';
14
 import {
13
 import {
15
     VIDEO_MUTISM_AUTHORITY,
14
     VIDEO_MUTISM_AUTHORITY,
19
 import { AbstractVideoMuteButton } from '../../base/toolbox/components';
18
 import { AbstractVideoMuteButton } from '../../base/toolbox/components';
20
 import type { AbstractButtonProps } from '../../base/toolbox/components';
19
 import type { AbstractButtonProps } from '../../base/toolbox/components';
21
 import { getLocalVideoType, isLocalCameraTrackMuted } from '../../base/tracks';
20
 import { getLocalVideoType, isLocalCameraTrackMuted } from '../../base/tracks';
21
+import { isVideoMuteButtonDisabled } from '../functions';
22
 
22
 
23
 declare var APP: Object;
23
 declare var APP: Object;
24
 
24
 
190
 
190
 
191
     return {
191
     return {
192
         _audioOnly: Boolean(audioOnly),
192
         _audioOnly: Boolean(audioOnly),
193
-        _videoDisabled: !hasAvailableDevices(state, 'videoInput'),
193
+        _videoDisabled: isVideoMuteButtonDisabled(state),
194
         _videoMediaType: getLocalVideoType(tracks),
194
         _videoMediaType: getLocalVideoType(tracks),
195
         _videoMuted: isLocalCameraTrackMuted(tracks)
195
         _videoMuted: isLocalCameraTrackMuted(tracks)
196
     };
196
     };

+ 2
- 0
react/features/toolbox/components/native/OverflowMenu.js 查看文件

23
 import AudioOnlyButton from './AudioOnlyButton';
23
 import AudioOnlyButton from './AudioOnlyButton';
24
 import MoreOptionsButton from './MoreOptionsButton';
24
 import MoreOptionsButton from './MoreOptionsButton';
25
 import RaiseHandButton from './RaiseHandButton';
25
 import RaiseHandButton from './RaiseHandButton';
26
+import ScreenSharingButton from './ScreenSharingButton.js';
26
 import ToggleCameraButton from './ToggleCameraButton';
27
 import ToggleCameraButton from './ToggleCameraButton';
27
 import styles from './styles';
28
 import styles from './styles';
28
 
29
 
131
                 <AudioOnlyButton { ...buttonProps } />
132
                 <AudioOnlyButton { ...buttonProps } />
132
                 <RaiseHandButton { ...buttonProps } />
133
                 <RaiseHandButton { ...buttonProps } />
133
                 <LobbyModeButton { ...buttonProps } />
134
                 <LobbyModeButton { ...buttonProps } />
135
+                <ScreenSharingButton { ...buttonProps } />
134
                 <MoreOptionsButton { ...moreOptionsButtonProps } />
136
                 <MoreOptionsButton { ...moreOptionsButtonProps } />
135
                 <Collapsible collapsed = { !showMore }>
137
                 <Collapsible collapsed = { !showMore }>
136
                     <ToggleCameraButton { ...buttonProps } />
138
                     <ToggleCameraButton { ...buttonProps } />

+ 77
- 0
react/features/toolbox/components/native/ScreenSharingButton.js 查看文件

1
+// @flow
2
+
3
+import { Platform } from 'react-native';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { IconShareDesktop } from '../../../base/icons';
7
+import { connect } from '../../../base/redux';
8
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
9
+import { toggleScreensharing, isLocalVideoTrackDesktop } from '../../../base/tracks';
10
+
11
+/**
12
+ * The type of the React {@code Component} props of {@link ScreenSharingButton}.
13
+ */
14
+type Props = AbstractButtonProps & {
15
+
16
+    /**
17
+     * Whether video is currently muted or not.
18
+     */
19
+    _screensharing: boolean,
20
+
21
+    /**
22
+     * The redux {@code dispatch} function.
23
+     */
24
+    dispatch: Function
25
+};
26
+
27
+/**
28
+ * An implementation of a button for toggling screen sharing.
29
+ */
30
+class ScreenSharingButton extends AbstractButton<Props, *> {
31
+    accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
32
+    icon = IconShareDesktop;
33
+    label = 'toolbar.startScreenSharing';
34
+    toggledLabel = 'toolbar.stopScreenSharing';
35
+
36
+    /**
37
+     * Handles clicking / pressing the button.
38
+     *
39
+     * @override
40
+     * @protected
41
+     * @returns {void}
42
+     */
43
+    _handleClick() {
44
+        this.props.dispatch(toggleScreensharing());
45
+    }
46
+
47
+    /**
48
+     * Indicates whether this button is in toggled state or not.
49
+     *
50
+     * @override
51
+     * @protected
52
+     * @returns {boolean}
53
+     */
54
+    _isToggled() {
55
+        return this.props._screensharing;
56
+    }
57
+}
58
+
59
+/**
60
+ * Maps (parts of) the redux state to the associated props for the
61
+ * {@code ToggleCameraButton} component.
62
+ *
63
+ * @param {Object} state - The Redux state.
64
+ * @private
65
+ * @returns {{
66
+ *     _disabled: boolean,
67
+ *     _screensharing: boolean
68
+ * }}
69
+ */
70
+function _mapStateToProps(state): Object {
71
+    return {
72
+        _screensharing: isLocalVideoTrackDesktop(state),
73
+        visible: Platform.OS === 'android'
74
+    };
75
+}
76
+
77
+export default translate(connect(_mapStateToProps)(ScreenSharingButton));

+ 12
- 0
react/features/toolbox/functions.native.js 查看文件

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { hasAvailableDevices } from '../base/devices';
3
 import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag } from '../base/flags';
4
 import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag } from '../base/flags';
4
 import { toState } from '../base/redux';
5
 import { toState } from '../base/redux';
6
+import { isLocalVideoTrackDesktop } from '../base/tracks';
5
 
7
 
6
 /**
8
 /**
7
  * Returns true if the toolbox is visible.
9
  * Returns true if the toolbox is visible.
18
 
20
 
19
     return enabled && (alwaysVisible || visible || participantCount === 1 || flag);
21
     return enabled && (alwaysVisible || visible || participantCount === 1 || flag);
20
 }
22
 }
23
+
24
+/**
25
+ * Indicates if the video mute button is disabled or not.
26
+ *
27
+ * @param {string} state - The state from the Redux store.
28
+ * @returns {boolean}
29
+ */
30
+export function isVideoMuteButtonDisabled(state: Object) {
31
+    return !hasAvailableDevices(state, 'videoInput') || isLocalVideoTrackDesktop(state);
32
+}

+ 10
- 0
react/features/toolbox/functions.web.js 查看文件

77
 export function isVideoSettingsButtonDisabled(state: Object) {
77
 export function isVideoSettingsButtonDisabled(state: Object) {
78
     return !hasAvailableDevices(state, 'videoInput');
78
     return !hasAvailableDevices(state, 'videoInput');
79
 }
79
 }
80
+
81
+/**
82
+ * Indicates if the video mute button is disabled or not.
83
+ *
84
+ * @param {string} state - The state from the Redux store.
85
+ * @returns {boolean}
86
+ */
87
+export function isVideoMuteButtonDisabled(state: Object) {
88
+    return !hasAvailableDevices(state, 'videoInput');
89
+}

+ 3
- 0
react/features/welcome/components/WelcomePage.native.js 查看文件

19
 import { ColorPalette } from '../../base/styles';
19
 import { ColorPalette } from '../../base/styles';
20
 import {
20
 import {
21
     createDesiredLocalTracks,
21
     createDesiredLocalTracks,
22
+    destroyLocalDesktopTrackIfExists,
22
     destroyLocalTracks
23
     destroyLocalTracks
23
 } from '../../base/tracks';
24
 } from '../../base/tracks';
24
 import { HelpView } from '../../help';
25
 import { HelpView } from '../../help';
81
         if (this.props._settings.startAudioOnly) {
82
         if (this.props._settings.startAudioOnly) {
82
             dispatch(destroyLocalTracks());
83
             dispatch(destroyLocalTracks());
83
         } else {
84
         } else {
85
+            dispatch(destroyLocalDesktopTrackIfExists());
86
+
84
             // Make sure we don't request the permission for the camera from
87
             // Make sure we don't request the permission for the camera from
85
             // the start. We will, however, create a video track iff the user
88
             // the start. We will, however, create a video track iff the user
86
             // already granted the permission.
89
             // already granted the permission.

正在加载...
取消
保存