fix(prejoin): Store prejoin tracks in 'features/base/tracks'

This commit is contained in:
Vlad Piersec 2020-06-19 10:03:26 +03:00 committed by Saúl Ibarra Corretgé
parent ec6ed6e8ec
commit 4f169988a3
17 changed files with 178 additions and 559 deletions

View File

@ -118,10 +118,7 @@ import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay
import { suspendDetected } from './react/features/power-monitor'; import { suspendDetected } from './react/features/power-monitor';
import { import {
initPrejoin, initPrejoin,
isPrejoinPageEnabled, isPrejoinPageEnabled
isPrejoinPageVisible,
replacePrejoinAudioTrack,
replacePrejoinVideoTrack
} from './react/features/prejoin'; } from './react/features/prejoin';
import { createRnnoiseProcessorPromise } from './react/features/rnnoise'; import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture'; import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
@ -1409,18 +1406,6 @@ export default {
useVideoStream(newStream) { useVideoStream(newStream) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
_replaceLocalVideoTrackQueue.enqueue(onFinish => { _replaceLocalVideoTrackQueue.enqueue(onFinish => {
/**
* When the prejoin page is visible there is no conference object
* created. The prejoin tracks are managed separately,
* so this updates the prejoin video track.
*/
if (isPrejoinPageVisible(APP.store.getState())) {
return APP.store.dispatch(replacePrejoinVideoTrack(newStream))
.then(resolve)
.catch(reject)
.then(onFinish);
}
APP.store.dispatch( APP.store.dispatch(
replaceLocalTrack(this.localVideo, newStream, room)) replaceLocalTrack(this.localVideo, newStream, room))
.then(() => { .then(() => {
@ -1474,18 +1459,6 @@ export default {
useAudioStream(newStream) { useAudioStream(newStream) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
_replaceLocalAudioTrackQueue.enqueue(onFinish => { _replaceLocalAudioTrackQueue.enqueue(onFinish => {
/**
* When the prejoin page is visible there is no conference object
* created. The prejoin tracks are managed separately,
* so this updates the prejoin audio stream.
*/
if (isPrejoinPageVisible(APP.store.getState())) {
return APP.store.dispatch(replacePrejoinAudioTrack(newStream))
.then(resolve)
.catch(reject)
.then(onFinish);
}
APP.store.dispatch( APP.store.dispatch(
replaceLocalTrack(this.localAudio, newStream, room)) replaceLocalTrack(this.localAudio, newStream, room))
.then(() => { .then(() => {

View File

@ -35,6 +35,10 @@
cursor: initial; cursor: initial;
color: #fff; color: #fff;
background-color: #a4b8d1; background-color: #a4b8d1;
&:hover {
background-color: #a4b8d1;
}
} }
svg { svg {

View File

@ -4,6 +4,17 @@ import { toState } from '../redux';
import { VIDEO_MUTISM_AUTHORITY } from './constants'; import { VIDEO_MUTISM_AUTHORITY } from './constants';
/**
* Determines whether audio is currently muted.
*
* @param {Function|Object} stateful - The redux store, state, or
* {@code getState} function.
* @returns {boolean}
*/
export function isAudioMuted(stateful: Function | Object) {
return Boolean(toState(stateful)['features/base/media'].audio.muted);
}
/** /**
* Determines whether video is currently muted by the audio-only authority. * Determines whether video is currently muted by the audio-only authority.
* *

View File

@ -9,7 +9,7 @@ export * from './functions.any';
* @returns {void} * @returns {void}
*/ */
export function getCurrentCameraDeviceId(state: Object) { export function getCurrentCameraDeviceId(state: Object) {
return state['features/base/settings'].cameraDeviceId; return getDeviceIdByType(state, 'isVideoTrack');
} }
/** /**
@ -19,7 +19,7 @@ export function getCurrentCameraDeviceId(state: Object) {
* @returns {void} * @returns {void}
*/ */
export function getCurrentMicDeviceId(state: Object) { export function getCurrentMicDeviceId(state: Object) {
return state['features/base/settings'].micDeviceId; return getDeviceIdByType(state, 'isAudioTrack');
} }
/** /**
@ -32,6 +32,22 @@ export function getCurrentOutputDeviceId(state: Object) {
return state['features/base/settings'].audioOutputDeviceId; return state['features/base/settings'].audioOutputDeviceId;
} }
/**
* Returns the deviceId for the corresponding local track type.
*
* @param {Object} state - The state of the application.
* @param {string} isType - Can be 'isVideoTrack' | 'isAudioTrack'.
* @returns {string}
*/
function getDeviceIdByType(state: Object, isType: string) {
const [ deviceId ] = state['features/base/tracks']
.map(t => t.jitsiTrack)
.filter(t => t && t.isLocal() && t[isType]())
.map(t => t.getDeviceId());
return deviceId || '';
}
/** /**
* Returns the saved display name. * Returns the saved display name.
* *

View File

@ -267,14 +267,29 @@ export function toggleScreensharing() {
* @returns {Function} * @returns {Function}
*/ */
export function replaceLocalTrack(oldTrack, newTrack, conference) { export function replaceLocalTrack(oldTrack, newTrack, conference) {
return (dispatch, getState) => { return async (dispatch, getState) => {
conference conference
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
|| (conference = getState()['features/base/conference'].conference); || (conference = getState()['features/base/conference'].conference);
return conference.replaceTrack(oldTrack, newTrack) if (conference) {
.then(() => { await conference.replaceTrack(oldTrack, newTrack);
}
return dispatch(replaceStoredTracks(oldTrack, newTrack));
};
}
/**
* Replaces a stored track with another.
*
* @param {JitsiLocalTrack|null} oldTrack - The track to dispose.
* @param {JitsiLocalTrack|null} newTrack - The track to use instead.
* @returns {Function}
*/
function replaceStoredTracks(oldTrack, newTrack) {
return dispatch => {
// We call dispose after doing the replace because dispose will // We call dispose after doing the replace because dispose will
// try and do a new o/a after the track removes itself. Doing it // try and do a new o/a after the track removes itself. Doing it
// after means the JitsiLocalTrack.conference is already // after means the JitsiLocalTrack.conference is already
@ -317,7 +332,6 @@ export function replaceLocalTrack(oldTrack, newTrack, conference) {
return dispatch(_addTracks([ newTrack ])); return dispatch(_addTracks([ newTrack ]));
} }
}); });
});
}; };
} }

View File

@ -200,6 +200,18 @@ export function getLocalVideoType(tracks) {
return presenterTrack ? MEDIA_TYPE.PRESENTER : MEDIA_TYPE.VIDEO; return presenterTrack ? MEDIA_TYPE.PRESENTER : MEDIA_TYPE.VIDEO;
} }
/**
* Returns the stored local video track.
*
* @param {Object} state - The redux state.
* @returns {Object}
*/
export function getLocalJitsiVideoTrack(state) {
const track = getLocalVideoTrack(state['features/base/tracks']);
return track?.jitsiTrack;
}
/** /**
* Returns track of specified media type for specified participant id. * Returns track of specified media type for specified participant id.
* *

View File

@ -2,6 +2,8 @@
import UIEvents from '../../../../service/UI/UIEvents'; import UIEvents from '../../../../service/UI/UIEvents';
import { hideNotification } from '../../notifications'; import { hideNotification } from '../../notifications';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getAvailableDevices } from '../devices/actions';
import { import {
CAMERA_FACING_MODE, CAMERA_FACING_MODE,
MEDIA_TYPE, MEDIA_TYPE,
@ -15,6 +17,7 @@ import {
import { MiddlewareRegistry } from '../redux'; import { MiddlewareRegistry } from '../redux';
import { import {
TRACK_ADDED,
TOGGLE_SCREENSHARING, TOGGLE_SCREENSHARING,
TRACK_NO_DATA_FROM_SOURCE, TRACK_NO_DATA_FROM_SOURCE,
TRACK_REMOVED, TRACK_REMOVED,
@ -44,6 +47,15 @@ declare var APP: Object;
*/ */
MiddlewareRegistry.register(store => next => action => { MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case TRACK_ADDED: {
// The devices list needs to be refreshed when no initial video permissions
// were granted and a local video track is added by umuting the video.
if (action.track.local) {
store.dispatch(getAvailableDevices());
}
break;
}
case TRACK_NO_DATA_FROM_SOURCE: { case TRACK_NO_DATA_FROM_SOURCE: {
const result = next(action); const result = next(action);
@ -281,7 +293,7 @@ function _setMuted(store, { ensureTrack, authority, muted }, mediaType: MEDIA_TY
// anymore, unless it is muted by audioOnly. // anymore, unless it is muted by audioOnly.
jitsiTrack && (jitsiTrack.videoType !== 'desktop' || isAudioOnly) jitsiTrack && (jitsiTrack.videoType !== 'desktop' || isAudioOnly)
&& setTrackMuted(jitsiTrack, muted); && setTrackMuted(jitsiTrack, muted);
} else if (!muted && ensureTrack && typeof APP === 'undefined') { } else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(store.getState()))) {
// FIXME: This only runs on mobile now because web has its own way of // FIXME: This only runs on mobile now because web has its own way of
// creating local tracks. Adjust the check once they are unified. // creating local tracks. Adjust the check once they are unified.
store.dispatch(createLocalTracksA({ devices: [ mediaType ] })); store.dispatch(createLocalTracksA({ devices: [ mediaType ] }));

View File

@ -1,18 +1,3 @@
/**
* Action type to add a video track to the store.
*/
export const ADD_PREJOIN_VIDEO_TRACK = 'ADD_PREJOIN_VIDEO_TRACK';
/**
* Action type to add an audio track to the store.
*/
export const ADD_PREJOIN_AUDIO_TRACK = 'ADD_PREJOIN_AUDIO_TRACK';
/**
* Action type to add a content sharing track to the store.
*/
export const ADD_PREJOIN_CONTENT_SHARING_TRACK
= 'ADD_PREJOIN_CONTENT_SHARING_TRACK';
/** /**
* Action type to signal the start of the conference. * Action type to signal the start of the conference.
@ -68,13 +53,3 @@ export const SET_PREJOIN_DEVICE_ERRORS = 'SET_PREJOIN_DEVICE_ERRORS';
* Action type to set the visibility of the prejoin page. * Action type to set the visibility of the prejoin page.
*/ */
export const SET_PREJOIN_PAGE_VISIBILITY = 'SET_PREJOIN_PAGE_VISIBILITY'; export const SET_PREJOIN_PAGE_VISIBILITY = 'SET_PREJOIN_PAGE_VISIBILITY';
/**
* Action type to mute/unmute the video while on prejoin page.
*/
export const SET_PREJOIN_VIDEO_DISABLED = 'SET_PREJOIN_VIDEO_DISABLED';
/**
* Action type to mute/unmute the video while on prejoin page.
*/
export const SET_PREJOIN_VIDEO_MUTED = 'SET_PREJOIN_VIDEO_MUTED';

View File

@ -5,14 +5,17 @@ import uuid from 'uuid';
import { getRoomName } from '../base/conference'; import { getRoomName } from '../base/conference';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions'; import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { createLocalTrack } from '../base/lib-jitsi-meet'; import { createLocalTrack } from '../base/lib-jitsi-meet';
import {
getLocalAudioTrack,
getLocalVideoTrack,
trackAdded,
replaceLocalTrack
} from '../base/tracks';
import { openURLInBrowser } from '../base/util'; import { openURLInBrowser } from '../base/util';
import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL } from '../invite/functions'; import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL } from '../invite/functions';
import { showErrorNotification } from '../notifications'; import { showErrorNotification } from '../notifications';
import { import {
ADD_PREJOIN_AUDIO_TRACK,
ADD_PREJOIN_CONTENT_SHARING_TRACK,
ADD_PREJOIN_VIDEO_TRACK,
PREJOIN_START_CONFERENCE, PREJOIN_START_CONFERENCE,
SET_DEVICE_STATUS, SET_DEVICE_STATUS,
SET_DIALOUT_COUNTRY, SET_DIALOUT_COUNTRY,
@ -20,19 +23,13 @@ import {
SET_DIALOUT_STATUS, SET_DIALOUT_STATUS,
SET_SKIP_PREJOIN, SET_SKIP_PREJOIN,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY, SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_PREJOIN_AUDIO_DISABLED,
SET_PREJOIN_AUDIO_MUTED,
SET_PREJOIN_DEVICE_ERRORS, SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_PAGE_VISIBILITY, SET_PREJOIN_PAGE_VISIBILITY
SET_PREJOIN_VIDEO_DISABLED,
SET_PREJOIN_VIDEO_MUTED
} from './actionTypes'; } from './actionTypes';
import { import {
getFullDialOutNumber, getFullDialOutNumber,
getAudioTrack,
getDialOutConferenceUrl, getDialOutConferenceUrl,
getDialOutCountry, getDialOutCountry,
getVideoTrack,
isJoinByPhoneDialogVisible isJoinByPhoneDialogVisible
} from './functions'; } from './functions';
import logger from './logger'; import logger from './logger';
@ -60,45 +57,6 @@ const STATUS_REQ_FREQUENCY = 2000;
*/ */
const STATUS_REQ_CAP = 45; const STATUS_REQ_CAP = 45;
/**
* Action used to add an audio track to the store.
*
* @param {Object} value - The track to be added.
* @returns {Object}
*/
export function addPrejoinAudioTrack(value: Object) {
return {
type: ADD_PREJOIN_AUDIO_TRACK,
value
};
}
/**
* Action used to add a video track to the store.
*
* @param {Object} value - The track to be added.
* @returns {Object}
*/
export function addPrejoinVideoTrack(value: Object) {
return {
type: ADD_PREJOIN_VIDEO_TRACK,
value
};
}
/**
* Action used to add a content sharing track to the store.
*
* @param {Object} value - The track to be added.
* @returns {Object}
*/
export function addPrejoinContentSharingTrack(value: Object) {
return {
type: ADD_PREJOIN_CONTENT_SHARING_TRACK,
value
};
}
/** /**
* Polls for status change after dial out. * Polls for status change after dial out.
* Changes dialog message based on response, closes the dialog if there is an error, * Changes dialog message based on response, closes the dialog if there is an error,
@ -232,27 +190,10 @@ export function dialOut(onSuccess: Function, onFail: Function) {
*/ */
export function initPrejoin(tracks: Object[], errors: Object) { export function initPrejoin(tracks: Object[], errors: Object) {
return async function(dispatch: Function) { return async function(dispatch: Function) {
const audioTrack = tracks.find(t => t.isAudioTrack());
const videoTrack = tracks.find(t => t.isVideoTrack());
dispatch(setPrejoinDeviceErrors(errors)); dispatch(setPrejoinDeviceErrors(errors));
if (audioTrack) {
dispatch(addPrejoinAudioTrack(audioTrack));
} else {
dispatch(setAudioDisabled());
}
if (videoTrack) { tracks.forEach(track => dispatch(trackAdded(track)));
if (videoTrack.videoType === 'desktop') {
dispatch(addPrejoinContentSharingTrack(videoTrack));
dispatch(setPrejoinVideoDisabled(true));
} else {
dispatch(addPrejoinVideoTrack(videoTrack));
}
} else {
dispatch(setPrejoinVideoDisabled(true));
}
}; };
} }
@ -275,12 +216,12 @@ export function joinConference() {
*/ */
export function joinConferenceWithoutAudio() { export function joinConferenceWithoutAudio() {
return async function(dispatch: Function, getState: Function) { return async function(dispatch: Function, getState: Function) {
const audioTrack = getAudioTrack(getState()); const tracks = getState()['features/base/tracks'];
const audioTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
if (audioTrack) { if (audioTrack) {
await dispatch(replacePrejoinAudioTrack(null)); await dispatch(replaceLocalTrack(audioTrack, null));
} }
dispatch(setAudioDisabled());
dispatch(joinConference()); dispatch(joinConference());
}; };
} }
@ -301,21 +242,6 @@ export function openDialInPage() {
}; };
} }
/**
* Replaces the existing audio track with a new one.
*
* @param {Object} track - The new track.
* @returns {Function}
*/
export function replacePrejoinAudioTrack(track: Object) {
return async (dispatch: Function, getState: Function) => {
const oldTrack = getAudioTrack(getState());
oldTrack && await oldTrack.dispose();
dispatch(addPrejoinAudioTrack(track));
};
}
/** /**
* Creates a new audio track based on a device id and replaces the current one. * Creates a new audio track based on a device id and replaces the current one.
* *
@ -323,11 +249,13 @@ export function replacePrejoinAudioTrack(track: Object) {
* @returns {Function} * @returns {Function}
*/ */
export function replaceAudioTrackById(deviceId: string) { export function replaceAudioTrackById(deviceId: string) {
return async (dispatch: Function) => { return async (dispatch: Function, getState: Function) => {
try { try {
const track = await createLocalTrack('audio', deviceId); const tracks = getState()['features/base/tracks'];
const newTrack = await createLocalTrack('audio', deviceId);
const oldTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
dispatch(replacePrejoinAudioTrack(track)); dispatch(replaceLocalTrack(oldTrack, newTrack));
} catch (err) { } catch (err) {
dispatch(setDeviceStatusWarning('prejoin.audioTrackError')); dispatch(setDeviceStatusWarning('prejoin.audioTrackError'));
logger.log('Error replacing audio track', err); logger.log('Error replacing audio track', err);
@ -335,21 +263,6 @@ export function replaceAudioTrackById(deviceId: string) {
}; };
} }
/**
* Replaces the existing video track with a new one.
*
* @param {Object} track - The new track.
* @returns {Function}
*/
export function replacePrejoinVideoTrack(track: Object) {
return async (dispatch: Function, getState: Function) => {
const oldTrack = getVideoTrack(getState());
oldTrack && await oldTrack.dispose();
dispatch(addPrejoinVideoTrack(track));
};
}
/** /**
* Creates a new video track based on a device id and replaces the current one. * Creates a new video track based on a device id and replaces the current one.
* *
@ -357,11 +270,13 @@ export function replacePrejoinVideoTrack(track: Object) {
* @returns {Function} * @returns {Function}
*/ */
export function replaceVideoTrackById(deviceId: Object) { export function replaceVideoTrackById(deviceId: Object) {
return async (dispatch: Function) => { return async (dispatch: Function, getState: Function) => {
try { try {
const track = await createLocalTrack('video', deviceId); const tracks = getState()['features/base/tracks'];
const newTrack = await createLocalTrack('video', deviceId);
const oldTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
dispatch(replacePrejoinVideoTrack(track)); dispatch(replaceLocalTrack(oldTrack, newTrack));
} catch (err) { } catch (err) {
dispatch(setDeviceStatusWarning('prejoin.videoTrackError')); dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));
logger.log('Error replacing video track', err); logger.log('Error replacing video track', err);
@ -369,58 +284,6 @@ export function replaceVideoTrackById(deviceId: Object) {
}; };
} }
/**
* Action used to mark audio muted.
*
* @param {boolean} value - True for muted.
* @returns {Object}
*/
export function setPrejoinAudioMuted(value: boolean) {
return {
type: SET_PREJOIN_AUDIO_MUTED,
value
};
}
/**
* Action used to mark video disabled.
*
* @param {boolean} value - True for muted.
* @returns {Object}
*/
export function setPrejoinVideoDisabled(value: boolean) {
return {
type: SET_PREJOIN_VIDEO_DISABLED,
value
};
}
/**
* Action used to mark video muted.
*
* @param {boolean} value - True for muted.
* @returns {Object}
*/
export function setPrejoinVideoMuted(value: boolean) {
return {
type: SET_PREJOIN_VIDEO_MUTED,
value
};
}
/**
* Action used to mark audio as disabled.
*
* @returns {Object}
*/
export function setAudioDisabled() {
return {
type: SET_PREJOIN_AUDIO_DISABLED
};
}
/** /**
* Sets the device status as OK with the corresponding text. * Sets the device status as OK with the corresponding text.
* *

View File

@ -6,9 +6,11 @@ import React, { Component } from 'react';
import { getRoomName } from '../../base/conference'; import { getRoomName } from '../../base/conference';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { Icon, IconPhone, IconVolumeOff } from '../../base/icons'; import { Icon, IconPhone, IconVolumeOff } from '../../base/icons';
import { isVideoMutedByUser } from '../../base/media';
import { ActionButton, InputField, PreMeetingScreen } from '../../base/premeeting'; import { ActionButton, InputField, PreMeetingScreen } from '../../base/premeeting';
import { connect } from '../../base/redux'; import { connect } from '../../base/redux';
import { getDisplayName, updateSettings } from '../../base/settings'; import { getDisplayName, updateSettings } from '../../base/settings';
import { getLocalJitsiVideoTrack } from '../../base/tracks';
import { import {
joinConference as joinConferenceAction, joinConference as joinConferenceAction,
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction, joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
@ -16,11 +18,9 @@ import {
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
} from '../actions'; } from '../actions';
import { import {
getActiveVideoTrack,
isJoinByPhoneButtonVisible, isJoinByPhoneButtonVisible,
isDeviceStatusVisible, isDeviceStatusVisible,
isJoinByPhoneDialogVisible, isJoinByPhoneDialogVisible
isPrejoinVideoMuted
} from '../functions'; } from '../functions';
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog'; import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
@ -315,8 +315,8 @@ function mapStateToProps(state): Object {
roomName: getRoomName(state), roomName: getRoomName(state),
showDialog: isJoinByPhoneDialogVisible(state), showDialog: isJoinByPhoneDialogVisible(state),
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state), hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
showCameraPreview: !isPrejoinVideoMuted(state), showCameraPreview: !isVideoMutedByUser(state),
videoTrack: getActiveVideoTrack(state) videoTrack: getLocalJitsiVideoTrack(state)
}; };
} }

View File

@ -2,25 +2,7 @@
import { getRoomName } from '../base/conference'; import { getRoomName } from '../base/conference';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions'; import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { isAudioMuted, isVideoMutedByUser } from '../base/media';
/**
* Mutes or unmutes a track.
*
* @param {Object} track - The track to be configured.
* @param {boolean} shouldMute - If it should mute or not.
* @returns {Promise<void>}
*/
function applyMuteOptionsToTrack(track, shouldMute) {
if (track.isMuted() === shouldMute) {
return;
}
if (shouldMute) {
return track.mute();
}
return track.unmute();
}
/** /**
* Selector for the visibility of the 'join by phone' button. * Selector for the visibility of the 'join by phone' button.
@ -39,73 +21,8 @@ export function isJoinByPhoneButtonVisible(state: Object): boolean {
* @returns {boolean} * @returns {boolean}
*/ */
export function isDeviceStatusVisible(state: Object): boolean { export function isDeviceStatusVisible(state: Object): boolean {
return !((isAudioDisabled(state) && isPrejoinVideoDisabled(state)) return !(isAudioMuted(state) && isVideoMutedByUser(state))
|| (isPrejoinAudioMuted(state) && isPrejoinVideoMuted(state))); && !state['features/base/config'].startSilent;
}
/**
* Selector for getting the active video/content sharing track.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function getActiveVideoTrack(state: Object): Object {
const track = getVideoTrack(state) || getContentSharingTrack(state);
if (track && track.isActive()) {
return track;
}
return null;
}
/**
* Returns a list with all the prejoin tracks configured according to
* user's preferences.
*
* @param {Object} state - The state of the app.
* @returns {Promise<Object[]>}
*/
export async function getAllPrejoinConfiguredTracks(state: Object): Promise<Object[]> {
const tracks = [];
const audioTrack = getAudioTrack(state);
const videoTrack = getVideoTrack(state);
const csTrack = getContentSharingTrack(state);
if (csTrack) {
tracks.push(csTrack);
} else if (videoTrack) {
await applyMuteOptionsToTrack(videoTrack, isPrejoinVideoMuted(state));
tracks.push(videoTrack);
}
if (audioTrack) {
await applyMuteOptionsToTrack(audioTrack, isPrejoinAudioMuted(state));
isPrejoinAudioMuted(state) && audioTrack.mute();
tracks.push(audioTrack);
}
return tracks;
}
/**
* Selector for getting the prejoin audio track.
*
* @param {Object} state - The state of the app.
* @returns {Object}
*/
export function getAudioTrack(state: Object): Object {
return state['features/prejoin']?.audioTrack;
}
/**
* Selector for getting the prejoin content sharing track.
*
* @param {Object} state - The state of the app.
* @returns {Object}
*/
export function getContentSharingTrack(state: Object): Object {
return state['features/prejoin']?.contentSharingTrack;
} }
/** /**
@ -181,36 +98,6 @@ export function getFullDialOutNumber(state: Object): string {
return `+${country.dialCode}${dialOutNumber}`; return `+${country.dialCode}${dialOutNumber}`;
} }
/**
* Selector for getting the prejoin video track.
*
* @param {Object} state - The state of the app.
* @returns {Object}
*/
export function getVideoTrack(state: Object): Object {
return state['features/prejoin']?.videoTrack;
}
/**
* Selector for getting the mute status of the prejoin audio.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinAudioMuted(state: Object): boolean {
return state['features/prejoin']?.audioMuted;
}
/**
* Selector for getting the mute status of the prejoin video.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinVideoMuted(state: Object): boolean {
return state['features/prejoin']?.videoMuted;
}
/** /**
* Selector for getting the error if any while creating streams. * Selector for getting the error if any while creating streams.
* *
@ -221,26 +108,6 @@ export function getRawError(state: Object): string {
return state['features/prejoin']?.rawError; return state['features/prejoin']?.rawError;
} }
/**
* Selector for getting state of the prejoin audio.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isAudioDisabled(state: Object): Object {
return state['features/prejoin']?.audioDisabled;
}
/**
* Selector for getting state of the prejoin video.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinVideoDisabled(state: Object): Object {
return state['features/prejoin']?.videoDisabled;
}
/** /**
* Selector for getting the visiblity state for the 'JoinByPhoneDialog'. * Selector for getting the visiblity state for the 'JoinByPhoneDialog'.
* *

View File

@ -1,16 +1,9 @@
// @flow // @flow
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { updateSettings } from '../base/settings'; import { updateSettings } from '../base/settings';
import { import { PREJOIN_START_CONFERENCE } from './actionTypes';
ADD_PREJOIN_AUDIO_TRACK,
ADD_PREJOIN_VIDEO_TRACK,
PREJOIN_START_CONFERENCE
} from './actionTypes';
import { setPrejoinAudioMuted, setPrejoinVideoMuted } from './actions';
import { getAllPrejoinConfiguredTracks } from './functions';
declare var APP: Object; declare var APP: Object;
@ -22,62 +15,22 @@ declare var APP: Object;
*/ */
MiddlewareRegistry.register(store => next => async action => { MiddlewareRegistry.register(store => next => async action => {
switch (action.type) { switch (action.type) {
case ADD_PREJOIN_AUDIO_TRACK: {
const { value: audioTrack } = action;
if (audioTrack) {
store.dispatch(
updateSettings({
micDeviceId: audioTrack.getDeviceId()
}),
);
}
break;
}
case ADD_PREJOIN_VIDEO_TRACK: {
const { value: videoTrack } = action;
if (videoTrack) {
store.dispatch(
updateSettings({
cameraDeviceId: videoTrack.getDeviceId()
}),
);
}
break;
}
case PREJOIN_START_CONFERENCE: { case PREJOIN_START_CONFERENCE: {
const { getState, dispatch } = store; const { getState, dispatch } = store;
const state = getState(); const state = getState();
const { userSelectedSkipPrejoin } = state['features/prejoin']; const { userSelectedSkipPrejoin } = state['features/prejoin'];
const tracks = state['features/base/tracks'];
userSelectedSkipPrejoin && dispatch(updateSettings({ userSelectedSkipPrejoin && dispatch(updateSettings({
userSelectedSkipPrejoin userSelectedSkipPrejoin
})); }));
APP.conference.prejoinStart(tracks.map(t => t.jitsiTrack));
const tracks = await getAllPrejoinConfiguredTracks(state);
APP.conference.prejoinStart(tracks);
break; break;
} }
case SET_AUDIO_MUTED: {
store.dispatch(setPrejoinAudioMuted(Boolean(action.muted)));
break;
} }
case SET_VIDEO_MUTED: {
store.dispatch(setPrejoinVideoMuted(Boolean(action.muted)));
break;
}
}
return next(action); return next(action);
}); });

View File

@ -1,28 +1,17 @@
import { ReducerRegistry } from '../base/redux'; import { ReducerRegistry } from '../base/redux';
import { import {
ADD_PREJOIN_AUDIO_TRACK,
ADD_PREJOIN_CONTENT_SHARING_TRACK,
ADD_PREJOIN_VIDEO_TRACK,
SET_DEVICE_STATUS, SET_DEVICE_STATUS,
SET_DIALOUT_NUMBER, SET_DIALOUT_NUMBER,
SET_DIALOUT_COUNTRY, SET_DIALOUT_COUNTRY,
SET_DIALOUT_STATUS, SET_DIALOUT_STATUS,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY, SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_SKIP_PREJOIN, SET_SKIP_PREJOIN,
SET_PREJOIN_AUDIO_DISABLED,
SET_PREJOIN_AUDIO_MUTED,
SET_PREJOIN_DEVICE_ERRORS, SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_PAGE_VISIBILITY, SET_PREJOIN_PAGE_VISIBILITY
SET_PREJOIN_VIDEO_DISABLED,
SET_PREJOIN_VIDEO_MUTED
} from './actionTypes'; } from './actionTypes';
const DEFAULT_STATE = { const DEFAULT_STATE = {
audioDisabled: false,
audioMuted: false,
audioTrack: null,
contentSharingTrack: null,
country: '', country: '',
deviceStatusText: 'prejoin.configuringDevices', deviceStatusText: 'prejoin.configuringDevices',
deviceStatusType: 'ok', deviceStatusType: 'ok',
@ -37,10 +26,7 @@ const DEFAULT_STATE = {
rawError: '', rawError: '',
showPrejoin: true, showPrejoin: true,
showJoinByPhoneDialog: false, showJoinByPhoneDialog: false,
userSelectedSkipPrejoin: false, userSelectedSkipPrejoin: false
videoTrack: null,
videoDisabled: false,
videoMuted: false
}; };
/** /**
@ -49,26 +35,6 @@ const DEFAULT_STATE = {
ReducerRegistry.register( ReducerRegistry.register(
'features/prejoin', (state = DEFAULT_STATE, action) => { 'features/prejoin', (state = DEFAULT_STATE, action) => {
switch (action.type) { switch (action.type) {
case ADD_PREJOIN_AUDIO_TRACK: {
return {
...state,
audioTrack: action.value
};
}
case ADD_PREJOIN_CONTENT_SHARING_TRACK: {
return {
...state,
contentSharingTrack: action.value
};
}
case ADD_PREJOIN_VIDEO_TRACK: {
return {
...state,
videoTrack: action.value
};
}
case SET_SKIP_PREJOIN: { case SET_SKIP_PREJOIN: {
return { return {
@ -83,25 +49,6 @@ ReducerRegistry.register(
showPrejoin: action.value showPrejoin: action.value
}; };
case SET_PREJOIN_VIDEO_DISABLED: {
return {
...state,
videoDisabled: action.value
};
}
case SET_PREJOIN_VIDEO_MUTED:
return {
...state,
videoMuted: action.value
};
case SET_PREJOIN_AUDIO_MUTED:
return {
...state,
audioMuted: action.value
};
case SET_PREJOIN_DEVICE_ERRORS: { case SET_PREJOIN_DEVICE_ERRORS: {
const status = getStatusFromErrors(action.value); const status = getStatusFromErrors(action.value);
@ -119,13 +66,6 @@ ReducerRegistry.register(
}; };
} }
case SET_PREJOIN_AUDIO_DISABLED: {
return {
...state,
audioDisabled: true
};
}
case SET_DIALOUT_NUMBER: { case SET_DIALOUT_NUMBER: {
return { return {
...state, ...state,

View File

@ -12,11 +12,6 @@ import { connect } from '../../base/redux';
import { AbstractAudioMuteButton } from '../../base/toolbox'; import { AbstractAudioMuteButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox'; import type { AbstractButtonProps } from '../../base/toolbox';
import { isLocalTrackMuted } from '../../base/tracks'; import { isLocalTrackMuted } from '../../base/tracks';
import {
isPrejoinAudioMuted,
isAudioDisabled,
isPrejoinPageVisible
} from '../../prejoin/functions';
import { muteLocal } from '../../remote-video-menu/actions'; import { muteLocal } from '../../remote-video-menu/actions';
declare var APP: Object; declare var APP: Object;
@ -154,18 +149,8 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
* }} * }}
*/ */
function _mapStateToProps(state): Object { function _mapStateToProps(state): Object {
let _audioMuted; const _audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
let _disabled; const _disabled = state['features/base/config'].startSilent;
if (isPrejoinPageVisible(state)) {
_audioMuted = isPrejoinAudioMuted(state);
_disabled = state['features/base/config'].startSilent;
} else {
const tracks = state['features/base/tracks'];
_audioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
_disabled = state['features/base/config'].startSilent || isAudioDisabled(state);
}
return { return {
_audioMuted, _audioMuted,

View File

@ -9,6 +9,7 @@ import {
sendAnalytics sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { setAudioOnly } from '../../base/audio-only'; import { setAudioOnly } from '../../base/audio-only';
import { hasAvailableDevices } from '../../base/devices';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { import {
VIDEO_MUTISM_AUTHORITY, VIDEO_MUTISM_AUTHORITY,
@ -18,11 +19,6 @@ import { connect } from '../../base/redux';
import { AbstractVideoMuteButton } from '../../base/toolbox'; import { AbstractVideoMuteButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox'; import type { AbstractButtonProps } from '../../base/toolbox';
import { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks'; import { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks';
import {
isPrejoinPageVisible,
isPrejoinVideoDisabled,
isPrejoinVideoMuted
} from '../../prejoin/functions';
declare var APP: Object; declare var APP: Object;
@ -191,19 +187,12 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
function _mapStateToProps(state): Object { function _mapStateToProps(state): Object {
const { enabled: audioOnly } = state['features/base/audio-only']; const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks']; const tracks = state['features/base/tracks'];
let _videoMuted = isLocalVideoTrackMuted(tracks);
let _videoDisabled = false;
if (isPrejoinPageVisible(state)) {
_videoMuted = isPrejoinVideoMuted(state);
_videoDisabled = isPrejoinVideoDisabled(state);
}
return { return {
_audioOnly: Boolean(audioOnly), _audioOnly: Boolean(audioOnly),
_videoDisabled, _videoDisabled: !hasAvailableDevices(state, 'videoInput'),
_videoMediaType: getLocalVideoType(tracks), _videoMediaType: getLocalVideoType(tracks),
_videoMuted _videoMuted: isLocalVideoTrackMuted(tracks)
}; };
} }

View File

@ -6,6 +6,7 @@ import { IconArrowDown } from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_'; import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox'; import { ToolboxButtonWithIcon } from '../../../base/toolbox';
import { getLocalJitsiVideoTrack } from '../../../base/tracks';
import { getMediaPermissionPromptVisibility } from '../../../overlay'; import { getMediaPermissionPromptVisibility } from '../../../overlay';
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings'; import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
import { isVideoSettingsButtonDisabled } from '../../functions'; import { isVideoSettingsButtonDisabled } from '../../functions';
@ -24,6 +25,11 @@ type Props = {
*/ */
permissionPromptVisibility: boolean, permissionPromptVisibility: boolean,
/**
* Whether there is a video track or not.
*/
hasVideoTrack: boolean,
/** /**
* If the button should be disabled * If the button should be disabled
*/ */
@ -66,6 +72,17 @@ class VideoSettingsButton extends Component<Props, State> {
}; };
} }
/**
* Returns true if the settings icon is disabled.
*
* @returns {boolean}
*/
_isIconDisabled() {
const { hasVideoTrack, isDisabled } = this.props;
return (!this.state.hasPermissions || isDisabled) && !hasVideoTrack;
}
/** /**
* Updates device permissions. * Updates device permissions.
* *
@ -116,14 +133,13 @@ class VideoSettingsButton extends Component<Props, State> {
* @inheritdoc * @inheritdoc
*/ */
render() { render() {
const { isDisabled, onVideoOptionsClick, visible } = this.props; const { onVideoOptionsClick, visible } = this.props;
const iconDisabled = !this.state.hasPermissions || isDisabled;
return visible ? ( return visible ? (
<VideoSettingsPopup> <VideoSettingsPopup>
<ToolboxButtonWithIcon <ToolboxButtonWithIcon
icon = { IconArrowDown } icon = { IconArrowDown }
iconDisabled = { iconDisabled } iconDisabled = { this._isIconDisabled() }
onIconClick = { onVideoOptionsClick }> onIconClick = { onVideoOptionsClick }>
<VideoMuteButton /> <VideoMuteButton />
</ToolboxButtonWithIcon> </ToolboxButtonWithIcon>
@ -140,6 +156,7 @@ class VideoSettingsButton extends Component<Props, State> {
*/ */
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)),
isDisabled: isVideoSettingsButtonDisabled(state), isDisabled: isVideoSettingsButtonDisabled(state),
permissionPromptVisibility: getMediaPermissionPromptVisibility(state) permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
}; };

View File

@ -1,11 +1,6 @@
// @flow // @flow
import { hasAvailableDevices } from '../base/devices'; import { hasAvailableDevices } from '../base/devices';
import {
isAudioDisabled,
isPrejoinPageVisible,
isPrejoinVideoDisabled
} from '../prejoin';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
@ -60,12 +55,9 @@ export function isToolboxVisible(state: Object) {
* @returns {boolean} * @returns {boolean}
*/ */
export function isAudioSettingsButtonDisabled(state: Object) { export function isAudioSettingsButtonDisabled(state: Object) {
const devicesMissing = !hasAvailableDevices(state, 'audioInput') return (!hasAvailableDevices(state, 'audioInput')
&& !hasAvailableDevices(state, 'audioOutput'); && !hasAvailableDevices(state, 'audioOutput'))
|| state['features/base/config'].startSilent;
return isPrejoinPageVisible(state)
? devicesMissing || isAudioDisabled(state)
: devicesMissing;
} }
/** /**
@ -75,9 +67,5 @@ export function isAudioSettingsButtonDisabled(state: Object) {
* @returns {boolean} * @returns {boolean}
*/ */
export function isVideoSettingsButtonDisabled(state: Object) { export function isVideoSettingsButtonDisabled(state: Object) {
const devicesMissing = !hasAvailableDevices(state, 'videoInput'); return !hasAvailableDevices(state, 'videoInput');
return isPrejoinPageVisible(state)
? devicesMissing || isPrejoinVideoDisabled(state)
: devicesMissing;
} }