|
|
@@ -93,10 +93,15 @@ import {
|
|
93
|
93
|
participantRoleChanged,
|
|
94
|
94
|
participantUpdated
|
|
95
|
95
|
} from './react/features/base/participants';
|
|
96
|
|
-import { updateSettings } from './react/features/base/settings';
|
|
97
|
96
|
import {
|
|
|
97
|
+ getUserSelectedCameraDeviceId,
|
|
|
98
|
+ updateSettings
|
|
|
99
|
+} from './react/features/base/settings';
|
|
|
100
|
+import {
|
|
|
101
|
+ createLocalPresenterTrack,
|
|
98
|
102
|
createLocalTracksF,
|
|
99
|
103
|
destroyLocalTracks,
|
|
|
104
|
+ isLocalVideoTrackMuted,
|
|
100
|
105
|
isLocalTrackMuted,
|
|
101
|
106
|
isUserInteractionRequiredForUnmute,
|
|
102
|
107
|
replaceLocalTrack,
|
|
|
@@ -113,6 +118,7 @@ import {
|
|
113
|
118
|
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
|
|
114
|
119
|
import { suspendDetected } from './react/features/power-monitor';
|
|
115
|
120
|
import { setSharedVideoStatus } from './react/features/shared-video';
|
|
|
121
|
+import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
|
116
|
122
|
import { endpointMessageReceived } from './react/features/subtitles';
|
|
117
|
123
|
|
|
118
|
124
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
|
|
@@ -437,6 +443,11 @@ export default {
|
|
437
|
443
|
*/
|
|
438
|
444
|
localAudio: null,
|
|
439
|
445
|
|
|
|
446
|
+ /**
|
|
|
447
|
+ * The local presenter video track (if any).
|
|
|
448
|
+ */
|
|
|
449
|
+ localPresenterVideo: null,
|
|
|
450
|
+
|
|
440
|
451
|
/**
|
|
441
|
452
|
* The local video track (if any).
|
|
442
|
453
|
* FIXME tracks from redux store should be the single source of truth, but
|
|
|
@@ -722,9 +733,8 @@ export default {
|
|
722
|
733
|
isLocalVideoMuted() {
|
|
723
|
734
|
// If the tracks are not ready, read from base/media state
|
|
724
|
735
|
return this._localTracksInitialized
|
|
725
|
|
- ? isLocalTrackMuted(
|
|
726
|
|
- APP.store.getState()['features/base/tracks'],
|
|
727
|
|
- MEDIA_TYPE.VIDEO)
|
|
|
736
|
+ ? isLocalVideoTrackMuted(
|
|
|
737
|
+ APP.store.getState()['features/base/tracks'])
|
|
728
|
738
|
: isVideoMutedByUser(APP.store);
|
|
729
|
739
|
},
|
|
730
|
740
|
|
|
|
@@ -798,6 +808,55 @@ export default {
|
|
798
|
808
|
this.muteAudio(!this.isLocalAudioMuted(), showUI);
|
|
799
|
809
|
},
|
|
800
|
810
|
|
|
|
811
|
+ /**
|
|
|
812
|
+ * Simulates toolbar button click for presenter video mute. Used by
|
|
|
813
|
+ * shortcuts and API.
|
|
|
814
|
+ * @param mute true for mute and false for unmute.
|
|
|
815
|
+ * @param {boolean} [showUI] when set to false will not display any error
|
|
|
816
|
+ * dialogs in case of media permissions error.
|
|
|
817
|
+ */
|
|
|
818
|
+ async mutePresenterVideo(mute, showUI = true) {
|
|
|
819
|
+ const maybeShowErrorDialog = error => {
|
|
|
820
|
+ showUI && APP.store.dispatch(notifyCameraError(error));
|
|
|
821
|
+ };
|
|
|
822
|
+
|
|
|
823
|
+ if (mute) {
|
|
|
824
|
+ try {
|
|
|
825
|
+ await this.localVideo.setEffect(undefined);
|
|
|
826
|
+ APP.store.dispatch(
|
|
|
827
|
+ setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
|
|
|
828
|
+ this._untoggleScreenSharing
|
|
|
829
|
+ = this._turnScreenSharingOff.bind(this, false);
|
|
|
830
|
+ } catch (err) {
|
|
|
831
|
+ logger.error('Failed to mute the Presenter video');
|
|
|
832
|
+ }
|
|
|
833
|
+
|
|
|
834
|
+ return;
|
|
|
835
|
+ }
|
|
|
836
|
+ const { height } = this.localVideo.track.getSettings();
|
|
|
837
|
+ const defaultCamera
|
|
|
838
|
+ = getUserSelectedCameraDeviceId(APP.store.getState());
|
|
|
839
|
+ let effect;
|
|
|
840
|
+
|
|
|
841
|
+ try {
|
|
|
842
|
+ effect = await this._createPresenterStreamEffect(height,
|
|
|
843
|
+ defaultCamera);
|
|
|
844
|
+ } catch (err) {
|
|
|
845
|
+ logger.error('Failed to unmute Presenter Video');
|
|
|
846
|
+ maybeShowErrorDialog(err);
|
|
|
847
|
+
|
|
|
848
|
+ return;
|
|
|
849
|
+ }
|
|
|
850
|
+ try {
|
|
|
851
|
+ await this.localVideo.setEffect(effect);
|
|
|
852
|
+ APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
|
|
|
853
|
+ this._untoggleScreenSharing
|
|
|
854
|
+ = this._turnScreenSharingOff.bind(this, true);
|
|
|
855
|
+ } catch (err) {
|
|
|
856
|
+ logger.error('Failed to apply the Presenter effect', err);
|
|
|
857
|
+ }
|
|
|
858
|
+ },
|
|
|
859
|
+
|
|
801
|
860
|
/**
|
|
802
|
861
|
* Simulates toolbar button click for video mute. Used by shortcuts and API.
|
|
803
|
862
|
* @param mute true for mute and false for unmute.
|
|
|
@@ -812,6 +871,10 @@ export default {
|
|
812
|
871
|
return;
|
|
813
|
872
|
}
|
|
814
|
873
|
|
|
|
874
|
+ if (this.isSharingScreen) {
|
|
|
875
|
+ return this.mutePresenterVideo(mute);
|
|
|
876
|
+ }
|
|
|
877
|
+
|
|
815
|
878
|
// If not ready to modify track's state yet adjust the base/media
|
|
816
|
879
|
if (!this._localTracksInitialized) {
|
|
817
|
880
|
// This will only modify base/media.video.muted which is then synced
|
|
|
@@ -1351,7 +1414,7 @@ export default {
|
|
1351
|
1414
|
* in case it fails.
|
|
1352
|
1415
|
* @private
|
|
1353
|
1416
|
*/
|
|
1354
|
|
- _turnScreenSharingOff(didHaveVideo, wasVideoMuted) {
|
|
|
1417
|
+ _turnScreenSharingOff(didHaveVideo) {
|
|
1355
|
1418
|
this._untoggleScreenSharing = null;
|
|
1356
|
1419
|
this.videoSwitchInProgress = true;
|
|
1357
|
1420
|
const { receiver } = APP.remoteControl;
|
|
|
@@ -1369,13 +1432,7 @@ export default {
|
|
1369
|
1432
|
.then(([ stream ]) => this.useVideoStream(stream))
|
|
1370
|
1433
|
.then(() => {
|
|
1371
|
1434
|
sendAnalytics(createScreenSharingEvent('stopped'));
|
|
1372
|
|
- logger.log('Screen sharing stopped, switching to video.');
|
|
1373
|
|
-
|
|
1374
|
|
- if (!this.localVideo && wasVideoMuted) {
|
|
1375
|
|
- return Promise.reject('No local video to be muted!');
|
|
1376
|
|
- } else if (wasVideoMuted && this.localVideo) {
|
|
1377
|
|
- return this.localVideo.mute();
|
|
1378
|
|
- }
|
|
|
1435
|
+ logger.log('Screen sharing stopped.');
|
|
1379
|
1436
|
})
|
|
1380
|
1437
|
.catch(error => {
|
|
1381
|
1438
|
logger.error('failed to switch back to local video', error);
|
|
|
@@ -1390,6 +1447,16 @@ export default {
|
|
1390
|
1447
|
promise = this.useVideoStream(null);
|
|
1391
|
1448
|
}
|
|
1392
|
1449
|
|
|
|
1450
|
+ // mute the presenter track if it exists.
|
|
|
1451
|
+ if (this.localPresenterVideo) {
|
|
|
1452
|
+ APP.store.dispatch(
|
|
|
1453
|
+ setVideoMuted(true, MEDIA_TYPE.PRESENTER));
|
|
|
1454
|
+ this.localPresenterVideo.dispose();
|
|
|
1455
|
+ APP.store.dispatch(
|
|
|
1456
|
+ trackRemoved(this.localPresenterVideo));
|
|
|
1457
|
+ this.localPresenterVideo = null;
|
|
|
1458
|
+ }
|
|
|
1459
|
+
|
|
1393
|
1460
|
return promise.then(
|
|
1394
|
1461
|
() => {
|
|
1395
|
1462
|
this.videoSwitchInProgress = false;
|
|
|
@@ -1415,7 +1482,7 @@ export default {
|
|
1415
|
1482
|
* 'window', etc.).
|
|
1416
|
1483
|
* @return {Promise.<T>}
|
|
1417
|
1484
|
*/
|
|
1418
|
|
- toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
|
|
|
1485
|
+ async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
|
|
1419
|
1486
|
if (this.videoSwitchInProgress) {
|
|
1420
|
1487
|
return Promise.reject('Switch in progress.');
|
|
1421
|
1488
|
}
|
|
|
@@ -1429,7 +1496,41 @@ export default {
|
|
1429
|
1496
|
}
|
|
1430
|
1497
|
|
|
1431
|
1498
|
if (toggle) {
|
|
1432
|
|
- return this._switchToScreenSharing(options);
|
|
|
1499
|
+ const wasVideoMuted = this.isLocalVideoMuted();
|
|
|
1500
|
+
|
|
|
1501
|
+ try {
|
|
|
1502
|
+ await this._switchToScreenSharing(options);
|
|
|
1503
|
+ } catch (err) {
|
|
|
1504
|
+ logger.error('Failed to switch to screensharing', err);
|
|
|
1505
|
+
|
|
|
1506
|
+ return;
|
|
|
1507
|
+ }
|
|
|
1508
|
+ if (wasVideoMuted) {
|
|
|
1509
|
+ return;
|
|
|
1510
|
+ }
|
|
|
1511
|
+ const { height } = this.localVideo.track.getSettings();
|
|
|
1512
|
+ const defaultCamera
|
|
|
1513
|
+ = getUserSelectedCameraDeviceId(APP.store.getState());
|
|
|
1514
|
+ let effect;
|
|
|
1515
|
+
|
|
|
1516
|
+ try {
|
|
|
1517
|
+ effect = await this._createPresenterStreamEffect(
|
|
|
1518
|
+ height, defaultCamera);
|
|
|
1519
|
+ } catch (err) {
|
|
|
1520
|
+ logger.error('Failed to create the presenter effect');
|
|
|
1521
|
+
|
|
|
1522
|
+ return;
|
|
|
1523
|
+ }
|
|
|
1524
|
+ try {
|
|
|
1525
|
+ await this.localVideo.setEffect(effect);
|
|
|
1526
|
+ muteLocalVideo(false);
|
|
|
1527
|
+
|
|
|
1528
|
+ return;
|
|
|
1529
|
+ } catch (err) {
|
|
|
1530
|
+ logger.error('Failed to create the presenter effect', err);
|
|
|
1531
|
+
|
|
|
1532
|
+ return;
|
|
|
1533
|
+ }
|
|
1433
|
1534
|
}
|
|
1434
|
1535
|
|
|
1435
|
1536
|
return this._untoggleScreenSharing
|
|
|
@@ -1455,7 +1556,6 @@ export default {
|
|
1455
|
1556
|
let externalInstallation = false;
|
|
1456
|
1557
|
let DSExternalInstallationInProgress = false;
|
|
1457
|
1558
|
const didHaveVideo = Boolean(this.localVideo);
|
|
1458
|
|
- const wasVideoMuted = this.isLocalVideoMuted();
|
|
1459
|
1559
|
|
|
1460
|
1560
|
const getDesktopStreamPromise = options.desktopStream
|
|
1461
|
1561
|
? Promise.resolve([ options.desktopStream ])
|
|
|
@@ -1506,8 +1606,7 @@ export default {
|
|
1506
|
1606
|
// Stores the "untoggle" handler which remembers whether was
|
|
1507
|
1607
|
// there any video before and whether was it muted.
|
|
1508
|
1608
|
this._untoggleScreenSharing
|
|
1509
|
|
- = this._turnScreenSharingOff
|
|
1510
|
|
- .bind(this, didHaveVideo, wasVideoMuted);
|
|
|
1609
|
+ = this._turnScreenSharingOff.bind(this, didHaveVideo);
|
|
1511
|
1610
|
desktopStream.on(
|
|
1512
|
1611
|
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
|
1513
|
1612
|
() => {
|
|
|
@@ -1532,6 +1631,45 @@ export default {
|
|
1532
|
1631
|
});
|
|
1533
|
1632
|
},
|
|
1534
|
1633
|
|
|
|
1634
|
+ /**
|
|
|
1635
|
+ * Creates a new instance of presenter effect. A new video track is created
|
|
|
1636
|
+ * using the new set of constraints that are calculated based on
|
|
|
1637
|
+ * the height of the desktop that is being currently shared.
|
|
|
1638
|
+ *
|
|
|
1639
|
+ * @param {number} height - The height of the desktop stream that is being
|
|
|
1640
|
+ * currently shared.
|
|
|
1641
|
+ * @param {string} cameraDeviceId - The device id of the camera to be used.
|
|
|
1642
|
+ * @return {Promise<JitsiStreamPresenterEffect>} - A promise resolved with
|
|
|
1643
|
+ * {@link JitsiStreamPresenterEffect} if it succeeds.
|
|
|
1644
|
+ */
|
|
|
1645
|
+ async _createPresenterStreamEffect(height, cameraDeviceId = null) {
|
|
|
1646
|
+ let presenterTrack;
|
|
|
1647
|
+
|
|
|
1648
|
+ try {
|
|
|
1649
|
+ presenterTrack = await createLocalPresenterTrack({
|
|
|
1650
|
+ cameraDeviceId
|
|
|
1651
|
+ },
|
|
|
1652
|
+ height);
|
|
|
1653
|
+ } catch (err) {
|
|
|
1654
|
+ logger.error('Failed to create a camera track for presenter', err);
|
|
|
1655
|
+
|
|
|
1656
|
+ return;
|
|
|
1657
|
+ }
|
|
|
1658
|
+ this.localPresenterVideo = presenterTrack;
|
|
|
1659
|
+ try {
|
|
|
1660
|
+ const effect = await createPresenterEffect(presenterTrack.stream);
|
|
|
1661
|
+
|
|
|
1662
|
+ APP.store.dispatch(trackAdded(this.localPresenterVideo));
|
|
|
1663
|
+
|
|
|
1664
|
+ return effect;
|
|
|
1665
|
+ } catch (err) {
|
|
|
1666
|
+ logger.error('Failed to create the presenter effect', err);
|
|
|
1667
|
+ APP.store.dispatch(
|
|
|
1668
|
+ setVideoMuted(true, MEDIA_TYPE.PRESENTER));
|
|
|
1669
|
+ APP.store.dispatch(notifyCameraError(err));
|
|
|
1670
|
+ }
|
|
|
1671
|
+ },
|
|
|
1672
|
+
|
|
1535
|
1673
|
/**
|
|
1536
|
1674
|
* Tries to switch to the screensharing mode by disposing camera stream and
|
|
1537
|
1675
|
* replacing it with a desktop one.
|
|
|
@@ -1992,36 +2130,56 @@ export default {
|
|
1992
|
2130
|
const videoWasMuted = this.isLocalVideoMuted();
|
|
1993
|
2131
|
|
|
1994
|
2132
|
sendAnalytics(createDeviceChangedEvent('video', 'input'));
|
|
1995
|
|
- createLocalTracksF({
|
|
1996
|
|
- devices: [ 'video' ],
|
|
1997
|
|
- cameraDeviceId,
|
|
1998
|
|
- micDeviceId: null
|
|
1999
|
|
- })
|
|
2000
|
|
- .then(([ stream ]) => {
|
|
2001
|
|
- // if we are in audio only mode or video was muted before
|
|
2002
|
|
- // changing device, then mute
|
|
2003
|
|
- if (this.isAudioOnly() || videoWasMuted) {
|
|
2004
|
|
- return stream.mute()
|
|
2005
|
|
- .then(() => stream);
|
|
2006
|
|
- }
|
|
2007
|
|
-
|
|
2008
|
|
- return stream;
|
|
2009
|
|
- })
|
|
2010
|
|
- .then(stream => {
|
|
2011
|
|
- // if we are screen sharing we do not want to stop it
|
|
2012
|
|
- if (this.isSharingScreen) {
|
|
2013
|
|
- return Promise.resolve();
|
|
2014
|
|
- }
|
|
2015
|
2133
|
|
|
2016
|
|
- return this.useVideoStream(stream);
|
|
2017
|
|
- })
|
|
2018
|
|
- .then(() => {
|
|
|
2134
|
+ // If both screenshare and video are in progress, restart the
|
|
|
2135
|
+ // presenter mode with the new camera device.
|
|
|
2136
|
+ if (this.isSharingScreen && !videoWasMuted) {
|
|
|
2137
|
+ const { height } = this.localVideo.track.getSettings();
|
|
|
2138
|
+
|
|
|
2139
|
+ // dispose the existing presenter track and create a new
|
|
|
2140
|
+ // camera track.
|
|
|
2141
|
+ APP.store.dispatch(setVideoMuted(true, MEDIA_TYPE.PRESENTER));
|
|
|
2142
|
+
|
|
|
2143
|
+ return this._createPresenterStreamEffect(height, cameraDeviceId)
|
|
|
2144
|
+ .then(effect => this.localVideo.setEffect(effect))
|
|
|
2145
|
+ .then(() => {
|
|
|
2146
|
+ muteLocalVideo(false);
|
|
|
2147
|
+ this.setVideoMuteStatus(false);
|
|
|
2148
|
+ logger.log('switched local video device');
|
|
|
2149
|
+ this._updateVideoDeviceId();
|
|
|
2150
|
+ })
|
|
|
2151
|
+ .catch(err => APP.store.dispatch(notifyCameraError(err)));
|
|
|
2152
|
+
|
|
|
2153
|
+ // If screenshare is in progress but video is muted,
|
|
|
2154
|
+ // update the default device id for video.
|
|
|
2155
|
+ } else if (this.isSharingScreen && videoWasMuted) {
|
|
2019
|
2156
|
logger.log('switched local video device');
|
|
2020
|
2157
|
this._updateVideoDeviceId();
|
|
2021
|
|
- })
|
|
2022
|
|
- .catch(err => {
|
|
2023
|
|
- APP.store.dispatch(notifyCameraError(err));
|
|
2024
|
|
- });
|
|
|
2158
|
+
|
|
|
2159
|
+ // if there is only video, switch to the new camera stream.
|
|
|
2160
|
+ } else {
|
|
|
2161
|
+ createLocalTracksF({
|
|
|
2162
|
+ devices: [ 'video' ],
|
|
|
2163
|
+ cameraDeviceId,
|
|
|
2164
|
+ micDeviceId: null
|
|
|
2165
|
+ })
|
|
|
2166
|
+ .then(([ stream ]) => {
|
|
|
2167
|
+ // if we are in audio only mode or video was muted before
|
|
|
2168
|
+ // changing device, then mute
|
|
|
2169
|
+ if (this.isAudioOnly() || videoWasMuted) {
|
|
|
2170
|
+ return stream.mute()
|
|
|
2171
|
+ .then(() => stream);
|
|
|
2172
|
+ }
|
|
|
2173
|
+
|
|
|
2174
|
+ return stream;
|
|
|
2175
|
+ })
|
|
|
2176
|
+ .then(stream => this.useVideoStream(stream))
|
|
|
2177
|
+ .then(() => {
|
|
|
2178
|
+ logger.log('switched local video device');
|
|
|
2179
|
+ this._updateVideoDeviceId();
|
|
|
2180
|
+ })
|
|
|
2181
|
+ .catch(err => APP.store.dispatch(notifyCameraError(err)));
|
|
|
2182
|
+ }
|
|
2025
|
2183
|
}
|
|
2026
|
2184
|
);
|
|
2027
|
2185
|
|