2017-10-04 22:36:09 +00:00
|
|
|
// @flow
|
|
|
|
|
2018-07-02 21:22:51 +00:00
|
|
|
import { reloadNow } from '../../app';
|
2017-12-11 18:48:32 +00:00
|
|
|
import {
|
2018-01-03 21:24:07 +00:00
|
|
|
ACTION_PINNED,
|
|
|
|
ACTION_UNPINNED,
|
2018-02-02 14:50:16 +00:00
|
|
|
createAudioOnlyChangedEvent,
|
2018-07-02 21:22:51 +00:00
|
|
|
createConnectionEvent,
|
2019-06-17 10:35:47 +00:00
|
|
|
createOfferAnswerFailedEvent,
|
2018-01-03 21:24:07 +00:00
|
|
|
createPinnedEvent,
|
|
|
|
sendAnalytics
|
2017-12-11 18:48:32 +00:00
|
|
|
} from '../../analytics';
|
2018-06-07 13:05:00 +00:00
|
|
|
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection';
|
2019-06-17 10:35:47 +00:00
|
|
|
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
2017-08-02 15:00:51 +00:00
|
|
|
import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
|
2016-10-05 14:36:59 +00:00
|
|
|
import {
|
|
|
|
getLocalParticipant,
|
|
|
|
getParticipantById,
|
2017-06-27 22:56:55 +00:00
|
|
|
getPinnedParticipant,
|
2019-01-13 19:33:28 +00:00
|
|
|
PARTICIPANT_UPDATED,
|
2016-10-05 14:36:59 +00:00
|
|
|
PIN_PARTICIPANT
|
|
|
|
} from '../participants';
|
2018-07-24 20:43:09 +00:00
|
|
|
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
|
2018-02-13 17:58:26 +00:00
|
|
|
import UIEvents from '../../../../service/UI/UIEvents';
|
2018-03-01 19:46:11 +00:00
|
|
|
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
|
2016-10-05 14:36:59 +00:00
|
|
|
|
2017-03-29 12:07:05 +00:00
|
|
|
import {
|
2018-06-07 13:05:00 +00:00
|
|
|
conferenceFailed,
|
2018-08-01 20:37:15 +00:00
|
|
|
conferenceWillLeave,
|
2017-03-29 12:07:05 +00:00
|
|
|
createConference,
|
2019-03-12 17:45:53 +00:00
|
|
|
setLastN,
|
|
|
|
setSubject
|
2017-03-29 12:07:05 +00:00
|
|
|
} from './actions';
|
2017-08-02 15:00:51 +00:00
|
|
|
import {
|
2018-05-15 20:24:58 +00:00
|
|
|
CONFERENCE_FAILED,
|
2017-08-02 15:00:51 +00:00
|
|
|
CONFERENCE_JOINED,
|
2019-03-12 17:45:53 +00:00
|
|
|
CONFERENCE_SUBJECT_CHANGED,
|
2018-08-01 20:37:15 +00:00
|
|
|
CONFERENCE_WILL_LEAVE,
|
2017-08-09 19:40:03 +00:00
|
|
|
DATA_CHANNEL_OPENED,
|
2017-08-02 15:00:51 +00:00
|
|
|
SET_AUDIO_ONLY,
|
2019-05-29 10:44:35 +00:00
|
|
|
SET_LASTN,
|
2019-05-29 10:46:19 +00:00
|
|
|
SET_PENDING_SUBJECT_CHANGE,
|
|
|
|
SET_ROOM
|
2017-08-02 15:00:51 +00:00
|
|
|
} from './actionTypes';
|
2016-10-05 14:36:59 +00:00
|
|
|
import {
|
|
|
|
_addLocalTracksToConference,
|
fix(base/participants): ensure default local id outside of conference
Makes sure that whenever a conference is left or switched, the local
participant's id will be equal to the default value.
The problem fixed by this commit is a situation where the local
participant may end up sharing the same ID with it's "ghost" when
rejoining a disconnected conference. The most important and easiest to
hit case is when the conference is left after the CONFERENCE_FAILED
event.
Another rare and harder to encounter in the real world issue is
where CONFERENCE_LEFT may come with the delay due to it's asynchronous
nature. The step by step scenario is as follows: trying to leave a
conference, but the network is not doing well, so it takes time,
requests are timing out. After getting back to the welcome page the
the CONFERENCE_LEFT has not arrived yet. The same conference is joined
again and the load config may timeout, but it will be read from the
cache. Now the network gets better and conference is joining which
results in our ghost participant added to the redux state. At this point
there's the root issue: two participants with the same id, because the
local one was neither cleared nor set to the new one yet
(PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the
id). Then comes CONFERENCE_JOINED and we try to update our local id.
We're updating the ID of both ghost and local participant. It could be
also that the delayed CONFERENCE_LEFT comes for the old conference, but
it's too late and it would update the id for both participants.
The approach here reasons that the ID of the local participant
may be reset as soon as the local participant and, respectively, her ID
is no longer involved in a recoverable JitsiConference of interest to
the user and, consequently, the app.
Co-authored-by: Pawel Domas <pawel.domas@jitsi.org>
Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
|
|
|
forEachConference,
|
2019-03-25 23:15:50 +00:00
|
|
|
getCurrentConference,
|
2016-10-05 14:36:59 +00:00
|
|
|
_handleParticipantError,
|
|
|
|
_removeLocalTracksFromConference
|
|
|
|
} from './functions';
|
|
|
|
|
2017-10-09 21:40:38 +00:00
|
|
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
|
|
|
|
2017-10-04 22:36:09 +00:00
|
|
|
declare var APP: Object;
|
|
|
|
|
2018-08-01 20:37:15 +00:00
|
|
|
/**
|
|
|
|
* Handler for before unload event.
|
|
|
|
*/
|
|
|
|
let beforeUnloadHandler;
|
|
|
|
|
2016-10-05 14:36:59 +00:00
|
|
|
/**
|
2016-12-05 15:14:50 +00:00
|
|
|
* Implements the middleware of the feature base/conference.
|
2016-10-05 14:36:59 +00:00
|
|
|
*
|
2017-08-04 21:06:42 +00:00
|
|
|
* @param {Store} store - The redux store.
|
2016-10-05 14:36:59 +00:00
|
|
|
* @returns {Function}
|
|
|
|
*/
|
|
|
|
MiddlewareRegistry.register(store => next => action => {
|
|
|
|
switch (action.type) {
|
2018-05-15 20:24:58 +00:00
|
|
|
case CONFERENCE_FAILED:
|
2018-05-21 03:58:34 +00:00
|
|
|
return _conferenceFailed(store, next, action);
|
2018-05-15 20:24:58 +00:00
|
|
|
|
2017-08-04 21:06:42 +00:00
|
|
|
case CONFERENCE_JOINED:
|
|
|
|
return _conferenceJoined(store, next, action);
|
|
|
|
|
2018-05-21 03:58:34 +00:00
|
|
|
case CONNECTION_ESTABLISHED:
|
|
|
|
return _connectionEstablished(store, next, action);
|
|
|
|
|
2018-06-07 13:05:00 +00:00
|
|
|
case CONNECTION_FAILED:
|
|
|
|
return _connectionFailed(store, next, action);
|
|
|
|
|
2019-03-12 17:45:53 +00:00
|
|
|
case CONFERENCE_SUBJECT_CHANGED:
|
|
|
|
return _conferenceSubjectChanged(store, next, action);
|
|
|
|
|
2018-08-01 20:37:15 +00:00
|
|
|
case CONFERENCE_WILL_LEAVE:
|
|
|
|
_conferenceWillLeave();
|
|
|
|
break;
|
|
|
|
|
2017-08-09 19:40:03 +00:00
|
|
|
case DATA_CHANNEL_OPENED:
|
|
|
|
return _syncReceiveVideoQuality(store, next, action);
|
|
|
|
|
2019-01-13 19:33:28 +00:00
|
|
|
case PARTICIPANT_UPDATED:
|
|
|
|
return _updateLocalParticipantInConference(store, next, action);
|
|
|
|
|
2016-10-05 14:36:59 +00:00
|
|
|
case PIN_PARTICIPANT:
|
2016-12-05 15:14:50 +00:00
|
|
|
return _pinParticipant(store, next, action);
|
2016-10-05 14:36:59 +00:00
|
|
|
|
2017-03-29 12:07:05 +00:00
|
|
|
case SET_AUDIO_ONLY:
|
|
|
|
return _setAudioOnly(store, next, action);
|
|
|
|
|
2017-03-29 10:24:18 +00:00
|
|
|
case SET_LASTN:
|
|
|
|
return _setLastN(store, next, action);
|
|
|
|
|
2019-05-29 10:46:19 +00:00
|
|
|
case SET_ROOM:
|
|
|
|
return _setRoom(store, next, action);
|
|
|
|
|
2016-10-05 14:36:59 +00:00
|
|
|
case TRACK_ADDED:
|
2016-12-05 15:14:50 +00:00
|
|
|
case TRACK_REMOVED:
|
|
|
|
return _trackAddedOrRemoved(store, next, action);
|
2016-10-05 14:36:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return next(action);
|
|
|
|
});
|
|
|
|
|
2018-07-24 20:43:09 +00:00
|
|
|
/**
|
|
|
|
* Registers a change handler for state['features/base/conference'] to update
|
|
|
|
* the preferred video quality levels based on user preferred and internal
|
|
|
|
* settings.
|
|
|
|
*/
|
|
|
|
StateListenerRegistry.register(
|
|
|
|
/* selector */ state => state['features/base/conference'],
|
|
|
|
/* listener */ (currentState, store, previousState = {}) => {
|
|
|
|
const {
|
|
|
|
conference,
|
|
|
|
maxReceiverVideoQuality,
|
|
|
|
preferredReceiverVideoQuality
|
|
|
|
} = currentState;
|
|
|
|
const changedPreferredVideoQuality = preferredReceiverVideoQuality
|
|
|
|
!== previousState.preferredReceiverVideoQuality;
|
|
|
|
const changedMaxVideoQuality = maxReceiverVideoQuality
|
|
|
|
!== previousState.maxReceiverVideoQuality;
|
|
|
|
|
|
|
|
if (changedPreferredVideoQuality || changedMaxVideoQuality) {
|
|
|
|
_setReceiverVideoConstraint(
|
|
|
|
conference,
|
|
|
|
preferredReceiverVideoQuality,
|
|
|
|
maxReceiverVideoQuality);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-05-15 20:24:58 +00:00
|
|
|
/**
|
|
|
|
* Makes sure to leave a failed conference in order to release any allocated
|
2018-05-21 03:58:34 +00:00
|
|
|
* resources like peer connections, emit participant left events, etc.
|
2018-05-15 20:24:58 +00:00
|
|
|
*
|
2018-05-21 03:58:34 +00:00
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code CONFERENCE_FAILED} which is
|
|
|
|
* being dispatched in the specified {@code store}.
|
2018-05-15 20:24:58 +00:00
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
2018-05-21 03:58:34 +00:00
|
|
|
function _conferenceFailed(store, next, action) {
|
2018-05-15 20:24:58 +00:00
|
|
|
const result = next(action);
|
2018-05-21 03:58:34 +00:00
|
|
|
|
2019-06-17 10:35:47 +00:00
|
|
|
const { conference, error } = action;
|
|
|
|
|
|
|
|
if (error.name === JitsiConferenceErrors.OFFER_ANSWER_FAILED) {
|
|
|
|
sendAnalytics(createOfferAnswerFailedEvent());
|
|
|
|
}
|
|
|
|
|
2018-06-20 07:44:56 +00:00
|
|
|
// FIXME: Workaround for the web version. Currently, the creation of the
|
|
|
|
// conference is handled by /conference.js and appropriate failure handlers
|
|
|
|
// are set there.
|
|
|
|
if (typeof APP !== 'undefined') {
|
2018-08-01 20:37:15 +00:00
|
|
|
if (typeof beforeUnloadHandler !== 'undefined') {
|
|
|
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
|
|
beforeUnloadHandler = undefined;
|
|
|
|
}
|
|
|
|
|
2018-06-20 07:44:56 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-05-21 03:58:34 +00:00
|
|
|
// XXX After next(action), it is clear whether the error is recoverable.
|
2018-05-15 20:24:58 +00:00
|
|
|
!error.recoverable
|
|
|
|
&& conference
|
2018-05-21 03:58:34 +00:00
|
|
|
&& conference.leave().catch(reason => {
|
|
|
|
// Even though we don't care too much about the failure, it may be
|
|
|
|
// good to know that it happen, so log it (on the info level).
|
|
|
|
logger.info('JitsiConference.leave() rejected with:', reason);
|
2018-05-15 20:24:58 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2017-07-11 13:06:58 +00:00
|
|
|
/**
|
2017-08-04 21:06:42 +00:00
|
|
|
* Does extra sync up on properties that may need to be updated after the
|
|
|
|
* conference was joined.
|
2017-07-11 13:06:58 +00:00
|
|
|
*
|
2018-05-21 03:58:34 +00:00
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
|
|
|
|
* being dispatched in the specified {@code store}.
|
2017-07-11 13:06:58 +00:00
|
|
|
* @private
|
2018-02-13 17:58:26 +00:00
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
2017-07-11 13:06:58 +00:00
|
|
|
*/
|
2018-02-13 17:58:26 +00:00
|
|
|
function _conferenceJoined({ dispatch, getState }, next, action) {
|
2017-07-11 13:06:58 +00:00
|
|
|
const result = next(action);
|
2018-02-13 17:58:26 +00:00
|
|
|
|
2019-03-12 17:45:53 +00:00
|
|
|
const {
|
|
|
|
audioOnly,
|
|
|
|
conference,
|
|
|
|
pendingSubjectChange
|
|
|
|
} = getState()['features/base/conference'];
|
|
|
|
|
|
|
|
if (pendingSubjectChange) {
|
|
|
|
dispatch(setSubject(pendingSubjectChange));
|
|
|
|
}
|
2017-07-11 13:06:58 +00:00
|
|
|
|
|
|
|
// 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.
|
2018-05-21 03:58:34 +00:00
|
|
|
audioOnly && conference.getLastN() !== 0 && dispatch(setLastN(0));
|
|
|
|
|
2018-08-01 20:37:15 +00:00
|
|
|
// FIXME: Very dirty solution. This will work on web only.
|
|
|
|
// When the user closes the window or quits the browser, lib-jitsi-meet
|
|
|
|
// handles the process of leaving the conference. This is temporary solution
|
|
|
|
// that should cover the described use case as part of the effort to
|
|
|
|
// implement the conferenceWillLeave action for web.
|
|
|
|
beforeUnloadHandler = () => {
|
|
|
|
dispatch(conferenceWillLeave(conference));
|
|
|
|
};
|
|
|
|
window.addEventListener('beforeunload', beforeUnloadHandler);
|
|
|
|
|
2018-05-21 03:58:34 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notifies the feature base/conference that the action
|
|
|
|
* {@code CONNECTION_ESTABLISHED} is being dispatched within a specific redux
|
|
|
|
* store.
|
|
|
|
*
|
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code CONNECTION_ESTABLISHED}
|
|
|
|
* which is being dispatched in the specified {@code store}.
|
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
|
|
|
function _connectionEstablished({ dispatch }, next, action) {
|
|
|
|
const result = next(action);
|
|
|
|
|
|
|
|
// FIXME: Workaround for the web version. Currently, the creation of the
|
|
|
|
// conference is handled by /conference.js.
|
|
|
|
typeof APP === 'undefined' && dispatch(createConference());
|
2017-07-11 13:06:58 +00:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-06-07 13:05:00 +00:00
|
|
|
/**
|
|
|
|
* Notifies the feature base/conference that the action
|
|
|
|
* {@code CONNECTION_FAILED} is being dispatched within a specific redux
|
|
|
|
* store.
|
|
|
|
*
|
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
|
|
|
|
* being dispatched in the specified {@code store}.
|
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
|
|
|
function _connectionFailed({ dispatch, getState }, next, action) {
|
2018-07-02 21:22:51 +00:00
|
|
|
// In the case of a split-brain error, reload early and prevent further
|
|
|
|
// handling of the action.
|
|
|
|
if (_isMaybeSplitBrainError(getState, action)) {
|
|
|
|
dispatch(reloadNow());
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-07 13:05:00 +00:00
|
|
|
const result = next(action);
|
|
|
|
|
2018-08-01 20:37:15 +00:00
|
|
|
if (typeof beforeUnloadHandler !== 'undefined') {
|
|
|
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
|
|
beforeUnloadHandler = undefined;
|
|
|
|
}
|
|
|
|
|
2018-06-07 13:05:00 +00:00
|
|
|
// FIXME: Workaround for the web version. Currently, the creation of the
|
|
|
|
// conference is handled by /conference.js and appropriate failure handlers
|
|
|
|
// are set there.
|
|
|
|
if (typeof APP === 'undefined') {
|
|
|
|
const { connection } = action;
|
|
|
|
const { error } = action;
|
|
|
|
|
|
|
|
forEachConference(getState, conference => {
|
|
|
|
// It feels that it would make things easier if JitsiConference
|
|
|
|
// in lib-jitsi-meet would monitor it's connection and emit
|
|
|
|
// CONFERENCE_FAILED when it's dropped. It has more knowledge on
|
|
|
|
// whether it can recover or not. But because the reload screen
|
|
|
|
// and the retry logic is implemented in the app maybe it can be
|
|
|
|
// left this way for now.
|
|
|
|
if (conference.getConnection() === connection) {
|
|
|
|
// XXX Note that on mobile the error type passed to
|
|
|
|
// connectionFailed is always an object with .name property.
|
|
|
|
// This fact needs to be checked prior to enabling this logic on
|
|
|
|
// web.
|
|
|
|
const conferenceAction
|
|
|
|
= conferenceFailed(conference, error.name);
|
|
|
|
|
|
|
|
// Copy the recoverable flag if set on the CONNECTION_FAILED
|
|
|
|
// action to not emit recoverable action caused by
|
|
|
|
// a non-recoverable one.
|
|
|
|
if (typeof error.recoverable !== 'undefined') {
|
|
|
|
conferenceAction.error.recoverable = error.recoverable;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch(conferenceAction);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-03-12 17:45:53 +00:00
|
|
|
/**
|
|
|
|
* Notifies the feature base/conference that the action
|
|
|
|
* {@code CONFERENCE_SUBJECT_CHANGED} is being dispatched within a specific
|
|
|
|
* redux store.
|
|
|
|
*
|
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code CONFERENCE_SUBJECT_CHANGED}
|
|
|
|
* which is being dispatched in the specified {@code store}.
|
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
2019-05-29 10:44:35 +00:00
|
|
|
function _conferenceSubjectChanged({ dispatch, getState }, next, action) {
|
2019-03-12 17:45:53 +00:00
|
|
|
const result = next(action);
|
|
|
|
const { subject } = getState()['features/base/conference'];
|
|
|
|
|
2019-05-29 10:44:35 +00:00
|
|
|
if (subject) {
|
|
|
|
dispatch({
|
|
|
|
type: SET_PENDING_SUBJECT_CHANGE,
|
|
|
|
subject: undefined
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-03-12 17:45:53 +00:00
|
|
|
typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-08-01 20:37:15 +00:00
|
|
|
/**
|
|
|
|
* Notifies the feature base/conference that the action
|
|
|
|
* {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
|
|
|
|
* store.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function _conferenceWillLeave() {
|
|
|
|
if (typeof beforeUnloadHandler !== 'undefined') {
|
|
|
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
|
|
beforeUnloadHandler = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-02 21:22:51 +00:00
|
|
|
/**
|
|
|
|
* Returns whether or not a CONNECTION_FAILED action is for a possible split
|
|
|
|
* brain error. A split brain error occurs when at least two users join a
|
|
|
|
* conference on different bridges. It is assumed the split brain scenario
|
|
|
|
* occurs very early on in the call.
|
|
|
|
*
|
|
|
|
* @param {Function} getState - The redux function for fetching the current
|
|
|
|
* state.
|
|
|
|
* @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
|
|
|
|
* being dispatched in the specified {@code store}.
|
|
|
|
* @private
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
function _isMaybeSplitBrainError(getState, action) {
|
|
|
|
const { error } = action;
|
|
|
|
const isShardChangedError = error
|
|
|
|
&& error.message === 'item-not-found'
|
|
|
|
&& error.details
|
|
|
|
&& error.details.shard_changed;
|
|
|
|
|
|
|
|
if (isShardChangedError) {
|
|
|
|
const state = getState();
|
|
|
|
const { timeEstablished } = state['features/base/connection'];
|
|
|
|
const { _immediateReloadThreshold } = state['features/base/config'];
|
|
|
|
|
|
|
|
const timeSinceConnectionEstablished
|
|
|
|
= timeEstablished && Date.now() - timeEstablished;
|
|
|
|
const reloadThreshold = typeof _immediateReloadThreshold === 'number'
|
|
|
|
? _immediateReloadThreshold : 1500;
|
|
|
|
|
|
|
|
const isWithinSplitBrainThreshold = !timeEstablished
|
|
|
|
|| timeSinceConnectionEstablished <= reloadThreshold;
|
|
|
|
|
|
|
|
sendAnalytics(createConnectionEvent('failed', {
|
|
|
|
...error,
|
|
|
|
connectionEstablished: timeEstablished,
|
|
|
|
splitBrain: isWithinSplitBrainThreshold,
|
|
|
|
timeSinceConnectionEstablished
|
|
|
|
}));
|
|
|
|
|
|
|
|
return isWithinSplitBrainThreshold;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-12-05 15:14:50 +00:00
|
|
|
/**
|
2018-05-21 03:58:34 +00:00
|
|
|
* Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
|
|
|
|
* is being dispatched within a specific redux store. Pins the specified remote
|
2016-12-05 15:14:50 +00:00
|
|
|
* participant in the associated conference, ignores the local participant.
|
|
|
|
*
|
2018-05-21 03:58:34 +00:00
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code PIN_PARTICIPANT} which is
|
|
|
|
* being dispatched in the specified {@code store}.
|
2016-12-05 15:14:50 +00:00
|
|
|
* @private
|
2018-02-13 17:58:26 +00:00
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
2016-10-05 14:36:59 +00:00
|
|
|
*/
|
2018-02-13 17:58:26 +00:00
|
|
|
function _pinParticipant({ getState }, next, action) {
|
|
|
|
const state = getState();
|
2017-06-27 22:56:55 +00:00
|
|
|
const { conference } = state['features/base/conference'];
|
2018-01-31 16:37:41 +00:00
|
|
|
|
|
|
|
if (!conference) {
|
|
|
|
return next(action);
|
|
|
|
}
|
|
|
|
|
2016-10-05 14:36:59 +00:00
|
|
|
const participants = state['features/base/participants'];
|
2016-12-05 15:14:50 +00:00
|
|
|
const id = action.participant.id;
|
2016-10-05 14:36:59 +00:00
|
|
|
const participantById = getParticipantById(participants, id);
|
|
|
|
|
2017-08-10 20:51:35 +00:00
|
|
|
if (typeof APP !== 'undefined') {
|
2017-06-27 22:56:55 +00:00
|
|
|
const pinnedParticipant = getPinnedParticipant(participants);
|
2018-01-31 16:37:41 +00:00
|
|
|
const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
|
|
|
|
const local
|
|
|
|
= (participantById && participantById.local)
|
2018-01-03 21:24:07 +00:00
|
|
|
|| (!id && pinnedParticipant && pinnedParticipant.local);
|
2019-01-11 18:08:05 +00:00
|
|
|
let participantIdForEvent;
|
|
|
|
|
|
|
|
if (local) {
|
|
|
|
participantIdForEvent = local;
|
|
|
|
} else {
|
|
|
|
participantIdForEvent = actionName === ACTION_PINNED
|
|
|
|
? id : pinnedParticipant && pinnedParticipant.id;
|
|
|
|
}
|
2018-01-03 21:24:07 +00:00
|
|
|
|
|
|
|
sendAnalytics(createPinnedEvent(
|
|
|
|
actionName,
|
2019-01-11 18:08:05 +00:00
|
|
|
participantIdForEvent,
|
2018-01-03 21:24:07 +00:00
|
|
|
{
|
2018-01-31 16:37:41 +00:00
|
|
|
local,
|
|
|
|
'participant_count': conference.getParticipantCount()
|
2018-01-03 21:24:07 +00:00
|
|
|
}));
|
2017-06-27 22:56:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The following condition prevents signaling to pin local participant and
|
|
|
|
// shared videos. The logic is:
|
2016-10-05 14:36:59 +00:00
|
|
|
// - If we have an ID, we check if the participant identified by that ID is
|
2017-06-27 22:56:55 +00:00
|
|
|
// local or a bot/fake participant (such as with shared video).
|
2016-10-05 14:36:59 +00:00
|
|
|
// - If we don't have an ID (i.e. no participant identified by an ID), we
|
|
|
|
// check for local participant. If she's currently pinned, then this
|
|
|
|
// action will unpin her and that's why we won't signal here too.
|
2018-01-31 16:37:41 +00:00
|
|
|
let pin;
|
|
|
|
|
2016-10-05 14:36:59 +00:00
|
|
|
if (participantById) {
|
2018-06-22 18:59:54 +00:00
|
|
|
pin = !participantById.local && !participantById.isFakeParticipant;
|
2016-10-05 14:36:59 +00:00
|
|
|
} else {
|
|
|
|
const localParticipant = getLocalParticipant(participants);
|
|
|
|
|
|
|
|
pin = !localParticipant || !localParticipant.pinned;
|
|
|
|
}
|
|
|
|
if (pin) {
|
|
|
|
try {
|
|
|
|
conference.pinParticipant(id);
|
|
|
|
} catch (err) {
|
|
|
|
_handleParticipantError(err);
|
|
|
|
}
|
|
|
|
}
|
2016-12-05 15:14:50 +00:00
|
|
|
|
|
|
|
return next(action);
|
|
|
|
}
|
|
|
|
|
2017-03-29 12:07:05 +00:00
|
|
|
/**
|
|
|
|
* Sets the audio-only flag for the current conference. When audio-only is set,
|
|
|
|
* local video is muted and last N is set to 0 to avoid receiving remote video.
|
|
|
|
*
|
2018-05-21 03:58:34 +00:00
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code SET_AUDIO_ONLY} which is
|
|
|
|
* being dispatched in the specified {@code store}.
|
2017-03-29 12:07:05 +00:00
|
|
|
* @private
|
2018-02-13 17:58:26 +00:00
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
2017-03-29 12:07:05 +00:00
|
|
|
*/
|
2017-08-04 21:06:42 +00:00
|
|
|
function _setAudioOnly({ dispatch, getState }, next, action) {
|
2018-04-17 03:02:37 +00:00
|
|
|
const { audioOnly: oldValue } = getState()['features/base/conference'];
|
2017-03-29 12:07:05 +00:00
|
|
|
const result = next(action);
|
2018-04-17 03:02:37 +00:00
|
|
|
const { audioOnly: newValue } = getState()['features/base/conference'];
|
|
|
|
|
|
|
|
// Send analytics. We could've done it in the action creator setAudioOnly.
|
|
|
|
// I don't know why it has to happen as early as possible but the analytics
|
|
|
|
// were originally sent before the SET_AUDIO_ONLY action was even dispatched
|
|
|
|
// in the redux store so I'm now sending the analytics as early as possible.
|
|
|
|
if (oldValue !== newValue) {
|
|
|
|
sendAnalytics(createAudioOnlyChangedEvent(newValue));
|
|
|
|
logger.log(`Audio-only ${newValue ? 'enabled' : 'disabled'}`);
|
|
|
|
}
|
2017-03-29 12:07:05 +00:00
|
|
|
|
|
|
|
// Set lastN to 0 in case audio-only is desired; leave it as undefined,
|
|
|
|
// otherwise, and the default lastN value will be chosen automatically.
|
2018-04-17 03:02:37 +00:00
|
|
|
dispatch(setLastN(newValue ? 0 : undefined));
|
2017-03-29 12:07:05 +00:00
|
|
|
|
2017-08-04 21:06:42 +00:00
|
|
|
// Mute/unmute the local video.
|
2017-08-14 13:27:24 +00:00
|
|
|
dispatch(
|
|
|
|
setVideoMuted(
|
2018-04-17 03:02:37 +00:00
|
|
|
newValue,
|
2017-08-14 13:27:24 +00:00
|
|
|
VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY,
|
2018-06-18 15:19:59 +00:00
|
|
|
action.ensureVideoTrack));
|
2017-03-29 12:07:05 +00:00
|
|
|
|
2017-04-05 15:14:26 +00:00
|
|
|
if (typeof APP !== 'undefined') {
|
2018-05-21 03:58:34 +00:00
|
|
|
// TODO This should be a temporary solution that lasts only until video
|
|
|
|
// tracks and all ui is moved into react/redux on the web.
|
2018-04-17 03:02:37 +00:00
|
|
|
APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, newValue);
|
2017-04-05 15:14:26 +00:00
|
|
|
}
|
|
|
|
|
2017-03-29 12:07:05 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2017-03-29 10:24:18 +00:00
|
|
|
/**
|
|
|
|
* Sets the last N (value) of the video channel in the conference.
|
|
|
|
*
|
2018-05-21 03:58:34 +00:00
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code SET_LASTN} which is being
|
|
|
|
* dispatched in the specified {@code store}.
|
2017-03-29 10:24:18 +00:00
|
|
|
* @private
|
2018-02-13 17:58:26 +00:00
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
2017-03-29 10:24:18 +00:00
|
|
|
*/
|
2018-02-13 17:58:26 +00:00
|
|
|
function _setLastN({ getState }, next, action) {
|
|
|
|
const { conference } = getState()['features/base/conference'];
|
2017-03-29 10:24:18 +00:00
|
|
|
|
|
|
|
if (conference) {
|
|
|
|
try {
|
|
|
|
conference.setLastN(action.lastN);
|
|
|
|
} catch (err) {
|
2018-07-30 14:38:25 +00:00
|
|
|
logger.error(`Failed to set lastN: ${err}`);
|
2017-03-29 10:24:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return next(action);
|
|
|
|
}
|
|
|
|
|
2017-08-09 19:40:03 +00:00
|
|
|
/**
|
2018-07-24 20:43:09 +00:00
|
|
|
* Helper function for updating the preferred receiver video constraint, based
|
|
|
|
* on the user preference and the internal maximum.
|
2018-07-24 16:23:38 +00:00
|
|
|
*
|
2018-07-24 20:43:09 +00:00
|
|
|
* @param {JitsiConference} conference - The JitsiConference instance for the
|
|
|
|
* current call.
|
|
|
|
* @param {number} preferred - The user preferred max frame height.
|
|
|
|
* @param {number} max - The maximum frame height the application should
|
|
|
|
* receive.
|
|
|
|
* @returns {void}
|
2018-07-24 16:23:38 +00:00
|
|
|
*/
|
2018-07-24 20:43:09 +00:00
|
|
|
function _setReceiverVideoConstraint(conference, preferred, max) {
|
2018-07-24 16:23:38 +00:00
|
|
|
if (conference) {
|
2018-07-24 20:43:09 +00:00
|
|
|
conference.setReceiverVideoConstraint(Math.min(preferred, max));
|
2018-07-24 16:23:38 +00:00
|
|
|
}
|
2017-08-09 19:40:03 +00:00
|
|
|
}
|
|
|
|
|
2019-05-29 10:46:19 +00:00
|
|
|
/**
|
|
|
|
* Notifies the feature base/conference that the action
|
|
|
|
* {@code SET_ROOM} is being dispatched within a specific
|
|
|
|
* redux store.
|
|
|
|
*
|
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code SET_ROOM}
|
|
|
|
* which is being dispatched in the specified {@code store}.
|
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
|
|
|
function _setRoom({ dispatch, getState }, next, action) {
|
|
|
|
const state = getState();
|
|
|
|
const { subject } = state['features/base/config'];
|
|
|
|
const { room } = action;
|
|
|
|
|
|
|
|
if (room) {
|
|
|
|
// Set the stored subject.
|
|
|
|
dispatch(setSubject(subject));
|
|
|
|
}
|
|
|
|
|
|
|
|
return next(action);
|
|
|
|
}
|
|
|
|
|
2016-12-12 00:29:13 +00:00
|
|
|
/**
|
|
|
|
* Synchronizes local tracks from state with local tracks in JitsiConference
|
|
|
|
* instance.
|
|
|
|
*
|
2017-08-04 21:06:42 +00:00
|
|
|
* @param {Store} store - The redux store.
|
2016-12-12 00:29:13 +00:00
|
|
|
* @param {Object} action - Action object.
|
|
|
|
* @private
|
|
|
|
* @returns {Promise}
|
|
|
|
*/
|
2017-10-04 22:36:09 +00:00
|
|
|
function _syncConferenceLocalTracksWithState({ getState }, action) {
|
2019-03-25 23:15:50 +00:00
|
|
|
const conference = getCurrentConference(getState);
|
2016-12-12 00:29:13 +00:00
|
|
|
let promise;
|
|
|
|
|
2019-03-25 23:15:50 +00:00
|
|
|
if (conference) {
|
2016-12-12 00:29:13 +00:00
|
|
|
const track = action.track.jitsiTrack;
|
|
|
|
|
|
|
|
if (action.type === TRACK_ADDED) {
|
|
|
|
promise = _addLocalTracksToConference(conference, [ track ]);
|
|
|
|
} else {
|
|
|
|
promise = _removeLocalTracksFromConference(conference, [ track ]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return promise || Promise.resolve();
|
|
|
|
}
|
|
|
|
|
2017-08-09 19:40:03 +00:00
|
|
|
/**
|
|
|
|
* Sets the maximum receive video quality.
|
|
|
|
*
|
2018-05-21 03:58:34 +00:00
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code DATA_CHANNEL_STATUS_CHANGED}
|
|
|
|
* which is being dispatched in the specified {@code store}.
|
2017-08-09 19:40:03 +00:00
|
|
|
* @private
|
2018-02-13 17:58:26 +00:00
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
2017-08-09 19:40:03 +00:00
|
|
|
*/
|
2017-10-04 22:36:09 +00:00
|
|
|
function _syncReceiveVideoQuality({ getState }, next, action) {
|
2018-07-24 20:43:09 +00:00
|
|
|
const {
|
|
|
|
conference,
|
|
|
|
maxReceiverVideoQuality,
|
|
|
|
preferredReceiverVideoQuality
|
|
|
|
} = getState()['features/base/conference'];
|
2017-08-09 19:40:03 +00:00
|
|
|
|
2018-07-24 20:43:09 +00:00
|
|
|
_setReceiverVideoConstraint(
|
|
|
|
conference,
|
|
|
|
preferredReceiverVideoQuality,
|
|
|
|
maxReceiverVideoQuality);
|
2017-08-09 19:40:03 +00:00
|
|
|
|
|
|
|
return next(action);
|
|
|
|
}
|
|
|
|
|
2016-12-05 15:14:50 +00:00
|
|
|
/**
|
2018-05-21 03:58:34 +00:00
|
|
|
* Notifies the feature base/conference that the action {@code TRACK_ADDED}
|
|
|
|
* or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
|
2016-12-05 15:14:50 +00:00
|
|
|
*
|
2018-05-21 03:58:34 +00:00
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code TRACK_ADDED} or
|
|
|
|
* {@code TRACK_REMOVED} which is being dispatched in the specified
|
|
|
|
* {@code store}.
|
2016-12-05 15:14:50 +00:00
|
|
|
* @private
|
2018-02-13 17:58:26 +00:00
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
2016-12-05 15:14:50 +00:00
|
|
|
*/
|
|
|
|
function _trackAddedOrRemoved(store, next, action) {
|
|
|
|
const track = action.track;
|
|
|
|
|
|
|
|
if (track && track.local) {
|
|
|
|
return (
|
|
|
|
_syncConferenceLocalTracksWithState(store, action)
|
|
|
|
.then(() => next(action)));
|
|
|
|
}
|
|
|
|
|
|
|
|
return next(action);
|
2016-10-05 14:36:59 +00:00
|
|
|
}
|
2019-01-13 19:33:28 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the conference object when the local participant is updated.
|
|
|
|
*
|
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} to the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action which is being dispatched in the
|
|
|
|
* specified {@code store}.
|
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
|
|
|
function _updateLocalParticipantInConference({ getState }, next, action) {
|
|
|
|
const { conference } = getState()['features/base/conference'];
|
|
|
|
const { participant } = action;
|
|
|
|
const result = next(action);
|
|
|
|
|
2019-01-31 23:46:34 +00:00
|
|
|
if (conference && participant.local && 'name' in participant) {
|
2019-01-13 19:33:28 +00:00
|
|
|
conference.setDisplayName(participant.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|