Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(screenshare): Audio only screenshare #8922

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions conference.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -1546,6 +1548,8 @@ export default {
this._desktopAudioStream = undefined;
}

APP.store.dispatch(setScreenAudioShareState(false));

if (didHaveVideo) {
promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] }))
.then(([ stream ]) => {
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to stop screenshare here only when audio-only screenshare is in progress ? If we are using the audio mixer effect and have both video and audio streams from the desktop tracks, any issues with audio track will result in the share being turned off. Is that how we want it to work ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo this behaviour is fine. If the user initiates a screen sharing session and goes through the trouble of ticking the Share audio box and there's a problem with that audio track at some point, then the behaviour wouldn't be as expected same as what would happen if something went wrong with the video track.
Of course this is just my point of view, if you feel strongly about it we can think of another way to approach this scenario.

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(
Expand Down Expand Up @@ -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
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 */
Expand Down
2 changes: 1 addition & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
// ],
Expand Down
4 changes: 4 additions & 0 deletions lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions modules/UI/UIErrors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions react/features/app/reducers.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion react/features/base/conference/middleware.web.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion react/features/base/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
9 changes: 9 additions & 0 deletions react/features/base/environment/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions react/features/base/icons/svg/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions react/features/base/icons/svg/share-audio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions react/features/base/tracks/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand Down
2 changes: 1 addition & 1 deletion react/features/base/tracks/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
12 changes: 12 additions & 0 deletions react/features/screen-share/actionTypes.js
Original file line number Diff line number Diff line change
@@ -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';

19 changes: 19 additions & 0 deletions react/features/screen-share/actions.js
Original file line number Diff line number Diff line change
@@ -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
};
}
25 changes: 25 additions & 0 deletions react/features/screen-share/functions.js
Original file line number Diff line number Diff line change
@@ -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());
}
4 changes: 4 additions & 0 deletions react/features/screen-share/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './actions';
export * from './actionTypes';
export * from './functions';

22 changes: 22 additions & 0 deletions react/features/screen-share/reducer.js
Original file line number Diff line number Diff line change
@@ -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;
}
});
26 changes: 23 additions & 3 deletions react/features/stream-effects/audio-mixer/AudioMixerEffect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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();
}

Expand All @@ -87,7 +107,7 @@ export class AudioMixerEffect {
* @returns {void}
*/
setMuted(muted: boolean) {
this._originalTrack.enabled = !muted;
this._mixedMediaTrack.enabled = !muted;
}

/**
Expand All @@ -96,6 +116,6 @@ export class AudioMixerEffect {
* @returns {boolean}
*/
isMuted() {
return !this._originalTrack.enabled;
return !this._mixedMediaTrack.enabled;
}
}
Loading