diff --git a/conference.js b/conference.js
index 1286327e6d54..57547e5b4349 100644
--- a/conference.js
+++ b/conference.js
@@ -6,6 +6,7 @@ import Logger from 'jitsi-meet-logger';
import { openConnection } from './connection';
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
+import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from './modules/UI/UIErrors';
import AuthHandler from './modules/UI/authentication/AuthHandler';
import UIUtil from './modules/UI/util/UIUtil';
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
@@ -126,6 +127,7 @@ import {
makePrecallTest
} from './react/features/prejoin';
import { disableReceiver, stopReceiver } from './react/features/remote-control';
+import { setScreenAudioShareState, isScreenAudioShared } from './react/features/screen-share/';
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
import { setSharedVideoStatus } from './react/features/shared-video/actions';
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
@@ -1546,6 +1548,8 @@ export default {
this._desktopAudioStream = undefined;
}
+ APP.store.dispatch(setScreenAudioShareState(false));
+
if (didHaveVideo) {
promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] }))
.then(([ stream ]) => {
@@ -1662,6 +1666,23 @@ export default {
= this._turnScreenSharingOff.bind(this, didHaveVideo);
const desktopVideoStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.VIDEO);
+ const dekstopAudioStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
+
+ if (dekstopAudioStream) {
+ dekstopAudioStream.on(
+ JitsiTrackEvents.LOCAL_TRACK_STOPPED,
+ () => {
+ logger.debug(`Local screensharing audio track stopped. ${this.isSharingScreen}`);
+
+ // Handle case where screen share was stopped from the browsers 'screen share in progress'
+ // window. If audio screen sharing is stopped via the normal UX flow this point shouldn't
+ // be reached.
+ isScreenAudioShared(APP.store.getState())
+ && this._untoggleScreenSharing
+ && this._untoggleScreenSharing();
+ }
+ );
+ }
if (desktopVideoStream) {
desktopVideoStream.on(
@@ -1830,14 +1851,28 @@ export default {
return this._createDesktopTrack(options)
.then(async streams => {
- const desktopVideoStream = streams.find(stream => stream.getType() === MEDIA_TYPE.VIDEO);
+ let desktopVideoStream = streams.find(stream => stream.getType() === MEDIA_TYPE.VIDEO);
+
+ this._desktopAudioStream = streams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
+
+ const { audioOnly = false } = options;
+
+ // If we're in audio only mode dispose of the video track otherwise the screensharing state will be
+ // inconsistent.
+ if (audioOnly) {
+ desktopVideoStream.dispose();
+ desktopVideoStream = undefined;
+
+ if (!this._desktopAudioStream) {
+ return Promise.reject(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK);
+ }
+ }
if (desktopVideoStream) {
logger.debug(`_switchToScreenSharing is using ${desktopVideoStream} for useVideoStream`);
await this.useVideoStream(desktopVideoStream);
}
- this._desktopAudioStream = streams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
if (this._desktopAudioStream) {
// If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing
@@ -1850,7 +1885,9 @@ export default {
// If no local stream is present ( i.e. no input audio devices) we use the screen share audio
// stream as we would use a regular stream.
await this.useAudioStream(this._desktopAudioStream);
+
}
+ APP.store.dispatch(setScreenAudioShareState(true));
}
})
.then(() => {
@@ -1918,6 +1955,9 @@ export default {
} else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
descriptionKey = 'dialog.screenSharingFailed';
titleKey = 'dialog.screenSharingFailedTitle';
+ } else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
+ descriptionKey = 'notify.screenShareNoAudio';
+ titleKey = 'notify.screenShareNoAudioTitle';
}
APP.UI.messageHandler.showError({
@@ -2409,7 +2449,9 @@ export default {
});
APP.UI.addListener(
- UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
+ UIEvents.TOGGLE_SCREENSHARING, audioOnly => {
+ this.toggleScreenSharing(undefined, { audioOnly });
+ }
);
/* eslint-disable max-params */
diff --git a/config.js b/config.js
index baefa32a0ab7..9d49d521e963 100644
--- a/config.js
+++ b/config.js
@@ -428,7 +428,7 @@ var config = {
// toolbarButtons: [
// 'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
// 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
- // 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
+ // 'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
// 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
// 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
// ],
diff --git a/lang/main.json b/lang/main.json
index 72a84c68b13c..4bf888a49327 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -511,6 +511,8 @@
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
"raisedHand": "{{name}} would like to speak.",
+ "screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
+ "screenShareNoAudioTitle": "Share audio was not chcked",
"somebody": "Somebody",
"startSilentTitle": "You joined with no audio output!",
"startSilentDescription": "Rejoin the meeting to enable audio",
@@ -752,6 +754,7 @@
"remoteVideoMute": "Disable camera of participant",
"security": "Security options",
"Settings": "Toggle settings",
+ "shareaudio": "Share audio",
"sharedvideo": "Toggle YouTube video sharing",
"shareRoom": "Invite someone",
"shareYourScreen": "Toggle screenshare",
@@ -811,6 +814,7 @@
"raiseYourHand": "Raise your hand",
"security": "Security options",
"Settings": "Settings",
+ "shareaudio": "Share audio",
"sharedvideo": "Share a YouTube video",
"shareRoom": "Invite someone",
"shortcuts": "View shortcuts",
diff --git a/modules/UI/UIErrors.js b/modules/UI/UIErrors.js
index 44f385daab72..7de7317d66e6 100644
--- a/modules/UI/UIErrors.js
+++ b/modules/UI/UIErrors.js
@@ -8,3 +8,10 @@
* @type {{FEEDBACK_REQUEST_IN_PROGRESS: string}}
*/
export const FEEDBACK_REQUEST_IN_PROGRESS = 'FeedbackRequestInProgress';
+
+/**
+ * Indicated an attempted audio only screen share session with no audio track present
+ *
+ * @type {{AUDIO_ONLY_SCREEN_SHARE_NO_TRACK: string}}
+ */
+export const AUDIO_ONLY_SCREEN_SHARE_NO_TRACK = 'AudioOnlyScreenShareNoTrack';
diff --git a/react/features/app/reducers.any.js b/react/features/app/reducers.any.js
index bf1dcbfa841d..060dd4e3be0b 100644
--- a/react/features/app/reducers.any.js
+++ b/react/features/app/reducers.any.js
@@ -44,6 +44,7 @@ import '../recent-list/reducer';
import '../recording/reducer';
import '../settings/reducer';
import '../subtitles/reducer';
+import '../screen-share/reducer';
import '../toolbox/reducer';
import '../transcribing/reducer';
import '../video-layout/reducer';
diff --git a/react/features/base/conference/middleware.web.js b/react/features/base/conference/middleware.web.js
index e696e85a82b9..55f0f2fc7ad1 100644
--- a/react/features/base/conference/middleware.web.js
+++ b/react/features/base/conference/middleware.web.js
@@ -32,7 +32,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
}
case TOGGLE_SCREENSHARING: {
if (typeof APP === 'object') {
- APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
+ APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING, action.audioOnly);
}
break;
diff --git a/react/features/base/config/constants.js b/react/features/base/config/constants.js
index db004c17b75c..b5165ceb722c 100644
--- a/react/features/base/config/constants.js
+++ b/react/features/base/config/constants.js
@@ -16,7 +16,7 @@ export const _CONFIG_STORE_PREFIX = 'config.js';
export const TOOLBAR_BUTTONS = [
'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
- 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
+ 'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
'security', 'toggle-camera'
diff --git a/react/features/base/environment/environment.js b/react/features/base/environment/environment.js
index 7f869be73071..d519d8d6783b 100644
--- a/react/features/base/environment/environment.js
+++ b/react/features/base/environment/environment.js
@@ -41,6 +41,15 @@ export function isBrowsersOptimal(browserName: string) {
.includes(browserName);
}
+/**
+ * Returns whether or not the current OS is Mac.
+ *
+ * @returns {boolean}
+ */
+export function isMacOS() {
+ return Platform.OS === 'macos';
+}
+
/**
* Returns whether or not the current browser or the list of passed in browsers
* is considered suboptimal. Suboptimal means it is a supported browser but has
diff --git a/react/features/base/icons/svg/index.js b/react/features/base/icons/svg/index.js
index 1befe06dc6b8..ec1b10f395ad 100644
--- a/react/features/base/icons/svg/index.js
+++ b/react/features/base/icons/svg/index.js
@@ -99,6 +99,7 @@ export { default as IconSignalLevel0 } from './signal_cellular_0.svg';
export { default as IconSignalLevel1 } from './signal_cellular_1.svg';
export { default as IconSignalLevel2 } from './signal_cellular_2.svg';
export { default as IconShare } from './share.svg';
+export { default as IconShareAudio } from './share-audio.svg';
export { default as IconShareDesktop } from './share-desktop.svg';
export { default as IconShareDoc } from './share-doc.svg';
export { default as IconShareVideo } from './shared-video.svg';
diff --git a/react/features/base/icons/svg/share-audio.svg b/react/features/base/icons/svg/share-audio.svg
new file mode 100644
index 000000000000..9d3f18016bd7
--- /dev/null
+++ b/react/features/base/icons/svg/share-audio.svg
@@ -0,0 +1,3 @@
+
diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js
index 5cfd4f5b2122..e00b8b31dfb8 100644
--- a/react/features/base/tracks/actions.js
+++ b/react/features/base/tracks/actions.js
@@ -257,13 +257,16 @@ export function showNoDataFromSourceVideoError(jitsiTrack) {
* Signals that the local participant is ending screensharing or beginning the
* screensharing flow.
*
+ * @param {boolean} audioOnly - Only share system audio.
* @returns {{
* type: TOGGLE_SCREENSHARING,
+ * audioOnly: boolean
* }}
*/
-export function toggleScreensharing() {
+export function toggleScreensharing(audioOnly = false) {
return {
- type: TOGGLE_SCREENSHARING
+ type: TOGGLE_SCREENSHARING,
+ audioOnly
};
}
diff --git a/react/features/base/tracks/middleware.js b/react/features/base/tracks/middleware.js
index b3747c6c60f8..b0b6800f573c 100644
--- a/react/features/base/tracks/middleware.js
+++ b/react/features/base/tracks/middleware.js
@@ -135,7 +135,7 @@ MiddlewareRegistry.register(store => next => action => {
case TOGGLE_SCREENSHARING:
if (typeof APP === 'object') {
- APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
+ APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING, action.audioOnly);
}
break;
diff --git a/react/features/screen-share/actionTypes.js b/react/features/screen-share/actionTypes.js
new file mode 100644
index 000000000000..a5d310a9c264
--- /dev/null
+++ b/react/features/screen-share/actionTypes.js
@@ -0,0 +1,12 @@
+// @flow
+
+/**
+ * Type of action which sets the current state of screen audio sharing.
+ *
+ * {
+ * type: SET_SCREEN_AUDIO_SHARE_STATE,
+ * isSharingAudio: boolean
+ * }
+ */
+export const SET_SCREEN_AUDIO_SHARE_STATE = 'SET_SCREEN_AUDIO_SHARE_STATE';
+
diff --git a/react/features/screen-share/actions.js b/react/features/screen-share/actions.js
new file mode 100644
index 000000000000..83ed8ad4f545
--- /dev/null
+++ b/react/features/screen-share/actions.js
@@ -0,0 +1,19 @@
+// @flow
+
+import { SET_SCREEN_AUDIO_SHARE_STATE } from './actionTypes';
+
+/**
+ * Updates the current known status of the shared video.
+ *
+ * @param {boolean} isSharingAudio - Is audio currently being shared or not.
+ * @returns {{
+ * type: SET_SCREEN_AUDIO_SHARE_STATE,
+ * isSharingAudio: boolean
+ * }}
+ */
+export function setScreenAudioShareState(isSharingAudio: boolean) {
+ return {
+ type: SET_SCREEN_AUDIO_SHARE_STATE,
+ isSharingAudio
+ };
+}
diff --git a/react/features/screen-share/functions.js b/react/features/screen-share/functions.js
new file mode 100644
index 000000000000..e1bccd6e7818
--- /dev/null
+++ b/react/features/screen-share/functions.js
@@ -0,0 +1,25 @@
+// @flow
+
+import { isMacOS } from '../base/environment';
+import { browser } from '../base/lib-jitsi-meet';
+
+
+/**
+ * State of audio sharing.
+ *
+ * @param {Object} state - The state of the application.
+ * @returns {boolean}
+ */
+export function isScreenAudioShared(state: Object) {
+ return state['features/screen-share'].isSharingAudio;
+}
+
+/**
+ * Returns the visibility of the audio only screen share button. Currently electron on mac os doesn't
+ * have support for this functionality.
+ *
+ * @returns {boolean}
+ */
+export function isScreenAudioSupported() {
+ return !(browser.isElectron() && isMacOS());
+}
diff --git a/react/features/screen-share/index.js b/react/features/screen-share/index.js
new file mode 100644
index 000000000000..7461a8bcfd9f
--- /dev/null
+++ b/react/features/screen-share/index.js
@@ -0,0 +1,4 @@
+export * from './actions';
+export * from './actionTypes';
+export * from './functions';
+
diff --git a/react/features/screen-share/reducer.js b/react/features/screen-share/reducer.js
new file mode 100644
index 000000000000..df88164084b1
--- /dev/null
+++ b/react/features/screen-share/reducer.js
@@ -0,0 +1,22 @@
+
+import { ReducerRegistry } from '../base/redux';
+
+import { SET_SCREEN_AUDIO_SHARE_STATE } from './actionTypes';
+
+/**
+ * Reduces the Redux actions of the feature features/screen-share.
+ */
+ReducerRegistry.register('features/screen-share', (state = {}, action) => {
+ const { isSharingAudio } = action;
+
+ switch (action.type) {
+ case SET_SCREEN_AUDIO_SHARE_STATE:
+ return {
+ ...state,
+ isSharingAudio
+ };
+
+ default:
+ return state;
+ }
+});
diff --git a/react/features/stream-effects/audio-mixer/AudioMixerEffect.js b/react/features/stream-effects/audio-mixer/AudioMixerEffect.js
index 4ddfbc0d50d4..bf25790cdf44 100644
--- a/react/features/stream-effects/audio-mixer/AudioMixerEffect.js
+++ b/react/features/stream-effects/audio-mixer/AudioMixerEffect.js
@@ -14,6 +14,16 @@ export class AudioMixerEffect {
*/
_mixAudio: Object;
+ /**
+ * MediaStream resulted from mixing.
+ */
+ _mixedMediaStream: Object;
+
+ /**
+ * MediaStreamTrack obtained from mixed stream.
+ */
+ _mixedMediaTrack: Object;
+
/**
* Original MediaStream from the JitsiLocalTrack that uses this effect.
*/
@@ -68,7 +78,14 @@ export class AudioMixerEffect {
this._audioMixer.addMediaStream(this._mixAudio.getOriginalStream());
this._audioMixer.addMediaStream(this._originalStream);
- return this._audioMixer.start();
+ this._mixedMediaStream = this._audioMixer.start();
+ this._mixedMediaTrack = this._mixedMediaStream.getTracks()[0];
+
+ // Sync the resulting mixed track enabled state with that of the track using the effect.
+ this.setMuted(!this._originalTrack.enabled);
+ this._originalTrack.enabled = true;
+
+ return this._mixedMediaStream;
}
/**
@@ -77,6 +94,9 @@ export class AudioMixerEffect {
* @returns {void}
*/
stopEffect() {
+ // Match state of the original track with that of the mixer track, not doing so can
+ // result in an inconsistent state e.g. redux state is muted yet track is enabled.
+ this._originalTrack.enabled = this._mixedMediaTrack.enabled;
this._audioMixer.reset();
}
@@ -87,7 +107,7 @@ export class AudioMixerEffect {
* @returns {void}
*/
setMuted(muted: boolean) {
- this._originalTrack.enabled = !muted;
+ this._mixedMediaTrack.enabled = !muted;
}
/**
@@ -96,6 +116,6 @@ export class AudioMixerEffect {
* @returns {boolean}
*/
isMuted() {
- return !this._originalTrack.enabled;
+ return !this._mixedMediaTrack.enabled;
}
}
diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js
index dcff0b46c5c0..53a1e46a92cb 100644
--- a/react/features/toolbox/components/web/Toolbox.js
+++ b/react/features/toolbox/components/web/Toolbox.js
@@ -23,6 +23,7 @@ import {
IconPresentation,
IconRaisedHand,
IconRec,
+ IconShareAudio,
IconShareDesktop
} from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
@@ -47,6 +48,7 @@ import {
LiveStreamButton,
RecordButton
} from '../../../recording';
+import { isScreenAudioShared, isScreenAudioSupported } from '../../../screen-share/';
import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton';
import {
SETTINGS_TABS,
@@ -252,6 +254,7 @@ class Toolbox extends Component {
this._onToolbarToggleProfile = this._onToolbarToggleProfile.bind(this);
this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
+ this._onToolbarToggleShareAudio = this._onToolbarToggleShareAudio.bind(this);
this._onToolbarOpenLocalRecordingInfoDialog = this._onToolbarOpenLocalRecordingInfoDialog.bind(this);
this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
}
@@ -487,11 +490,12 @@ class Toolbox extends Component {
* Dispatches an action to toggle screensharing.
*
* @private
+ * @param {boolean} audioOnly - Only share system audio.
* @returns {void}
*/
- _doToggleScreenshare() {
+ _doToggleScreenshare(audioOnly = false) {
if (this.props._desktopSharingEnabled) {
- this.props.dispatch(toggleScreensharing());
+ this.props.dispatch(toggleScreensharing(audioOnly));
}
}
@@ -857,6 +861,18 @@ class Toolbox extends Component {
this._doToggleScreenshare();
}
+ _onToolbarToggleShareAudio: () => void;
+
+ /**
+ * Handles toggle share audio action.
+ *
+ * @returns {void}
+ */
+ _onToolbarToggleShareAudio() {
+ this._closeOverflowMenuIfOpen();
+ this._doToggleScreenshare(true);
+ }
+
_onToolbarOpenLocalRecordingInfoDialog: () => void;
/**
@@ -983,6 +999,13 @@ class Toolbox extends Component {
&& ,
+ this._shouldShowButton('shareaudio') && isScreenAudioSupported()
+ && ,
this._shouldShowButton('etherpad')
&&