diff --git a/conference.js b/conference.js
index 25252db96..ef1099acc 100644
--- a/conference.js
+++ b/conference.js
@@ -26,6 +26,7 @@ import {
conferenceFailed,
conferenceJoined,
conferenceLeft,
+ toggleAudioOnly,
EMAIL_COMMAND,
lockStateChanged
} from './react/features/base/conference';
@@ -459,11 +460,14 @@ export default {
* Creates local media tracks and connects to a room. Will show error
* dialogs in case accessing the local microphone and/or camera failed. Will
* show guidance overlay for users on how to give access to camera and/or
- * microphone,
+ * microphone.
* @param {string} roomName
* @param {object} options
- * @param {boolean} options.startScreenSharing - if true should
- * start with screensharing instead of camera video.
+ * @param {boolean} options.startAudioOnly=false - if true then
+ * only audio track will be created and the audio only mode will be turned
+ * on.
+ * @param {boolean} options.startScreenSharing=false - if true
+ * should start with screensharing instead of camera video.
* @returns {Promise.}
*/
createInitialLocalTracksAndConnect(roomName, options = {}) {
@@ -482,8 +486,18 @@ export default {
// First try to retrieve both audio and video.
let tryCreateLocalTracks;
+ // FIXME there is no video muted indication visible on the remote side,
+ // after starting in audio only (there's no video track)
// FIXME the logic about trying to go audio only on error is duplicated
- if (options.startScreenSharing) {
+ if (options.startAudioOnly) {
+ tryCreateLocalTracks
+ = createLocalTracks({ devices: ['audio'] }, true)
+ .catch(err => {
+ audioOnlyError = err;
+
+ return [];
+ });
+ } else if (options.startScreenSharing) {
tryCreateLocalTracks = this._createDesktopTrack()
.then(desktopStream => {
return createLocalTracks({ devices: ['audio'] }, true)
@@ -591,6 +605,7 @@ export default {
analytics.init();
return this.createInitialLocalTracksAndConnect(
options.roomName, {
+ startAudioOnly: config.startAudioOnly,
startScreenSharing: config.startScreenSharing
});
}).then(([tracks, con]) => {
@@ -649,6 +664,15 @@ export default {
this.updateVideoIconEnabled();
}
+ // Enable audio only mode
+ if (config.startAudioOnly) {
+ // It is important to have that toggled after video muted
+ // state is adjusted by the code about lack of video tracks
+ // above. That's because audio only will store muted state
+ // on toggle action.
+ APP.store.dispatch(toggleAudioOnly());
+ }
+
this._initDeviceList();
if (config.iAmRecorder)
@@ -2035,7 +2059,7 @@ export default {
JitsiMeetJS.mediaDevices.enumerateDevices(devices => {
// Ugly way to synchronize real device IDs with local
// storage and settings menu. This is a workaround until
- // getConstraints() method will be implemented
+ // getConstraints() method will be implemented
// in browsers.
if (localAudio) {
APP.settings.setMicDeviceId(
diff --git a/config.js b/config.js
index 38a7942d5..011e2a2b2 100644
--- a/config.js
+++ b/config.js
@@ -72,6 +72,7 @@ var config = { // eslint-disable-line no-unused-vars
// page redirection when call is hangup
disableSimulcast: false,
// requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.
+ startAudioOnly: false, // Will start the conference in the audio only mode (no video is being received nor sent)
startScreenSharing: false, // Will try to start with screensharing instead of camera
// startAudioMuted: 10, // every participant after the Nth will start audio muted
// startVideoMuted: 10, // every participant after the Nth will start video muted
diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js
index bd61aaae4..fbec4c46a 100644
--- a/react/features/base/conference/middleware.js
+++ b/react/features/base/conference/middleware.js
@@ -15,7 +15,7 @@ import {
_setAudioOnlyVideoMuted,
setLastN
} from './actions';
-import { SET_AUDIO_ONLY, SET_LASTN } from './actionTypes';
+import { CONFERENCE_JOINED, SET_AUDIO_ONLY, SET_LASTN } from './actionTypes';
import {
_addLocalTracksToConference,
_handleParticipantError,
@@ -33,6 +33,9 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_ESTABLISHED:
return _connectionEstablished(store, next, action);
+ case CONFERENCE_JOINED:
+ return _conferenceJoined(store, next, action);
+
case PIN_PARTICIPANT:
return _pinParticipant(store, next, action);
@@ -76,6 +79,35 @@ function _connectionEstablished(store, next, action) {
return result;
}
+/**
+ * Does extra sync up on properties that may need to be updated, after
+ * the conference was joined.
+ *
+ * @param {Store} store - The Redux store in which the specified action is being
+ * dispatched.
+ * @param {Dispatch} next - The Redux dispatch function to dispatch the
+ * specified action to the specified store.
+ * @param {Action} action - The Redux action CONFERENCE_JOINED which is being
+ * dispatched in the specified store.
+ * @private
+ * @returns {Object} The new state that is the result of the reduction of the
+ * specified action.
+ */
+function _conferenceJoined(store, next, action) {
+ const result = next(action);
+ const { audioOnly, conference }
+ = store.getState()['features/base/conference'];
+
+ // FIXME On Web the audio only mode for "start audio only" is toggled before
+ // conference is added to the redux store ("on conference joined" action)
+ // and the LastN value needs to be synchronized here.
+ if (audioOnly && conference.getLastN() !== 0) {
+ store.dispatch(setLastN(0));
+ }
+
+ return result;
+}
+
/**
* Notifies the feature base/conference that the action PIN_PARTICIPANT is being
* dispatched within a specific Redux store. Pins the specified remote